diff options
| author | Roland Reichwein <mail@reichwein.it> | 2020-04-21 18:57:11 +0200 | 
|---|---|---|
| committer | Roland Reichwein <mail@reichwein.it> | 2020-04-21 18:57:11 +0200 | 
| commit | ef7ed9034bebe80a429112930ab0481c8aa66c95 (patch) | |
| tree | faf6d962cc3b33587122122a7cb4b161a0719a93 | |
| parent | 8b93572b386256c53c12012be87adabd73d80520 (diff) | |
Weblog (WIP)
| -rw-r--r-- | TODO | 5 | ||||
| -rw-r--r-- | debian/webserver.conf | 53 | ||||
| -rw-r--r-- | debian/webserver.install | 1 | ||||
| -rw-r--r-- | plugins/weblog/weblog.cpp | 260 | ||||
| -rw-r--r-- | server.cpp | 2 | ||||
| -rw-r--r-- | webserver.conf | 1 | 
6 files changed, 256 insertions, 66 deletions
@@ -1,9 +1,10 @@ - +Integrate into Debian +Ubuntu version  Request properties: Remote Address, e.g. [::1]:8081 -> ipv6 / ipv4  Speed up config.GetPath  read: The socket was closed due to a timeout  statistics -index page +file index page  webbox: Info if not selected: all  webbox: Copy function  wildcard ip listen diff --git a/debian/webserver.conf b/debian/webserver.conf new file mode 100644 index 0000000..55e7246 --- /dev/null +++ b/debian/webserver.conf @@ -0,0 +1,53 @@ +<webserver> + <user>www-data</user> + <group>www-data</group> + <threads>10</threads> + <plugin-directory>/usr/lib/webserver/plugins</plugin-directory> + <sites> +  <site> +   <name>antcom.de</name> +   <host>lists.antcom.de</host> +   <host>antcom.de</host> +   <host>www.antcom.de</host> +   <path requested="/"> +    <plugin>static-files</plugin> +    <target>/home/ernie/homepage/test</target> +   </path> +   <path requested="/webbox"> +    <plugin>static-files</plugin> +    <target>/home/ernie/code/webserver/plugins/webbox/html</target> +   </path> +   <path requested="/webbox/bin"> +    <plugin>webbox</plugin> +    <target>/home/ernie/testbox</target> +    <WEBBOX_NAME>Testbox1</WEBBOX_NAME> +    <WEBBOX_READONLY>1</WEBBOX_READONLY> +   </path> +   <certpath>/home/ernie/code/webserver/fullchain.pem</certpath> +   <keypath>/home/ernie/code/webserver/privkey.pem</keypath> +  </site> + </sites> + <sockets> +  <socket> +   <address>127.0.0.1</address> +   <port>80</port> +   <protocol>http</protocol> +  </socket> +  <socket> +   <address>::1</address> +   <port>80</port> +   <protocol>http</protocol> +  </socket> +  <socket> +   <address>127.0.0.1</address> +   <port>443</port> +   <protocol>https</protocol> +  </socket> +  <socket> +   <address>::1</address> +   <port>443</port> +   <protocol>https</protocol> +  </socket> + </sockets> +</webserver> + diff --git a/debian/webserver.install b/debian/webserver.install new file mode 100644 index 0000000..b4997f3 --- /dev/null +++ b/debian/webserver.install @@ -0,0 +1 @@ +debian/webserver.conf etc diff --git a/plugins/weblog/weblog.cpp b/plugins/weblog/weblog.cpp index 1c58b73..0c0ffcd 100644 --- a/plugins/weblog/weblog.cpp +++ b/plugins/weblog/weblog.cpp @@ -2,6 +2,7 @@  #include <boost/algorithm/string/predicate.hpp> +#include <algorithm>  #include <filesystem>  #include <fstream>  #include <iostream> @@ -12,79 +13,203 @@ namespace fs = std::filesystem;  namespace { -// Return a reasonable mime type based on the extension of a file. -std::string -mime_type(fs::path path) -{ - using boost::algorithm::iequals; - auto const ext = [&path] + const size_t number_of_articles_on_front_page {10}; + const std::string article_filename{"article.data"}; + + // Return a reasonable mime type based on the extension of a file. + std::string mime_type(fs::path path)   { -  size_t pos = path.string().rfind("."); -  if (pos == std::string::npos) -   return std::string{}; -  return path.string().substr(pos); - }(); - if(iequals(ext, ".htm"))  return "text/html"; // TODO: unordered_map - if(iequals(ext, ".html")) return "text/html"; - if(iequals(ext, ".php"))  return "text/html"; - if(iequals(ext, ".css"))  return "text/css"; - if(iequals(ext, ".txt"))  return "text/plain"; - if(iequals(ext, ".js"))   return "application/javascript"; - if(iequals(ext, ".json")) return "application/json"; - if(iequals(ext, ".xml"))  return "application/xml"; - if(iequals(ext, ".swf"))  return "application/x-shockwave-flash"; - if(iequals(ext, ".flv"))  return "video/x-flv"; - if(iequals(ext, ".png"))  return "image/png"; - if(iequals(ext, ".jpe"))  return "image/jpeg"; - if(iequals(ext, ".jpeg")) return "image/jpeg"; - if(iequals(ext, ".jpg"))  return "image/jpeg"; - if(iequals(ext, ".gif"))  return "image/gif"; - if(iequals(ext, ".bmp"))  return "image/bmp"; - if(iequals(ext, ".ico"))  return "image/vnd.microsoft.icon"; - if(iequals(ext, ".tiff")) return "image/tiff"; - if(iequals(ext, ".tif"))  return "image/tiff"; - if(iequals(ext, ".svg"))  return "image/svg+xml"; - if(iequals(ext, ".svgz")) return "image/svg+xml"; - return "application/text"; -} +  using boost::algorithm::iequals; +  auto const ext = [&path] +  { +   size_t pos = path.string().rfind("."); +   if (pos == std::string::npos) +    return std::string{}; +   return path.string().substr(pos); +  }(); +  if(iequals(ext, ".htm"))  return "text/html"; // TODO: unordered_map +  if(iequals(ext, ".html")) return "text/html"; +  if(iequals(ext, ".php"))  return "text/html"; +  if(iequals(ext, ".css"))  return "text/css"; +  if(iequals(ext, ".txt"))  return "text/plain"; +  if(iequals(ext, ".js"))   return "application/javascript"; +  if(iequals(ext, ".json")) return "application/json"; +  if(iequals(ext, ".xml"))  return "application/xml"; +  if(iequals(ext, ".swf"))  return "application/x-shockwave-flash"; +  if(iequals(ext, ".flv"))  return "video/x-flv"; +  if(iequals(ext, ".png"))  return "image/png"; +  if(iequals(ext, ".jpe"))  return "image/jpeg"; +  if(iequals(ext, ".jpeg")) return "image/jpeg"; +  if(iequals(ext, ".jpg"))  return "image/jpeg"; +  if(iequals(ext, ".gif"))  return "image/gif"; +  if(iequals(ext, ".bmp"))  return "image/bmp"; +  if(iequals(ext, ".ico"))  return "image/vnd.microsoft.icon"; +  if(iequals(ext, ".tiff")) return "image/tiff"; +  if(iequals(ext, ".tif"))  return "image/tiff"; +  if(iequals(ext, ".svg"))  return "image/svg+xml"; +  if(iequals(ext, ".svgz")) return "image/svg+xml"; +  return "application/text"; + } -std::string getFile(const fs::path& filename) -{ - std::ifstream file(filename.string(), std::ios::in | std::ios::binary | std::ios::ate); + // 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; + } - if (file.is_open()) { -  std::ifstream::pos_type fileSize = file.tellg(); -  file.seekg(0, std::ios::beg); + std::string getFile(const fs::path& filename) + { +  std::ifstream file(filename.string(), std::ios::in | std::ios::binary | std::ios::ate); + +  if (file.is_open()) { +   std::ifstream::pos_type fileSize = file.tellg(); +   file.seekg(0, std::ios::beg); -  std::string bytes(fileSize, ' '); -  file.read(reinterpret_cast<char*>(bytes.data()), fileSize); +   std::string bytes(fileSize, ' '); +   file.read(reinterpret_cast<char*>(bytes.data()), fileSize); -  return bytes; +   return bytes; - } else { -  throw std::runtime_error("Opening "s + filename.string() + " for reading"); +  } else { +   throw std::runtime_error("Opening "s + filename.string() + " for reading"); +  }   } -} -bool is_index_page(std::string& path) -{ - return (path.size() == 0 || path.back() == '/'); -} + bool is_index_page(std::string& rel_target) + { +  return (rel_target.size() == 0 || rel_target == "/"); + } -std::string generateIndexPage(std::function<plugin_interface_setter_type>& SetResponseHeader) -{ - return "<html><body><h1>Blog</h1></body></html>"; -} + bool is_article_page(std::string& rel_target, fs::path& path) + { +  return (rel_target.size() >= 2 && rel_target.back() == '/' && fs::is_directory(path)); + } -// 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; -} + bool is_article_file(std::string& rel_target, fs::path& path) + { +  return (fs::is_regular_file(path) && path.filename().string() != article_filename); + } -} + struct ArticleInfo + { +  fs::path path; +  std::string subject; +  std::string date; + }; + + // get article metadata from header lines + std::unordered_map<std::string, std::string> getMetaData(fs::path path) + { +  if (path.string().size() > 0 && path.string().back() == '/') { +   std::string s {path.string()}; +   path = s.substr(0, s.size() - 1); +  } +  std::unordered_map<std::string, std::string> result; + +  std::string pathname{path.filename().string()}; +  // ISO date +  std::string date{pathname.substr(0, 4) + "-"s + pathname.substr(4, 2) + "-"s + pathname.substr(6, 2)}; +  std::string time{pathname.substr(9, 2) + ":"s + pathname.substr(11, 2)}; +  if (time != "00:00") +   date += " " + time; + +  result["Date"] = date; + +  fs::path filepath {path / article_filename}; + +  std::ifstream file(filepath.string(), std::ios::in); + +  if (file.is_open()) { +   std::string line; +   while (!file.eof()) { +    std::getline(file, line); +    if (line.empty()) // found header end +     break; +    size_t pos {line.find(": ")}; +    if (pos == line.npos) { +     std::cerr << "Warning: Found bad header line in " << filepath << ": " << line << std::endl; +     continue; +    } +    result[line.substr(0, pos)] = line.substr(pos + 2); +   } +   return result; + +  } else { +   throw std::runtime_error("Opening "s + filepath.string() + " for reading"); +  } + } + + std::vector<ArticleInfo> getArticleList(fs::path& path) + { +  std::vector<ArticleInfo> result; + +  for (auto& year_entry: fs::directory_iterator(path)) { +   for (auto& entry: fs::directory_iterator(year_entry.path())) { +    auto metaData{getMetaData(entry.path())}; +    result.emplace_back(ArticleInfo{entry.path(), metaData.at("Subject"), metaData.at("Date")}); +   } +  } + +  size_t size{std::min(number_of_articles_on_front_page, result.size())}; +  // sort backwards +  std::partial_sort(result.begin(), result.begin() + size, result.end(), [](const ArticleInfo& a0, const ArticleInfo& a1){ return a0.date > a1.date;}); +   +  return {result.begin(), result.begin() + size}; + } + + std::string generateIndexPage(fs::path& path, +                               std::function<std::string(const std::string& key)>& GetRequestParam, +                               std::function<plugin_interface_setter_type>& SetResponseHeader) + { +  try { +   std::string result{"<html><body><h1>"s + GetRequestParam("WEBLOG_NAME") + "</h1>"s}; +    +   fs::path link{ GetRequestParam("rel_target")}; +    +   auto list{getArticleList(path)}; +   for (const auto& article: list) { +    result += "<h2><a href=\"" + (link / article.path.filename()).string() + "/\">"s + article.subject + "</a></h2>"s + article.date + "<br/>"s; +   } +   result += "</body></html>"; +   return result; +  } catch (const std::exception& ex) { +   return HttpStatus("500", "Reading Index page: "s + ex.what(), SetResponseHeader); +  } + } + + std::string generateArticlePage(fs::path& path, +                                 std::function<plugin_interface_setter_type>& SetResponseHeader) + { +  try { +   auto metaData{getMetaData(path)}; + +   std::string data { getFile(path / article_filename)}; + +   size_t pos {data.find("\n\n")}; +   if (pos == data.npos) +    throw std::runtime_error("Error parsing article"); +    +   std::string result { "<html><body><h1>"s + metaData.at("Subject") + "</h1>"s + metaData.at("Date") + "<br/><br/>"s + data.substr(pos + 2) + "</body></html>"s}; + +   return result; +  } catch (const std::exception& ex) { +   return HttpStatus("500", "Reading Article: "s + ex.what(), SetResponseHeader); +  } + } + + std::string generateArticleFile(fs::path& path, std::function<plugin_interface_setter_type>& SetResponseHeader) + { +  try { +   SetResponseHeader("content_type", mime_type(path)); +   return getFile(path); +  } catch (const std::exception& ex) { +   return HttpStatus("500", "Reading Article file: "s + ex.what(), SetResponseHeader); +  } + } + +} // anonymous namespace  std::string weblog_plugin::name()  { @@ -122,6 +247,9 @@ std::string weblog_plugin::generate_page(    // Build the path to the requested file    std::string doc_root{GetRequestParam("doc_root")}; +  if (rel_target.size() >= 4 && std::for_each(rel_target.begin(), rel_target.begin() + 4, isdigit)) { +   rel_target = rel_target.substr(0, 4) + "/" + rel_target; +  }    fs::path path {fs::path{doc_root} / rel_target};    if (target.size() && target.back() != '/' && fs::is_directory(path)) {     std::string location{GetRequestParam("location") + "/"s}; @@ -132,7 +260,13 @@ std::string weblog_plugin::generate_page(    SetResponseHeader("content_type", "text/html");    if (is_index_page(rel_target)) -   return generateIndexPage(SetResponseHeader); +   return generateIndexPage(path, GetRequestParam, SetResponseHeader); + +  if (is_article_page(rel_target, path)) +   return generateArticlePage(path, SetResponseHeader); + +  if (is_article_file(rel_target, path)) +   return generateArticleFile(path, SetResponseHeader);    return HttpStatus("404", "Bad path specification: "s + rel_target, SetResponseHeader); @@ -35,7 +35,7 @@ namespace net = boost::asio;            // from <boost/asio.hpp>  namespace ssl = boost::asio::ssl;       // from <boost/asio/ssl.hpp>  using tcp = boost::asio::ip::tcp;       // from <boost/asio/ip/tcp.hpp> -const std::string Server::VersionString{ "Webserver "s + std::string{VERSION} }; +const std::string Server::VersionString{ "Reichwein.IT Webserver "s + std::string{VERSION} };  Server::Server(Config& config, boost::asio::io_context& ioc, const Socket& socket, plugins_container_type& plugins)   : m_config(config) diff --git a/webserver.conf b/webserver.conf index 16ac3e0..4aa0daa 100644 --- a/webserver.conf +++ b/webserver.conf @@ -34,6 +34,7 @@     <path requested="/blog">      <plugin>weblog</plugin>      <target>/home/ernie/testblog</target> +    <WEBLOG_NAME>Roland Reichweins Blog</WEBLOG_NAME>     </path>     <path requested="/cgi-bin">      <plugin>cgi</plugin>  | 
