From a20443c285dabdec8d2ee740500c62e31ad90c7b Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Thu, 23 Apr 2015 12:43:52 +0200 Subject: Implement apache service --- brep/module | 27 +++--- brep/module.cxx | 144 ++++++++++++++++++++++++++++--- brep/search.cxx | 31 +++++-- brep/view | 20 +++++ brep/view.cxx | 15 ++++ etc/apachectl | 170 ++++++++++++++++++++++++++++++++++++ etc/httpd.conf | 61 +++++++++++++ services.cxx | 8 +- web/apache/log | 50 +++++++++-- web/apache/request | 228 +++++++++++++++++++++++++++++++++++++++++++++++++ web/apache/request.cxx | 186 ++++++++++++++++++++++++++++++++++++++++ web/apache/request.ixx | 218 ++++++++++++++++++++++++++++++++++++++++++++++ web/apache/service | 94 +++++++++++++++++++- web/apache/stream | 161 ++++++++++++++++++++++++++++++++++ web/module | 66 ++++++++++++-- www/htdocs/index.html | 5 ++ 16 files changed, 1428 insertions(+), 56 deletions(-) create mode 100644 brep/view create mode 100644 brep/view.cxx create mode 100755 etc/apachectl create mode 100644 etc/httpd.conf create mode 100644 web/apache/request create mode 100644 web/apache/request.cxx create mode 100644 web/apache/request.ixx create mode 100644 web/apache/stream create mode 100644 www/htdocs/index.html diff --git a/brep/module b/brep/module index 3f7b409..d976559 100644 --- a/brep/module +++ b/brep/module @@ -5,7 +5,9 @@ #ifndef BREP_MODULE #define BREP_MODULE +#include #include // move() +#include #include @@ -20,28 +22,14 @@ namespace brep // web::module and our module. Or maybe not, need to try. // using web::status_code; + using web::invalid_request; + using web::sequence_error; using web::name_value; using web::name_values; using web::request; using web::response; using web::log; - // This exception is used to signal that the request is invalid - // (4XX codes) rather than that it could not be processed (5XX). - // By default 422 is returned, which means the request was - // semantically invalid. - // - struct invalid_request - { - status_code status {422}; - std::string description; - - //@@ Maybe optional "try again" link? - // - invalid_request (std::string d, status_code s = 422) - : status (s), description (std::move (d)) {} - }; - // And this exception indicated a server error (5XX). In particular, // it is thrown by the fail diagnostics stream and is caught by the // module implementation where it is both logged as an error and @@ -105,6 +93,7 @@ namespace brep // protected: module (); + module (const module& ); virtual void handle (request&, response&, log&); @@ -114,6 +103,12 @@ namespace brep private: log* log_ {nullptr}; // Diagnostics backend provided by the web server. + // Extract function name from a __PRETTY_FUNCTION__. + // Throw std::invalid_argument if fail to parse. + // + static std::string + func_name (const std::string& pretty_name); + void log_write (diag_data&&) const; diff --git a/brep/module.cxx b/brep/module.cxx index 15e996f..17790de 100644 --- a/brep/module.cxx +++ b/brep/module.cxx @@ -4,7 +4,16 @@ #include +#include +#include #include // bind() +#include // strncmp() + +#include + +#include + +#include using namespace std; using namespace placeholders; // For std::bind's _1, etc. @@ -15,6 +24,7 @@ namespace brep handle (request& rq, response& rs, log& l) { log_ = &l; + const basic_mark error (severity::error, log_writer_, __PRETTY_FUNCTION__); try { @@ -22,39 +32,151 @@ namespace brep } catch (const invalid_request& e) { - // @@ Both log and format as HTML in proper style, etc. - // - rs.content (e.status, "text/html;charset=utf-8") << e.description; + if (e.description.empty ()) + { + rs.status (e.status); + } + else + { + try + { + rs.content (e.status, "text/html;charset=utf-8") << e.description; + } + catch (const sequence_error& se) + { + error << se.what (); + rs.status (e.status); + } + } } catch (server_error& e) // Non-const because of move() below. { - // @@ Both log and return as 505. - // log_write (move (e.data)); + rs.status (HTTP_INTERNAL_SERVER_ERROR); } catch (const exception& e) { - // @@ Exception: log e.what () & 505. - // - rs.status (505); + error << e.what (); + rs.status (HTTP_INTERNAL_SERVER_ERROR); } catch (...) { - // @@ Unknown exception: log & 505. - // - rs.status (505); + error << "unknown error"; + rs.status (HTTP_INTERNAL_SERVER_ERROR); } } module:: module (): log_writer_ (bind (&module::log_write, this, _1)) {} + // Custom copy constructor is required to initialize log_writer_ properly. + // + module:: + module (const module& m): module () {verb_ = m.verb_;} + +// For function func declared like this: +// using B = std::string (*)(int); +// using A = B (*)(int,int); +// A func(B (*)(char),B (*)(wchar_t)); +// __PRETTY_FUNCTION__ looks like this: +// virtual std::string (* (* brep::search::func(std::string (* (*)(char))(int)\ +// ,std::string (* (*)(wchar_t))(int)) const)(int, int))(int) +// + string module:: + func_name (const string& pretty_name) + { + string::size_type b (0); + string::size_type e (pretty_name.find (' ')); + + // Position b at beginning of supposed function name, + // + if (e != string::npos && !strncmp (pretty_name.c_str (), "virtual ", 8)) + { + // Skip keyword virtual. + // + b = pretty_name.find_first_not_of (' ', e); + e = pretty_name.find (' ', b); + } + + if (pretty_name.find ('(', b) > e) + { + // Not a constructor nor destructor. Skip type or *. + // + b = pretty_name.find_first_not_of (' ', e); + } + + if (b != string::npos) + { + // Position e at the last character of supposed function name. + // + e = pretty_name.find_last_of (')'); + + if (e != string::npos && e > b) + { + size_t d (1); + + while (--e > b && d) + { + switch (pretty_name[e]) + { + case ')': ++d; break; + case '(': --d; break; + } + } + + if (!d) + { + return pretty_name[b] == '(' && pretty_name[e] == ')' ? + // Not a name yet, go deeper. + // + func_name (string(pretty_name, b + 1, e - b - 1)) : + // Got the name. + // + string (pretty_name, b, e - b + 1); + } + } + } + + throw invalid_argument (""); + } + void module:: log_write (diag_data&& d) const { if (log_ == nullptr) return; // No backend yet. + auto al = dynamic_cast<::web::apache::log*> (log_); + + if (al) + { + // Considered using lambda for mapping but looks too verbose while can + // be a bit safer in runtime. + // + static int s[] = { APLOG_ERR, APLOG_WARNING, APLOG_INFO, APLOG_TRACE1 }; + + for (const auto& e : d) + { + string name; + + try + { + name = func_name (e.name); + } + catch (const invalid_argument&) + { + // Log "pretty" function description, see in log file & fix. + name = e.name; + } + + al->write (e.loc.file.c_str(), + e.loc.line, + name.c_str(), + s[static_cast (e.sev)], + e.msg.c_str()); + } + } + //@@ Cast log_ to apache::log and write the records. // diff --git a/brep/search.cxx b/brep/search.cxx index 683016c..93690e4 100644 --- a/brep/search.cxx +++ b/brep/search.cxx @@ -5,6 +5,10 @@ #include #include +#include +#include + +#include using namespace std; @@ -15,26 +19,39 @@ namespace brep { MODULE_DIAG; + chrono::seconds ma (60); + rs.cookie ("Oh", " Ah\n\n", &ma, "/"); + rs.cookie ("Hm", ";Yes", &ma); + + info << "handling search request from "; // << rq.client_ip (); + + ostream& o (rs.content (200, "text/html;charset=utf-8", false)); + + o << "Params:"; + const name_values& ps (rq.parameters ()); if (ps.empty ()) - throw invalid_request ("search parameters expected"); + throw invalid_request (422, "search parameters expected"); if (ps.size () > 100) fail << "too many parameters: " << ps.size () << info << "are you crazy to specify so many?"; - info << "handling search request from "; // << rq.client_ip (); - level2 ([&]{trace << "search request with " << ps.size () << " params";}); - ostream& o (rs.content (202, "text/html;charset=utf-8")); + for (const auto& p: ps) + { + o << "
\n" << p.name << "=" << p.value; + } - o << "Search page:" << endl; + o << "
\nCookies:"; - for (const name_value& p: ps) + for (const auto& c : rq.cookies ()) { - o << p.name << "=" << p.value << endl; + o << "
\n" << c.name << "=" << c.value << " "; } + + o << ""; } } diff --git a/brep/view b/brep/view new file mode 100644 index 0000000..819eff3 --- /dev/null +++ b/brep/view @@ -0,0 +1,20 @@ +// file : brep/view -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Tools CC +// license : MIT; see accompanying LICENSE file + +#ifndef BREP_VIEW +#define BREP_VIEW + +#include + +namespace brep +{ + class view: public module + { + public: + virtual void + handle (request&, response&); + }; +} + +#endif // BREP_VIEW diff --git a/brep/view.cxx b/brep/view.cxx new file mode 100644 index 0000000..6dafa1b --- /dev/null +++ b/brep/view.cxx @@ -0,0 +1,15 @@ +// file : brep/view.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Tools CC +// license : MIT; see accompanying LICENSE file + +#include + +using namespace std; + +namespace brep +{ + void view:: + handle (request& rq, response& rs) + { + } +} diff --git a/etc/apachectl b/etc/apachectl new file mode 100755 index 0000000..d9c1ee9 --- /dev/null +++ b/etc/apachectl @@ -0,0 +1,170 @@ +#!/bin/sh +# +# Copyright (c) 2000-2002 The Apache Software Foundation. +# See license at the end of this file. +# +# Apache control script designed to allow an easy command line interface +# to controlling Apache. Written by Marc Slemko, 1997/08/23 +# +# The exit codes returned are: +# XXX this doc is no longer correct now that the interesting +# XXX functions are handled by httpd +# 0 - operation completed successfully +# 1 - +# 2 - usage error +# 3 - httpd could not be started +# 4 - httpd could not be stopped +# 5 - httpd could not be started during a restart +# 6 - httpd could not be restarted during a restart +# 7 - httpd could not be restarted during a graceful restart +# 8 - configuration syntax error +# +# When multiple arguments are given, only the error from the _last_ +# one is reported. Run "apachectl help" for usage info + +ARGV="$@" +# +# |||||||||||||||||||| START CONFIGURATION SECTION |||||||||||||||||||| +# -------------------- -------------------- + +PORT=7180 +LOG_LEVEL=trace1 +ADMIN_EMAIL=admin@cppget.org + +# |||||||||||||||||||| END CONFIGURATION SECTION ||||||||||||||||||||||| + +PROJECT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/.. + +site_config="$PROJECT_DIR/etc" +workspace="$PROJECT_DIR/var" +www="$PROJECT_DIR/www" + +mkdir -p "$workspace" + +# the path to your httpd binary, including options if necessary + +HTTPD="/usr/sbin/httpd -d $workspace -f $site_config/httpd.conf" + +# a command that outputs a formatted text version of the HTML at the +# url given on the command line. Designed for lynx, however other +# programs may work. +LYNX="lynx -dump" + +# the URL to your server's mod_status status page. If you do not +# have one, then status and fullstatus will not work. +STATUSURL="http://localhost:$PORT/server-status" + +# Set this variable to a command that increases the maximum +# number of file descriptors allowed per child process. This is +# critical for configurations that use many file descriptors, +# such as mass vhosting, or a multithreaded server. +ULIMIT_MAX_FILES="ulimit -S -n `ulimit -H -n`" +# -------------------- -------------------- +# |||||||||||||||||||| END CONFIGURATION SECTION |||||||||||||||||||| + +# Set the maximum number of file descriptors allowed per child process. +if [ "x$ULIMIT_MAX_FILES" != "x" ] ; then + $ULIMIT_MAX_FILES +fi + +ERROR=0 +if [ "x$ARGV" = "x" ] ; then + ARGV="-h" +fi + +case $ARGV in +start) + $HTTPD -C "Listen $PORT" -C "ServerName cppget.org:$PORT" \ + -C "DocumentRoot $www/htdocs" -C "CoreDumpDirectory $workspace" \ + -C "PidFile $workspace/httpd.pid" \ + -C "LogLevel $LOG_LEVEL" \ + -C "LoadModule search_srv $PROJECT_DIR/libbrep.so" \ + -C "LoadModule view_srv $PROJECT_DIR/libbrep.so" \ + -C "ServerAdmin $ADMIN_EMAIL" \ + -k $ARGV + + ERROR=$? + ;; +stop|restart|graceful) + $HTTPD -C "ServerName cppget.org:$PORT" \ + -C "PidFile $workspace/httpd.pid" -k $ARGV + ERROR=$? + ;; +startssl|sslstart|start-SSL) + $HTTPD -k start -DSSL + ERROR=$? + ;; +configtest) + $HTTPD -t + ERROR=$? + ;; +status) + $LYNX $STATUSURL | awk ' /process$/ { print; exit } { print } ' + ;; +fullstatus) + $LYNX $STATUSURL + ;; +*) + $HTTPD $ARGV + ERROR=$? +esac + +exit $ERROR + +# ==================================================================== +# The Apache Software License, Version 1.1 +# +# Copyright (c) 2000-2003 The Apache Software Foundation. All rights +# reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# 3. The end-user documentation included with the redistribution, +# if any, must include the following acknowledgment: +# "This product includes software developed by the +# Apache Software Foundation (http://www.apache.org/)." +# Alternately, this acknowledgment may appear in the software itself, +# if and wherever such third-party acknowledgments normally appear. +# +# 4. The names "Apache" and "Apache Software Foundation" must +# not be used to endorse or promote products derived from this +# software without prior written permission. For written +# permission, please contact apache@apache.org. +# +# 5. Products derived from this software may not be called "Apache", +# nor may "Apache" appear in their name, without prior written +# permission of the Apache Software Foundation. +# +# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR +# ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# ==================================================================== +# +# This software consists of voluntary contributions made by many +# individuals on behalf of the Apache Software Foundation. For more +# information on the Apache Software Foundation, please see +# . +# +# Portions of this software are based upon public domain software +# originally written at the National Center for Supercomputing Applications, +# University of Illinois, Urbana-Champaign. +# diff --git a/etc/httpd.conf b/etc/httpd.conf new file mode 100644 index 0000000..95b887d --- /dev/null +++ b/etc/httpd.conf @@ -0,0 +1,61 @@ +User apache +Group apache + +ErrorLog error_log +#ErrorLog "|/usr/sbin/rotatelogs /Users/karen/projects/brep/var/error_log.%Y%m%d 86400" + +ErrorLogFormat "[%t] [%l] [%m] %M" + +Timeout 60 + +KeepAlive On +KeepAliveTimeout 3 + +ThreadLimit 1000 +ServerLimit 2 +StartServers 1 +MaxClients 1000 +MinSpareThreads 400 +MaxSpareThreads 600 +ThreadsPerChild 500 +MaxRequestsPerChild 0 + +LoadModule mpm_worker_module /usr/lib64/httpd/modules/mod_mpm_worker.so +LoadModule unixd_module /usr/lib64/httpd/modules/mod_unixd.so +LoadModule filter_module /usr/lib64/httpd/modules/mod_filter.so +LoadModule access_compat_module /usr/lib64/httpd/modules/mod_access_compat.so +LoadModule authn_core_module /usr/lib64/httpd/modules/mod_authn_core.so +LoadModule authz_core_module /usr/lib64/httpd/modules/mod_authz_core.so +LoadModule status_module /usr/lib64/httpd/modules/mod_status.so +LoadModule mime_module /usr/lib64/httpd/modules/mod_mime.so +LoadModule deflate_module /usr/lib64/httpd/modules/mod_deflate.so +LoadModule authz_host_module /usr/lib64/httpd/modules/mod_authz_host.so +LoadModule expires_module /usr/lib64/httpd/modules/mod_expires.so +LoadModule dir_module /usr/lib64/httpd/modules/mod_dir.so + +TypesConfig /etc/mime.types + + + SetHandler search + + + + SetHandler view + + +DirectoryIndex index.html + +ExtendedStatus On + + + SetHandler server-status + Order deny,allow + Deny from all + Allow from 127.0.0 + Allow from localhost + + + + Options FollowSymLinks + AllowOverride None + diff --git a/services.cxx b/services.cxx index 2345114..2da5bca 100644 --- a/services.cxx +++ b/services.cxx @@ -5,13 +5,13 @@ #include #include -//#include +#include using namespace brep; using web::apache::service; static const search search_mod; -service /*AP_MODULE_DECLARE_DATA*/ search_srv ("search", search_mod); +service AP_MODULE_DECLARE_DATA search_srv ("search", search_mod); -//static const view view_mod; -//service AP_MODULE_DECLARE_DATA view_srv ("view", view_mod); +static const view view_mod; +service AP_MODULE_DECLARE_DATA view_srv ("view", view_mod); diff --git a/web/apache/log b/web/apache/log index 0e39420..151efb4 100644 --- a/web/apache/log +++ b/web/apache/log @@ -5,7 +5,11 @@ #ifndef WEB_APACHE_LOG #define WEB_APACHE_LOG -#include // uint64_t +#include // min() +#include // uint64_t + +#include // request_rec +#include #include @@ -16,7 +20,8 @@ namespace web class log: public web::log { public: - // ... + + log (request_rec* req) noexcept : req_ (req) {} virtual void write (const char* msg) {write (APLOG_ERR, msg);} @@ -24,13 +29,48 @@ namespace web // Apache-specific interface. // void - write (int level, const char* msg) {write (nullptr, 0, level, msg);} + write (int level, const char* msg) + { + write (nullptr, 0, nullptr, level, msg); + } void - write (const char* file, std::uint64_t line, int level, const char* msg); + write (const char* file, + std::uint64_t line, + const char* func, + int level, + const char* msg) + { + if (file && *file) + file = nullptr; // skip file/line placeholder from log line. + + level = std::min (level, APLOG_TRACE8); + + if (func) + ap_log_rerror (file, + line, + APLOG_NO_MODULE, + level, + 0, + req_, + "[%s]: %s", + func, + msg); + else + // skip function name placeholder from log line + // + ap_log_rerror (file, + line, + APLOG_NO_MODULE, + level, + 0, + req_, + ": %s", + msg); + } private: - // ... + request_rec* req_; }; } } diff --git a/web/apache/request b/web/apache/request new file mode 100644 index 0000000..ab5c765 --- /dev/null +++ b/web/apache/request @@ -0,0 +1,228 @@ +// file : web/apache/request -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Tools CC +// license : MIT; see accompanying LICENSE file + +#ifndef WEB_APACHE_REQUEST +#define WEB_APACHE_REQUEST + +#include +#include +#include +#include +#include +#include +#include +#include // unique_ptr +#include // move +#include +#include + +#include + +#include +#include +#include + +#include +#include + +namespace web +{ + namespace apache + { + class request : public web::request, public web::response + { + friend class service; + + request (request_rec* rec) noexcept: rec_ (rec) {} + + // Flush of buffered content. + // + int + flush (); + + // Get request body data stream. + // + virtual std::istream& + data () + { + if (write_flag ()) + { + throw sequence_error ("::web::apache::request::data"); + } + + if (!in_) + { + std::unique_ptr in_buf (new istreambuf (rec_)); + in_.reset (new std::istream (in_buf.get ())); + in_buf_ = std::move (in_buf); + in_->exceptions (std::ios::failbit | std::ios::badbit); + + // Save form data now otherwise will not be available to do later + // when data read from stream. + // + form_data (); + } + + return *in_; + } + + // Get request parameters. + // + virtual const name_values& + parameters () + { + if (!parameters_) + { + parameters_.reset (new name_values()); + + try + { + parse_parameters(rec_->args); + parse_parameters(form_data ()->c_str ()); + } + catch(const std::invalid_argument& ) + { + throw invalid_request(); + } + } + + return *parameters_; + } + + // Get request cookies. + // + virtual const name_values& + cookies (); + + // Get response status code. + // + status_code status () const noexcept {return status_;} + + // Set response status code. + // + virtual void + status (status_code status) + { + if (status != status_) + { + // Setting status code in exception handler is a common usecase + // where no sense to throw but still need to signal apache a + // proper status code. + // + if (write_flag () && !std::current_exception ()) + { + throw sequence_error ("::web::apache::request::status"); + } + + status_ = status; + type_.clear (); + buffer_ = true; + out_.reset (); + out_buf_.reset (); + set_content_type (); + } + } + + // Set response status code, content type and get body stream. + // + virtual std::ostream& + content (status_code status, + const std::string& type, + bool buffer = true); + + // Add response cookie. + // + virtual void + cookie (const char* name, + const char* value, + const std::chrono::seconds* max_age = 0, + const char* path = 0, + const char* domain = 0, + bool secure = false); + + private: + using string_ptr = std::unique_ptr; + + // Get application/x-www-form-urlencoded form data. + // + const string_ptr& + form_data (); + + void + parse_parameters (const char* args); + + static void + mime_url_encode (const char* v, std::ostream& o); + + static std::string + mime_url_decode (const char* b, const char* e, bool trim = false); + + // Save content type to apache internals. + // + void + set_content_type () const noexcept + { + if (type_.empty ()) + ap_set_content_type (rec_, nullptr); + else + { + if(status_ == HTTP_OK) + { + ap_set_content_type (rec_, + apr_pstrdup (rec_->pool, type_.c_str ())); + } + else + { + // Unfortunatelly there is no way to set a proper content type + // for error custom response. Depending on presense of + // "suppress-error-charset" key in request_rec::subprocess_env + // table content type is set to "text/html" otherwise to + // "text/html; charset=iso-8859-1" (read http_protocol.c for + // details). I have chosen the first one as it is better not to + // specify charset than to set a wrong one. Ensure to put + // a proper encoding to + // + // tag so browser can render the page properly. + // The clean solution would be patching apache but let's leave this + // troublesome option untill really required. + // + apr_table_set (rec_->subprocess_env, "suppress-error-charset", ""); + } + } + } + + bool + write_flag () const noexcept + { + if (!buffer_) + { + assert (out_buf_); + auto b = dynamic_cast (out_buf_.get ()); + assert (b); + return b->write_flag (); + } + + return false; + } + + private: + + request_rec* rec_; + status_code status_ {HTTP_OK}; + std::string type_; + bool buffer_ {true}; + std::unique_ptr out_buf_; + std::unique_ptr out_; + std::unique_ptr in_buf_; + std::unique_ptr in_; + std::unique_ptr parameters_; + std::unique_ptr cookies_; + string_ptr form_data_; + }; + } +} + +#include + +#endif // WEB_APACHE_REQUEST diff --git a/web/apache/request.cxx b/web/apache/request.cxx new file mode 100644 index 0000000..6f043bc --- /dev/null +++ b/web/apache/request.cxx @@ -0,0 +1,186 @@ +// file : web/apache/request.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Tools CC +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include +#include +#include +#include +#include // unique_ptr +#include // move() +#include +#include + +#include // strcasecmp() + +#include + +using namespace std; + +namespace web +{ + namespace apache + { + const name_values& request:: + cookies () + { + if (!cookies_) + { + cookies_.reset (new name_values ()); + + const apr_array_header_t* ha = apr_table_elts (rec_->headers_in); + size_t n = ha->nelts; + + for (auto h (reinterpret_cast (ha->elts)); + n--; ++h) + { + if (!::strcasecmp (h->key, "Cookie")) + { + for (const char* n (h->val); n != 0; ) + { + const char* v = strchr (n, '='); + const char* e = strchr (n, ';'); + + if (e && e < v) + v = 0; + + string name ( + v ? mime_url_decode (n, v, true) : + (e ? mime_url_decode (n, e, true) : + mime_url_decode (n, n + strlen (n), true))); + + string value; + + if (v++) + { + value = e ? mime_url_decode (v, e, true) : + mime_url_decode (v, v + strlen (v), true); + } + + if (!name.empty () || !value.empty ()) + cookies_->emplace_back (move (name), move (value)); + + n = e ? e + 1 : 0; + } + } + } + } + + return *cookies_; + } + + ostream& request:: + content (status_code status, const std::string& type, bool buffer) + { + if (type.empty ()) + { + // Getting content stream for writing assumes type to be provided. + // + throw std::invalid_argument ( + "::web::apache::request::content invalid type"); + } + + // Due to apache implementation of error custom response there is no + // way to make it unbuffered. + // + buffer = buffer || status != HTTP_OK; + + if ((status != status_ || type != type_ || buffer != buffer_) & + write_flag ()) + { + throw sequence_error ("::web::apache::request::content"); + } + + if (status == status_ && type == type_ && buffer == buffer_) + { + assert (out_); + return *out_; + } + + if (!buffer) + // Request body will be discarded prior first byte of content is + // written. Save form data now to make it available for furture + // parameters () call. + // + form_data (); + + std::unique_ptr out_buf( + buffer ? static_cast (new std::stringbuf ()) : + static_cast (new ostreambuf (rec_))); + + out_.reset (new std::ostream (out_buf.get ())); + + out_buf_ = std::move (out_buf); + + out_->exceptions ( + std::ios::eofbit | std::ios::failbit | std::ios::badbit); + + status_ = status; + type_ = type; + buffer_ = buffer; + + if (!buffer_) + set_content_type (); + + return *out_; + } + + void request:: + cookie (const char* name, + const char* value, + const std::chrono::seconds* max_age, + const char* path, + const char* domain, + bool secure) + { + if (write_flag ()) + { + throw sequence_error ("::web::apache::request::cookie"); + } + + std::ostringstream s; + mime_url_encode (name, s); + s << "="; + mime_url_encode (value, s); + + if (max_age) + { + std::chrono::system_clock::time_point tp = + std::chrono::system_clock::now () + *max_age; + + std::time_t t = std::chrono::system_clock::to_time_t (tp); + + // Assume global "C" locale is not changed. + // + char b[100]; + std::strftime (b, + sizeof (b), + "%a, %d-%b-%Y %H:%M:%S GMT", + std::gmtime (&t)); + + s << "; Expires=" << b; + } + + if (path) + { + s << ";Path=" << path; + } + + if (domain) + { + s << ";Domain=" << domain; + } + + if (secure) + { + s << ";Secure"; + } + + apr_table_add (rec_->err_headers_out, "Set-Cookie", s.str ().c_str ()); + } + + } +} diff --git a/web/apache/request.ixx b/web/apache/request.ixx new file mode 100644 index 0000000..a427fd4 --- /dev/null +++ b/web/apache/request.ixx @@ -0,0 +1,218 @@ +// file : web/apache/request.ixx -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Tools CC +// license : MIT; see accompanying LICENSE file + +#include +#include +#include +#include + +#include // strcasecmp() + +namespace web +{ + namespace apache + { + inline int request:: + flush () + { + if (buffer_ && out_buf_) + { + set_content_type (); + + auto b = dynamic_cast (out_buf_.get ()); + assert(b); + + std::string s (b->str ()); + + if (!s.empty ()) + { + // Before writing response read and discard request body if any. + // + int r = ap_discard_request_body (rec_); + + if (r == OK) + { + if (status_ == HTTP_OK) + { + if (ap_rwrite (s.c_str (), s.length (), rec_) < 0) + { + status_ = HTTP_REQUEST_TIME_OUT; + } + } + else + { + ap_custom_response (rec_, status_, s.c_str ()); + } + } + else + status_ = r; + } + + out_.reset (); + out_buf_.reset (); + } + + return status_ == HTTP_OK ? OK : status_; + } + + inline const request::string_ptr& request:: + form_data () + { + if (!form_data_) + { + form_data_.reset (new std::string ()); + const char *ct = apr_table_get (rec_->headers_in, "Content-Type"); + + if (ct && !strncasecmp ("application/x-www-form-urlencoded", ct, 33)) + { + std::istream& istr (data ()); + std::getline (istr, *form_data_); + + // Make request data still be available. + // + + std::unique_ptr in_buf ( + new std::stringbuf (*form_data_)); + + in_.reset (new std::istream (in_buf.get ())); + in_buf_ = std::move (in_buf); + in_->exceptions (std::ios::failbit | std::ios::badbit); + } + } + + return form_data_; + } + + inline void request:: + parse_parameters (const char* args) + { + for (auto n (args); n != 0; ) + { + const char* v = strchr (n, '='); + const char* e = strchr (n, '&'); + + if (e && e < v) + v = 0; + + std::string name (v + ? mime_url_decode (n, v) : + (e + ? mime_url_decode (n, e) + : mime_url_decode (n, n + std::strlen (n)))); + + std::string value; + + if (v++) + { + value = e + ? mime_url_decode (v, e) + : mime_url_decode (v, v + std::strlen (v)); + } + + if (!name.empty () || !value.empty ()) + parameters_->emplace_back (std::move (name), std::move (value)); + + n = e ? e + 1 : 0; + } + } + + inline void request:: + mime_url_encode (const char* v, std::ostream& o) + { + char f = o.fill (); + std::ios_base::fmtflags g = o.flags (); + o << std::hex << std::uppercase << std::right << std::setfill ('0'); + char c; + + while ((c = *v++) != '\0') + { + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9')) + { + o << c; + } + else + switch (c) + { + case ' ': o << '+'; break; + case '.': + case '_': + case '-': + case '~': o << c; break; + default: o << "%" << std::setw (2) << (unsigned short)c; + } + } + + o.flags (g); + o.fill (f); + } + + inline std::string request:: + mime_url_decode (const char* b, const char* e, bool trim) + { + if (trim) + { + b += std::strspn (b, " "); + + if (b >= e) + return std::string(); + + while (*--e == ' '); + ++e; + } + + std::string value; + value.reserve (e - b); + + char bf[3]; + bf[2] = '\0'; + + while (b != e) + { + char c = *b++; + + switch (c) + { + case '+': + { + value.append (" "); + break; + } + case '%': + { + if (*b == '\0' || b[1] == '\0') + { + throw std::invalid_argument ( + "::web::apache::request::mime_url_decode short"); + } + + *bf = *b; + bf[1] = b[1]; + + char* ebf = 0; + size_t vl = std::strtoul (bf, &ebf, 16); + + if (*ebf != '\0') + { + throw std::invalid_argument ( + "::web::apache::request::mime_url_decode wrong"); + } + + value.append (1, (char)vl); + b += 2; + break; + } + default: + { + value.append (1, c); + break; + } + } + } + + return value; + } + + } +} diff --git a/web/apache/service b/web/apache/service index 8dd21d8..688f8f1 100644 --- a/web/apache/service +++ b/web/apache/service @@ -5,22 +5,106 @@ #ifndef WEB_APACHE_SERVICE #define WEB_APACHE_SERVICE +#include #include +#include + +#include +#include #include +#include +#include + namespace web { namespace apache { - class service + class service : ::module { public: // Note that the module exemplar is stored by-reference. // template service (const std::string& name, const M& exemplar) - : exemplar_ (exemplar), handle_ (&handle_impl) {} + : ::module + { + STANDARD20_MODULE_STUFF, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + ®ister_hooks + }, + name_ (name), + exemplar_ (exemplar) + +// Doesn't look like handle_ member is required at all. +// handle_ (&handle_impl) + { + // instance () invented to delegate processing from apache request + // handler C function to service non static member function. + // This appoach resticts number of service objects per module + // implementation class with just one instance. + // + service*& srv = instance (); + assert (srv == nullptr); + srv = this; + } + + template + static service*& + instance () noexcept + { + static service* instance; + return instance; + } + + template + static void + register_hooks (apr_pool_t *pool) noexcept + { + ap_hook_handler (&request_handler, NULL, NULL, APR_HOOK_LAST); + } + + template + static int + request_handler (request_rec* r) noexcept + { + auto srv = instance (); + + if (!r->handler || srv->name_ != r->handler) + return DECLINED; + + request req (r); + log l(r); + + // As soons as M (), handle () and flush () can throw need to handle + // exceptions here. + // + try + { + M m (static_cast (srv->exemplar_)); + static_cast (m).handle (req, req, l); + return req.flush(); + } + catch (const std::exception& e) + { + l.write (nullptr, 0, __PRETTY_FUNCTION__, APLOG_ERR, e.what ()); + } + catch (...) + { + l.write (nullptr, + 0, + __PRETTY_FUNCTION__, + APLOG_ERR, + "unknown error"); + } + + return HTTP_INTERNAL_SERVER_ERROR; + } //@@ Implementation calls handle_ function pointer below: // @@ -28,6 +112,7 @@ namespace web // private: +/* template static void handle_impl (request& rq, response& rs, log& l, const module& exemplar) @@ -35,9 +120,10 @@ namespace web M m (static_cast (exemplar)); static_cast (m).handle (rq, rs, l); } - +*/ + std::string name_; const module& exemplar_; - void (*handle_) (request&, response&, log&, const module&); +// void (*handle_) (request&, response&, log&, const module&); }; } } diff --git a/web/apache/stream b/web/apache/stream new file mode 100644 index 0000000..eb62b85 --- /dev/null +++ b/web/apache/stream @@ -0,0 +1,161 @@ +// file : web/apache/stream -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Tools CC +// license : MIT; see accompanying LICENSE file + +#ifndef WEB_APACHE_STREAM +#define WEB_APACHE_STREAM + +#include +#include // streamsize +#include // min(), max() +#include // memmove() +#include // unique_ptr + +#include +#include + +#include + +namespace web +{ + namespace apache + { + class ostreambuf : public std::streambuf + { + public: + ostreambuf (request_rec* rec) : rec_ (rec) {} + + bool + write_flag () const noexcept {return write_;} + + private: + virtual int_type + overflow (int_type c) + { + if (c != traits_type::eof ()) + { + flag_write (); + + char chr = c; + + // Throwing allows to distinguish comm failure from other IO error + // conditions. + // + if (ap_rwrite (&chr, sizeof (chr), rec_) == -1) + throw invalid_request (HTTP_REQUEST_TIME_OUT); + } + + return c; + } + + virtual std::streamsize + xsputn (const char* s, std::streamsize num) + { + flag_write (); + + if (ap_rwrite (s, num, rec_) < 0) + { + throw invalid_request (HTTP_REQUEST_TIME_OUT); + } + + return num; + } + + virtual int + sync () + { + if(ap_rflush (rec_) < 0) + { + throw invalid_request (HTTP_REQUEST_TIME_OUT); + } + + return 0; + } + + void + flag_write () noexcept + { + if (!write_) + { + // Preparing to write a response read and discard request + // body if any. + // + int r = ap_discard_request_body (rec_); + + if (r != OK) + { + throw invalid_request (r); + } + + write_ = true; + } + } + + private: + + request_rec* rec_; + bool write_ {false}; + }; + + class istreambuf : public std::streambuf + { + public: + istreambuf (request_rec* rec, size_t bufsize = 1024, size_t putback = 1) + : rec_ (rec), + bufsize_ (std::max (bufsize, (size_t)1)), + putback_ (std::min (putback, bufsize_ - 1)), + buf_ (new char[bufsize_]) + { + char* p = buf_.get () + putback_; + setg (p, p, p); + + int status = ap_setup_client_block (rec_, REQUEST_CHUNKED_DECHUNK); + + if (status != OK) + { + throw invalid_request (status); + } + } + + private: + virtual int_type + underflow () + { + if (gptr () < egptr ()) + return traits_type::to_int_type (*gptr ()); + + size_t pb = std::min ((size_t)(gptr () - eback ()), putback_); + std::memmove (buf_.get () + putback_ - pb, gptr () - pb, pb); + + char* p = buf_.get () + putback_; + int rb = ap_get_client_block (rec_, p, bufsize_ - putback_); + + if (rb == 0) + { + return traits_type::eof (); + } + + if (rb < 0) + { + throw invalid_request (HTTP_REQUEST_TIME_OUT); + } + + setg (p - pb, p, p + rb); + return traits_type::to_int_type (*gptr ()); + } + + bool error () const noexcept {return error_;} + + private: + + request_rec* rec_; + size_t bufsize_; + size_t putback_; + std::unique_ptr buf_; + bool error_ {false}; + }; + + } +} + +#endif // WEB_APACHE_STREAM diff --git a/web/module b/web/module index 642b1bd..9f1c778 100644 --- a/web/module +++ b/web/module @@ -5,27 +5,49 @@ #ifndef WEB_MODULE #define WEB_MODULE +#include // move() +#include // runtime_error #include #include #include +#include #include // uint16_t -#include // move() -#include // runtime_error namespace web { - // Exception indicating HTTP request/response sequencing error. - // For example, trying to change the status code after some - // content has already been written. - // - struct sequence_error: std::runtime_error {}; - // HTTP status code. // // @@ Define some commonly used constants? // using status_code = std::uint16_t; + // This exception is used to signal that the request is invalid + // (4XX codes) rather than that it could not be processed (5XX). + // By default 400 is returned, which means the request is malformed. + // Non empty description of a caught by the module implementation exception + // can be sent to client in http response body with + // Content-Type:text/html;charset=utf-8 header. + // + struct invalid_request + { + status_code status; + std::string description; + + //@@ Maybe optional "try again" link? + // + invalid_request (status_code s = 400, std::string d = "") + : status (s), description (std::move (d)) {} + }; + + // Exception indicating HTTP request/response sequencing error. + // For example, trying to change the status code after some + // content has already been written. + // + struct sequence_error: std::runtime_error + { + sequence_error (std::string d) : std::runtime_error (d) {} + }; + struct name_value { // These should eventually become string_view's. @@ -48,8 +70,21 @@ namespace web // in name_values. //@@ Maybe parameter_list() and parameter_map()? // + // Throw invalid_request if mime url decode of name or value fail. + // virtual const name_values& parameters () = 0; + + // Throw invalid_request if Cookie header is malformed. + // + virtual const name_values& + cookies () = 0; + + // Get stream to read request body data. + // Throw sequence_error if some unbuffered content is already written. + // + virtual std::istream& + data () = 0; }; class response @@ -75,9 +110,22 @@ namespace web content (status_code, const std::string& type, bool buffer = true) = 0; // Set status code without writing any content. + // On status change discards buffered output and throw sequence_error + // if output were not buffered. // virtual void status (status_code) = 0; + + // Throw sequence_error if some unbuffered content is already written as + // will not be able to send Set-Cookie header. + // + virtual void + cookie (const char* name, + const char* value, + const std::chrono::seconds* max_age = 0, + const char* path = 0, + const char* domain = 0, + bool secure = false) = 0; }; // A web server logging backend. The module can use it to log @@ -91,7 +139,7 @@ namespace web { public: virtual void - write (const char* msg); + write (const char* msg) = 0; }; // The web server creates a new module instance for each request diff --git a/www/htdocs/index.html b/www/htdocs/index.html new file mode 100644 index 0000000..3d6d51e --- /dev/null +++ b/www/htdocs/index.html @@ -0,0 +1,5 @@ + + + Home Page + + -- cgit v1.1