diff options
| author | Roland Reichwein <mail@reichwein.it> | 2020-05-01 11:19:21 +0200 | 
|---|---|---|
| committer | Roland Reichwein <mail@reichwein.it> | 2020-05-01 11:19:21 +0200 | 
| commit | b77bb246e366d346b55cc8cfb4f1d0ac83211ae7 (patch) | |
| tree | d3fda755578a4fb4084e2b4f02252c498201ed0e | |
| parent | 82558fb7bfd7b0a36119d23bcd47d4cd4bd7b970 (diff) | |
Added fcgi module (WIP)
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | TODO | 1 | ||||
| -rw-r--r-- | plugins/cgi/cgi.cpp | 6 | ||||
| -rw-r--r-- | plugins/fcgi/Makefile | 124 | ||||
| -rw-r--r-- | plugins/fcgi/fastcgi.h | 136 | ||||
| -rw-r--r-- | plugins/fcgi/fcgi.cpp | 284 | ||||
| -rw-r--r-- | plugins/fcgi/fcgi.h | 21 | ||||
| -rw-r--r-- | webserver.conf | 4 | 
8 files changed, 574 insertions, 4 deletions
@@ -1,7 +1,7 @@  DISTROS=debian10 ubuntu1910  VERSION=$(shell dpkg-parsechangelog --show-field Version)  PROJECTNAME=webserver -PLUGINS=static-files webbox cgi weblog statistics # fcgi +PLUGINS=static-files webbox cgi weblog statistics fcgi  CXX=clang++-10 @@ -8,3 +8,4 @@ webbox: Copy function  config consistency: check double keys  new plugin: redirect-plugin  new plugin: fcgi +support alternative SSL libs: mbedtls, gnutls, wolfssl, botan, (matrixssl, libressl, cryptlib: not in debian) diff --git a/plugins/cgi/cgi.cpp b/plugins/cgi/cgi.cpp index 318dd1f..75e401e 100644 --- a/plugins/cgi/cgi.cpp +++ b/plugins/cgi/cgi.cpp @@ -173,7 +173,7 @@ namespace {    env["HTTPS"] = c.GetRequestParam("https");   } - std::string executeFile(const fs::path& filename, CGIContext& context) + std::string executeFile(CGIContext& context)   {    bp::pipe is_in;    bp::ipstream is_out; @@ -181,7 +181,7 @@ namespace {    bp::environment env {boost::this_process::environment()};    setCGIEnvironment(env, context); -  bp::child child(filename.string(), env, bp::std_out > is_out, bp::std_err > stderr, bp::std_in < is_in); +  bp::child child(context.path.string(), env, bp::std_out > is_out, bp::std_err > stderr, bp::std_in < is_in);    std::string body{ context.GetRequestParam("body") };    is_in.write(body.data(), body.size()); @@ -311,7 +311,7 @@ std::string cgi_plugin::generate_page(    CGIContext context(GetServerParam, GetRequestParam, SetResponseHeader, path, file_path, path_info);    try { -   return executeFile(path, context); +   return executeFile(context);    } catch (const std::runtime_error& ex) {     return HttpStatus("404", "Not found: "s + GetRequestParam("target"), SetResponseHeader);    } catch (const std::exception& ex) { diff --git a/plugins/fcgi/Makefile b/plugins/fcgi/Makefile new file mode 100644 index 0000000..bb54d7c --- /dev/null +++ b/plugins/fcgi/Makefile @@ -0,0 +1,124 @@ +DISTROS=debian10 +VERSION=$(shell dpkg-parsechangelog --show-field Version) +PROJECTNAME=fcgi + +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=\ +    fcgi.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/fcgi/fastcgi.h b/plugins/fcgi/fastcgi.h new file mode 100644 index 0000000..d5b5468 --- /dev/null +++ b/plugins/fcgi/fastcgi.h @@ -0,0 +1,136 @@ +/*  + * fastcgi.h -- + * + *	Defines for the FastCGI protocol. + * + * + * Copyright (c) 1995-1996 Open Market, Inc. + * + * See the file "LICENSE.TERMS" for information on usage and redistribution + * of this file, and for a DISCLAIMER OF ALL WARRANTIES. + * + * $Id: fastcgi.h,v 1.1.1.1 1997/09/16 15:36:32 stanleyg Exp $ + */ + +#ifndef _FASTCGI_H +#define _FASTCGI_H + +/* + * Listening socket file number + */ +#define FCGI_LISTENSOCK_FILENO 0 + +typedef struct { +    unsigned char version; +    unsigned char type; +    unsigned char requestIdB1; +    unsigned char requestIdB0; +    unsigned char contentLengthB1; +    unsigned char contentLengthB0; +    unsigned char paddingLength; +    unsigned char reserved; +} FCGI_Header; + +#define FCGI_MAX_LENGTH 0xffff + +/* + * Number of bytes in a FCGI_Header.  Future versions of the protocol + * will not reduce this number. + */ +#define FCGI_HEADER_LEN  8 + +/* + * Value for version component of FCGI_Header + */ +#define FCGI_VERSION_1           1 + +/* + * Values for type component of FCGI_Header + */ +#define FCGI_BEGIN_REQUEST       1 +#define FCGI_ABORT_REQUEST       2 +#define FCGI_END_REQUEST         3 +#define FCGI_PARAMS              4 +#define FCGI_STDIN               5 +#define FCGI_STDOUT              6 +#define FCGI_STDERR              7 +#define FCGI_DATA                8 +#define FCGI_GET_VALUES          9 +#define FCGI_GET_VALUES_RESULT  10 +#define FCGI_UNKNOWN_TYPE       11 +#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE) + +/* + * Value for requestId component of FCGI_Header + */ +#define FCGI_NULL_REQUEST_ID     0 + + +typedef struct { +    unsigned char roleB1; +    unsigned char roleB0; +    unsigned char flags; +    unsigned char reserved[5]; +} FCGI_BeginRequestBody; + +typedef struct { +    FCGI_Header header; +    FCGI_BeginRequestBody body; +} FCGI_BeginRequestRecord; + +/* + * Mask for flags component of FCGI_BeginRequestBody + */ +#define FCGI_KEEP_CONN  1 + +/* + * Values for role component of FCGI_BeginRequestBody + */ +#define FCGI_RESPONDER  1 +#define FCGI_AUTHORIZER 2 +#define FCGI_FILTER     3 + + +typedef struct { +    unsigned char appStatusB3; +    unsigned char appStatusB2; +    unsigned char appStatusB1; +    unsigned char appStatusB0; +    unsigned char protocolStatus; +    unsigned char reserved[3]; +} FCGI_EndRequestBody; + +typedef struct { +    FCGI_Header header; +    FCGI_EndRequestBody body; +} FCGI_EndRequestRecord; + +/* + * Values for protocolStatus component of FCGI_EndRequestBody + */ +#define FCGI_REQUEST_COMPLETE 0 +#define FCGI_CANT_MPX_CONN    1 +#define FCGI_OVERLOADED       2 +#define FCGI_UNKNOWN_ROLE     3 + + +/* + * Variable names for FCGI_GET_VALUES / FCGI_GET_VALUES_RESULT records + */ +#define FCGI_MAX_CONNS  "FCGI_MAX_CONNS" +#define FCGI_MAX_REQS   "FCGI_MAX_REQS" +#define FCGI_MPXS_CONNS "FCGI_MPXS_CONNS" + + +typedef struct { +    unsigned char type;     +    unsigned char reserved[7]; +} FCGI_UnknownTypeBody; + +typedef struct { +    FCGI_Header header; +    FCGI_UnknownTypeBody body; +} FCGI_UnknownTypeRecord; + +#endif	/* _FASTCGI_H */ + diff --git a/plugins/fcgi/fcgi.cpp b/plugins/fcgi/fcgi.cpp new file mode 100644 index 0000000..d301579 --- /dev/null +++ b/plugins/fcgi/fcgi.cpp @@ -0,0 +1,284 @@ +// WIP! +#include "fcgi.h" + +#include "fastcgi.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 { + + const std::string gateway_interface{"CGI/1.1"}; + + struct FCGIContext + { +   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 + +   FCGIContext(std::function<std::string(const std::string& key)>& p_GetServerParam, +              std::function<std::string(const std::string& key)>& p_GetRequestParam, +              std::function<void(const std::string& key, const std::string& value)>& p_SetResponseHeader +              ) +    : GetServerParam(p_GetServerParam) +    , GetRequestParam(p_GetRequestParam) +    , SetResponseHeader(p_SetResponseHeader) +  { +  } + }; + + // 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] +  { +   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"; + } + + typedef boost::coroutines2::coroutine<std::string> coro_t; + + // returns true iff std::string is empty or contains newline + bool isEmpty(const std::string& s) + { +  return s.empty() || s == "\r" || s == "\n"s || s == "\r\n"s; + } + + void trimLinebreak(std::string& s) + { +  size_t pos = s.find_last_not_of("\r\n"); +  if (pos != s.npos) +   s = s.substr(0, pos + 1); + } + + std::unordered_map<std::string, std::function<void(std::string&, FCGIContext&)>> headerMap { +  { "CACHE-CONTROL", [](std::string& v, FCGIContext& c){ c.SetResponseHeader("cache_control", v); } }, + +  { "CONTENT-TYPE", [](std::string& v, FCGIContext& c){ c.SetResponseHeader("content_type", v); } }, + +  { "SET-COOKIE", [](std::string& v, FCGIContext& c){ c.SetResponseHeader("set_cookie", v); } }, +   +  { "STATUS", [](std::string& v, FCGIContext& c) { +      std::string status{"500"}; +      if (v.size() >= 3) { +       status = v.substr(0, 3); +      } +      c.SetResponseHeader("status", status); +  } } + }; + + void handleHeader(const std::string& s, FCGIContext& context) + { +  size_t pos = s.find(": "); +  if (pos == s.npos) +   return; + +  std::string key {s.substr(0, pos)}; +  std::string value {s.substr(pos + 2)}; + +  std::transform(key.begin(), key.end(), key.begin(), ::toupper); +  auto it {headerMap.find(key)}; +  if (it == headerMap.end()) +   std::cout << "Warning: Unhandled CGI header: " << s << std::endl; +  else +   it->second(value, context); + } + + void setCGIEnvironment(bp::environment& env, FCGIContext& c) + { +  std::string authorization {c.GetRequestParam("authorization")}; +  if (!authorization.empty()) +   env["AUTH_TYPE"] = c.GetRequestParam("authorization"); + +  env["CONTENT_LENGTH"] = c.GetRequestParam("content_length"); +  env["CONTENT_TYPE"] = c.GetRequestParam("content_type"); +  env["GATEWAY_INTERFACE"] = gateway_interface; + +  std::string target {c.GetRequestParam("target")}; +  size_t query_pos {target.find("?")}; +  std::string query; +  if (query_pos != target.npos) { +   query = target.substr(query_pos + 1); +   target = target.substr(0, query_pos); +  } + +  //TODO: env["PATH_INFO"] = c.path_info.string(); +  //TODO: env["PATH_TRANSLATED"] = c.path.string(); +  env["QUERY_STRING"] = query; +  env["REMOTE_ADDR"] = ""; +  env["REMOTE_HOST"] = ""; +  env["REMOTE_IDENT"] = ""; +  env["REMOTE_USER"] = ""; +  env["REQUEST_METHOD"] = c.GetRequestParam("method"); +  env["REQUEST_URI"] = target; +  //TODO: env["SCRIPT_NAME"] = c.file_path; +  env["SERVER_NAME"] = c.GetRequestParam("host"); +  env["SERVER_PORT"] = c.GetServerParam("port"); +  env["SERVER_PROTOCOL"] = c.GetRequestParam("http_version"); +  env["SERVER_SOFTWARE"] = c.GetServerParam("version"); + +  env["HTTP_ACCEPT"] = c.GetRequestParam("http_accept"); +  env["HTTP_ACCEPT_CHARSET"] = c.GetRequestParam("http_accept_charset"); +  env["HTTP_ACCEPT_ENCODING"] = c.GetRequestParam("http_accept_encoding"); +  env["HTTP_ACCEPT_LANGUAGE"] = c.GetRequestParam("http_accept_language"); +  env["HTTP_CONNECTION"] = c.GetRequestParam("http_connection"); +  env["HTTP_HOST"] = c.GetRequestParam("http_host"); +  env["HTTP_USER_AGENT"] = c.GetRequestParam("http_user_agent"); +  env["HTTP_REFERER"] = c.GetRequestParam("referer"); +  env["HTTP_COOKIE"] = c.GetRequestParam("cookie"); +  env["HTTPS"] = c.GetRequestParam("https"); + } + + std::string fcgiQuery(FCGIContext& context) + { +  bp::pipe is_in; +  bp::ipstream is_out; + +  bp::environment env {boost::this_process::environment()}; +  setCGIEnvironment(env, context); + +  bp::child child("", env, bp::std_out > is_out, bp::std_err > stderr, bp::std_in < is_in); + +  std::string body{ context.GetRequestParam("body") }; +  is_in.write(body.data(), body.size()); +  is_in.close(); + +  std::string output; +  std::string line; + +  // TODO: C++20 coroutine  +  coro_t::push_type processLine( [&](coro_t::pull_type& in){ +                                std::string line; +                                // read header lines +                                while (in && !isEmpty(line = in.get())) { +                                 trimLinebreak(line); +                                 handleHeader(line, context); +                                 in(); +                                } +                                 +                                // read empty line +                                if (!isEmpty(line)) +                                 throw std::runtime_error("Missing empty line between CGI header and body"); +                                if (in) +                                 in(); + +                                // read remainder +                                while (in) { +                                 line = in.get(); +                                 output += line + '\n'; +                                 in(); +                                } + +                                throw std::runtime_error("Input missing on processing CGI body"); +  }); + +  while (child.running() && std::getline(is_out, line)) { +   processLine(line); +  } + +  child.wait(); + +  return output; + } + + // 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 fcgi_plugin::name() +{ + return "fcgi"; +} + +fcgi_plugin::fcgi_plugin() +{ + //std::cout << "Plugin constructor" << std::endl; +} + +fcgi_plugin::~fcgi_plugin() +{ + //std::cout << "Plugin destructor" << std::endl; +} + +std::string fcgi_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 app_addr{GetRequestParam("doc_root")}; + +  SetResponseHeader("content_type", "text/html"); + +  FCGIContext context(GetServerParam, GetRequestParam, SetResponseHeader); + +  try { +   return fcgiQuery(context); +  } catch (const std::runtime_error& ex) { +   return HttpStatus("404", "Not found: "s + GetRequestParam("target"), SetResponseHeader); +  } catch (const std::exception& ex) { +   return HttpStatus("500", "Internal Server Error: "s + ex.what(), SetResponseHeader); +  } + + } catch (const std::exception& ex) { +  return HttpStatus("500", "Unknown Error: "s + ex.what(), SetResponseHeader); + } +} + diff --git a/plugins/fcgi/fcgi.h b/plugins/fcgi/fcgi.h new file mode 100644 index 0000000..7edfe91 --- /dev/null +++ b/plugins/fcgi/fcgi.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../../plugin_interface.h" + +class fcgi_plugin: public webserver_plugin_interface  +{ +public: + fcgi_plugin(); + ~fcgi_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 fcgi_plugin webserver_plugin; +fcgi_plugin webserver_plugin; diff --git a/webserver.conf b/webserver.conf index 5282af1..cc62bc7 100644 --- a/webserver.conf +++ b/webserver.conf @@ -47,6 +47,10 @@      <target>/home/ernie/code/webserver/cgi-bin</target>      <auth login="abc" password="p3p0Jka3YM5Fk"/>     </path> +   <path requested="/fcgi"> +    <plugin>fcgi</plugin> +    <target>127.0.0.1:9000</target> +   </path>     <certpath>/home/ernie/code/webserver/fullchain.pem</certpath>     <keypath>/home/ernie/code/webserver/privkey.pem</keypath>    </site>  | 
