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 --- 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 ++++++++++++-- 7 files changed, 985 insertions(+), 18 deletions(-) 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 (limited to 'web') 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 -- cgit v1.1