diff options
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | TODO | 2 | ||||
| -rw-r--r-- | plugins/statistics/Makefile | 124 | ||||
| -rw-r--r-- | plugins/statistics/statistics.cpp | 97 | ||||
| -rw-r--r-- | plugins/statistics/statistics.h | 21 | ||||
| -rw-r--r-- | response.cpp | 63 | ||||
| -rw-r--r-- | server.cpp | 15 | ||||
| -rw-r--r-- | statistics.cpp | 69 | ||||
| -rw-r--r-- | statistics.h | 5 | ||||
| -rw-r--r-- | webserver.conf | 4 | 
10 files changed, 353 insertions, 49 deletions
@@ -1,7 +1,7 @@  DISTROS=debian10  VERSION=$(shell dpkg-parsechangelog --show-field Version)  PROJECTNAME=webserver -PLUGINS=static-files webbox cgi weblog # fcgi +PLUGINS=static-files webbox cgi weblog statistics # fcgi  CXX=clang++-10 @@ -1,3 +1,4 @@ +Upload: read: body limit exceeded  weblog: blättern  weblog: link consistency check (cron?)  weblog: style: zitate @@ -5,7 +6,6 @@ Integrate into Debian  Ubuntu version  Speed up config.GetPath  read: The socket was closed due to a timeout -print statistics  webbox: Info if not selected: all  webbox: Copy function  crypt pws diff --git a/plugins/statistics/Makefile b/plugins/statistics/Makefile new file mode 100644 index 0000000..48c2e8c --- /dev/null +++ b/plugins/statistics/Makefile @@ -0,0 +1,124 @@ +DISTROS=debian10 +VERSION=$(shell dpkg-parsechangelog --show-field Version) +PROJECTNAME=statistics + +CXX=clang++-10 + +ifeq ($(shell which $(CXX)),) +CXX=clang++ +endif + +ifeq ($(shell which $(CXX)),) +CXX=g++-9 +endif + +ifeq ($(CXXFLAGS),) +#CXXFLAGS=-O2 -DNDEBUG +CXXFLAGS=-O0 -g -D_DEBUG +endif +# -fprofile-instr-generate -fcoverage-mapping +# gcc:--coverage + +CXXFLAGS+= -Wall -I. + +CXXFLAGS+= -pthread -fvisibility=hidden -fPIC +ifeq ($(CXX),clang++-10) +CXXFLAGS+=-std=c++20 #-stdlib=libc++ +else +CXXFLAGS+=-std=c++17 +endif + +CXXTESTFLAGS=-Igoogletest/include -Igooglemock/include/ -Igoogletest -Igooglemock + +LIBS=\ +-lboost_context \ +-lboost_coroutine \ +-lboost_program_options \ +-lboost_system \ +-lboost_thread \ +-lboost_filesystem \ +-lboost_regex \ +-lpthread \ +-lssl -lcrypto \ +-ldl + +ifeq ($(CXX),clang++-10) +LIBS+= \ +-fuse-ld=lld-10 \ +-lstdc++ +#-lc++ \ +#-lc++abi +#-lc++fs +#-lstdc++fs +else +LIBS+= \ +-lstdc++ \ +-lstdc++fs +endif + +PROGSRC=\ +    statistics.cpp + +TESTSRC=\ +    test-webserver.cpp \ +    googlemock/src/gmock-all.cpp \ +    googletest/src/gtest-all.cpp \ +    $(PROGSRC) + +SRC=$(PROGSRC) + +all: $(PROJECTNAME).so + +# testsuite ---------------------------------------------- +test-$(PROJECTNAME): $(TESTSRC:.cpp=.o) +	$(CXX) $(CXXFLAGS) $^ $(LIBS) -o $@ + +$(PROJECTNAME).so: $(SRC:.cpp=.o) +	$(CXX) -shared $(CXXFLAGS) $^ $(LIBS) -o $@ + +dep: $(TESTSRC:.cpp=.d) + +%.d: %.cpp +	$(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -MM -MP -MF $@ -c $< + +%.o: %.cpp %.d +	$(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@ + +googletest/src/%.o: googletest/src/%.cc +	$(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@ + +# dependencies + +ADD_DEP=Makefile + +install: +	mkdir -p $(DESTDIR)/usr/lib/webserver/plugins +	cp $(PROJECTNAME).so $(DESTDIR)/usr/lib/webserver/plugins + +# misc --------------------------------------------------- +deb: +	# build binary deb package +	dpkg-buildpackage -us -uc -rfakeroot + +deb-src: +	dpkg-source -b . + +$(DISTROS): deb-src +	sudo pbuilder build --basetgz /var/cache/pbuilder/$@.tgz --buildresult result/$@ ../webserver_$(VERSION).dsc ; \ + +debs: $(DISTROS) + +clean: +	-rm -f test-$(PROJECTNAME) $(PROJECTNAME) +	-find . -name '*.o' -o -name '*.so' -o -name '*.d' -o -name '*.gcno' -o -name '*.gcda' | xargs rm -f + +zip: clean +	-rm -f ../$(PROJECTNAME).zip +	zip -r ../$(PROJECTNAME).zip * +	ls -l ../$(PROJECTNAME).zip + + + +.PHONY: clean all zip install deb deb-src debs all $(DISTROS) + +-include $(wildcard $(SRC:.cpp=.d)) diff --git a/plugins/statistics/statistics.cpp b/plugins/statistics/statistics.cpp new file mode 100644 index 0000000..03f4c94 --- /dev/null +++ b/plugins/statistics/statistics.cpp @@ -0,0 +1,97 @@ +#include "statistics.h" + +#include <boost/algorithm/string/predicate.hpp> +#include <boost/coroutine2/coroutine.hpp> +#include <boost/process.hpp> + +#include <algorithm> +#include <filesystem> +#include <fstream> +#include <iostream> +#include <string> +#include <unordered_map> + +using namespace std::string_literals; +namespace bp = boost::process; +namespace fs = std::filesystem; + +namespace { + + // Used to return errors by generating response page and HTTP status code + std::string HttpStatus(std::string status, std::string message, std::function<plugin_interface_setter_type>& SetResponseHeader) + { +  SetResponseHeader("status", status); +  SetResponseHeader("content_type", "text/html"); +  return status + " " + message; + } + +} // anonymous namespace + +std::string statistics_plugin::name() +{ + return "statistics"; +} + +statistics_plugin::statistics_plugin() +{ + //std::cout << "Plugin constructor" << std::endl; +} + +statistics_plugin::~statistics_plugin() +{ + //std::cout << "Plugin destructor" << std::endl; +} + +std::string statistics_plugin::generate_page( +  std::function<std::string(const std::string& key)>& GetServerParam, +  std::function<std::string(const std::string& key)>& GetRequestParam, // request including body (POST...) +  std::function<void(const std::string& key, const std::string& value)>& SetResponseHeader // to be added to result string +) +{ + try { +  // Request path must not contain "..". +  std::string rel_target{GetRequestParam("rel_target")}; +  size_t query_pos{rel_target.find("?")}; +  if (query_pos != rel_target.npos) +   rel_target = rel_target.substr(0, query_pos); + +  std::string target{GetRequestParam("target")}; +  if (rel_target.find("..") != std::string::npos) { +   return HttpStatus("400", "Illegal request: "s + target, SetResponseHeader); +  } + +  // Build the path to the requested file +  std::string doc_root{GetRequestParam("doc_root")}; +  fs::path path {fs::path{doc_root} / rel_target}; +  if (target.size() && target.back() != '/' && fs::is_directory(path)) { +   std::string location{GetRequestParam("location") + "/"s}; +   SetResponseHeader("location", location); +   return HttpStatus("301", "Correcting directory path", SetResponseHeader); +  } + +  try { +   SetResponseHeader("content_type", "text/html"); + +   std::string header {"<!DOCTYPE html><html><head>" +             "<meta charset=\"utf-8\"/>" +             "<title>Webserver Statistics</title>" +             "</head><body>"}; +   std::string footer{"<br/><br/><br/></body></html>"}; + +   std::string result{header}; + +   result += "<h1>Webserver Statistics</h1>"; +   result += "<pre>"s + GetServerParam("statistics") + "</pre>"s; + +   result += footer; +    +   return result; +  } catch (const std::exception& ex) { +   return HttpStatus("500", "Statistics error: "s + ex.what(), SetResponseHeader); +  } + + } catch (const std::exception& ex) { +  return HttpStatus("500", "Unknown Error: "s + ex.what(), SetResponseHeader); + } +} + diff --git a/plugins/statistics/statistics.h b/plugins/statistics/statistics.h new file mode 100644 index 0000000..5db309b --- /dev/null +++ b/plugins/statistics/statistics.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../../plugin_interface.h" + +class statistics_plugin: public webserver_plugin_interface  +{ +public: + statistics_plugin(); + ~statistics_plugin(); +  + std::string name(); + std::string generate_page( +  std::function<std::string(const std::string& key)>& GetServerParam, +  std::function<std::string(const std::string& key)>& GetRequestParam, // request including body (POST...) +  std::function<void(const std::string& key, const std::string& value)>& SetResponseHeader // to be added to result string + ); + +}; + +extern "C" BOOST_SYMBOL_EXPORT statistics_plugin webserver_plugin; +statistics_plugin webserver_plugin; diff --git a/response.cpp b/response.cpp index c5ba426..696b859 100644 --- a/response.cpp +++ b/response.cpp @@ -79,10 +79,11 @@ bool is_ipv6_address(const std::string& addr)  std::unordered_map<std::string, std::function<std::string(Server&)>> GetServerParamFunctions{   // following are the supported fields: - {"version", [](Server& server) { return Server::VersionString; }},   {"address", [](Server& server) { return server.GetSocket().address; }},   {"ipv6", [](Server& server) { return is_ipv6_address(server.GetSocket().address) ? "yes" : "no"; }},   {"port", [](Server& server) { return server.GetSocket().port; }}, + {"statistics", [](Server& server) { return server.GetStatistics().getValues(); }}, + {"version", [](Server& server) { return Server::VersionString; }},  };  std::string GetServerParam(const std::string& key, Server& server) @@ -233,42 +234,34 @@ mime_type(beast::string_view path)      return "application/text";  } -// Used to return errors by generating response page and HTTP status code  response_type HttpStatus(std::string status, std::string message, response_type& res)  { - res.result(unsigned(stoul(status))); - res.set(http::field::content_type, "text/html"); - if (res.result_int() == 401) -  res.set(http::field::www_authenticate, "Basic realm=\"Webbox Login\""); - res.body() = "<html><body><h1>"s + Server::VersionString + " Error</h1><p>"s + status + " "s + message + "</p></body></html>"s; - res.prepare_payload(); + if (status != "200") { // already handled at res init +  res.result(unsigned(stoul(status))); +  res.set(http::field::content_type, "text/html"); +  if (res.result_int() == 401) +   res.set(http::field::www_authenticate, "Basic realm=\"Webbox Login\""); +  res.body() = "<html><body><h1>"s + Server::VersionString + " Error</h1><p>"s + status + " "s + message + "</p></body></html>"s; +  res.prepare_payload(); + }   return res;  } -// Do statistics at end of response generation, handle all exit paths via RAII -class StatisticsGuard +// Used to return errors by generating response page and HTTP status code +response_type HttpStatusAndStats(std::string status, std::string message, RequestContext& req_ctx, response_type& res)  { - request_type& mReq; - response_type& mRes; - Server& mServer; -public: - StatisticsGuard(request_type& req, response_type& res, Server& server) -  : mReq(req) -  , mRes(res) -  , mServer(server) - { - } + HttpStatus(status, message, res); - ~StatisticsGuard() - { -  mServer.GetStatistics().count(mReq.body().size(), -                                mRes.body().size(), -                                mRes.result_int() == 200, -                                is_ipv6_address(mServer.GetSocket().address), -                                mServer.GetSocket().protocol == SocketProtocol::HTTPS); - } -}; + req_ctx.GetServer().GetStatistics().count( +   req_ctx.GetReq().body().size(), +   res.body().size(), +   res.result_int() != 200, +   is_ipv6_address(req_ctx.GetServer().GetSocket().address), +   req_ctx.GetServer().GetSocket().protocol == SocketProtocol::HTTPS); + + return std::move(res); +}  } // anonymous namespace @@ -279,8 +272,6 @@ response_type generate_response(request_type& req, Server& server)   res.set(http::field::content_type, mime_type(extend_index_html(std::string(req.target()))));   res.keep_alive(req.keep_alive()); - StatisticsGuard statsGuard{req, res, server}; -   try {    RequestContext req_ctx{req, server}; // can throw std::out_of_range @@ -288,21 +279,21 @@ response_type generate_response(request_type& req, Server& server)    if (auth.size() != 0) {     std::string authorization{req[http::field::authorization]};     if (authorization.substr(0, 6) != "Basic "s) -    return HttpStatus("401", "Bad Authorization Type", res); +    return HttpStatusAndStats("401", "Bad Authorization Type", req_ctx, res);     authorization = authorization.substr(6);     authorization = decode64(authorization);     size_t pos {authorization.find(':')};     if (pos == authorization.npos) -    return HttpStatus("401", "Bad Authorization Encoding", res); +    return HttpStatusAndStats("401", "Bad Authorization Encoding", req_ctx, res);     std::string login{authorization.substr(0, pos)};     std::string password{authorization.substr(pos + 1)};     auto it {auth.find(login)};     if (it == auth.end() || it->second != password) -    return HttpStatus("401", "Bad Authorization", res); +    return HttpStatusAndStats("401", "Bad Authorization", req_ctx, res);    }    plugin_type plugin{req_ctx.GetPlugin()}; @@ -318,8 +309,8 @@ response_type generate_response(request_type& req, Server& server)     res.body() = res_data;     res.prepare_payload();    } - -  return res; +   +  return HttpStatusAndStats("200", "OK", req_ctx, res);   } catch(const std::out_of_range& ex) {    return HttpStatus("400", "Bad request: Host "s + std::string{req["host"]} + ":"s + std::string{req.target()} + " unknown"s, res);   } catch(const std::exception& ex) { @@ -20,6 +20,7 @@  #include <boost/config.hpp>  #include <exception> +#include <functional>  #include <iostream>  #include <thread>  #include <vector> @@ -39,6 +40,10 @@ using tcp = boost::asio::ip::tcp;       // from <boost/asio/ip/tcp.hpp>  const std::string Server::VersionString{ "Reichwein.IT Webserver "s + std::string{VERSION} }; +namespace { + const int32_t stats_timer_seconds { 24 * 60 * 60 }; // save stats once a day +} // anonymous namespace +  Server::Server(Config& config, boost::asio::io_context& ioc, const Socket& socket, plugins_container_type& plugins, Statistics& statistics)   : m_config(config)   , m_ioc(ioc) @@ -66,6 +71,16 @@ int run_server(Config& config, plugins_container_type& plugins)                      ioc.stop();                      }); + // Save stats once a day + boost::asio::steady_timer stats_save_timer(ioc, boost::asio::chrono::seconds(stats_timer_seconds)); + std::function<void(const boost::system::error_code&)> stats_callback = +   [&](const boost::system::error_code& error){ +         stats.save(); +         stats_save_timer.expires_at(stats_save_timer.expires_at() + boost::asio::chrono::seconds(stats_timer_seconds)); +         stats_save_timer.async_wait(stats_callback); +   }; + stats_save_timer.async_wait(stats_callback); +   std::vector<std::shared_ptr<Server>> servers;   const auto& sockets {config.Sockets()}; diff --git a/statistics.cpp b/statistics.cpp index 19d0258..3fb99a3 100644 --- a/statistics.cpp +++ b/statistics.cpp @@ -5,13 +5,15 @@  #include <iostream>  namespace fs = std::filesystem; +using namespace std::string_literals;  namespace {   const fs::path statsfilepath{ "/var/lib/webserver/stats.db" };  } // anonymous namespace -Statistics::Statistics() +void Statistics::load()  { + std::lock_guard<std::mutex> lock(mMutex);   std::cout << "Loading statistics..." << std::endl;   std::ifstream file{statsfilepath, std::ios::in | std::ios::binary};   if (file.is_open()) { @@ -21,22 +23,38 @@ Statistics::Statistics()   } else {    std::cerr << "Warning: Couldn't read statistics" << std::endl;   } + + mChanged = false;  } -Statistics::~Statistics() +void Statistics::save()  { - std::cout << "Saving statistics..." << std::endl; - std::lock_guard<std::mutex> lock(mMutex); - std::ofstream file{statsfilepath, std::ios::out | std::ios::binary | std::ios::trunc}; - if (file.is_open()) { -  Serialization::OArchive archive{file}; - -  archive << mBins; - } else { -  std::cerr << "Warning: Couldn't write statistics" << std::endl; + if (mChanged) { +  std::lock_guard<std::mutex> lock(mMutex); +  std::cout << "Saving statistics..." << std::endl; +  std::ofstream file{statsfilepath, std::ios::out | std::ios::binary | std::ios::trunc}; +  if (file.is_open()) { +   Serialization::OArchive archive{file}; + +   archive << mBins; +  } else { +   std::cerr << "Warning: Couldn't write statistics" << std::endl; +  } + +  mChanged = false;   }  } +Statistics::Statistics() +{ + load(); +} + +Statistics::~Statistics() +{ + save(); +} +  bool Statistics::Bin::expired() const  {   auto now {time(nullptr)}; @@ -57,6 +75,8 @@ void Statistics::count(size_t bytes_in, size_t bytes_out, bool error, bool ipv6,  {   std::lock_guard<std::mutex> lock(mMutex); + mChanged = true; +   if (mBins.empty() || mBins.back().expired()) {    mBins.emplace_back(Bin{static_cast<uint64_t>((time(nullptr) / binsize) * binsize), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});   } @@ -85,3 +105,30 @@ void Statistics::count(size_t bytes_in, size_t bytes_out, bool error, bool ipv6,   limit();  } +std::string Statistics::getValues() +{ + std::lock_guard<std::mutex> lock(mMutex); + + std::string result; + + for (const auto& bin: mBins) { +  result += std::to_string(bin.start_time) + ","s + + +            std::to_string(bin.requests) + ","s + +            std::to_string(bin.errors) + ","s + +            std::to_string(bin.bytes_in) + ","s + +            std::to_string(bin.bytes_out) + ","s + + +            std::to_string(bin.requests_ipv6) + ","s + +            std::to_string(bin.errors_ipv6) + ","s + +            std::to_string(bin.bytes_in_ipv6) + ","s + +            std::to_string(bin.bytes_out_ipv6) + ","s + + +            std::to_string(bin.requests_https) + ","s + +            std::to_string(bin.errors_https) + ","s + +            std::to_string(bin.bytes_in_https) + ","s + +            std::to_string(bin.bytes_out_https) + "\n"s; + } + + return result; +} diff --git a/statistics.h b/statistics.h index c4fce93..7e4da7e 100644 --- a/statistics.h +++ b/statistics.h @@ -60,9 +60,11 @@ public:   };  private: + bool mChanged{};   std::deque<Bin> mBins;   std::mutex mMutex; + void load();   void limit();  public: @@ -70,6 +72,9 @@ public:   ~Statistics();   void count(size_t bytes_in, size_t bytes_out, bool error, bool ipv6, bool https); + void save(); + + std::string getValues();  };  // Serialization and Deserialization as free functions diff --git a/webserver.conf b/webserver.conf index 52075a2..5adc9ba 100644 --- a/webserver.conf +++ b/webserver.conf @@ -38,6 +38,10 @@      <WEBLOG_DESCRIPTION>Roland Reichweins Blog</WEBLOG_DESCRIPTION>      <WEBLOG_KEYWORDS>Roland Reichwein, Blog</WEBLOG_KEYWORDS>     </path> +   <path requested="/stats"> +    <plugin>statistics</plugin> +    <target></target> +   </path>     <path requested="/cgi-bin">      <plugin>cgi</plugin>      <target>/home/ernie/code/webserver/cgi-bin</target>  | 
