diff options
| author | Joel Klinghed <the_jk@yahoo.com> | 2015-05-28 21:14:52 +0200 |
|---|---|---|
| committer | Joel Klinghed <the_jk@yahoo.com> | 2015-05-28 21:16:14 +0200 |
| commit | 720295848ea0d909cb39c004cbeaf1055fa7cffc (patch) | |
| tree | 3a4c0332f8145986ddfc1c83e89b9cf8a1491546 /src | |
| parent | 978bb2523f2eef42eca8dbe35e0ad351a4953aa7 (diff) | |
Start of CGI interface
Diffstat (limited to 'src')
| -rw-r--r-- | src/.gitignore | 1 | ||||
| -rw-r--r-- | src/Makefile.am | 10 | ||||
| -rw-r--r-- | src/cgi.cc | 213 | ||||
| -rw-r--r-- | src/cgi.hh | 42 | ||||
| -rw-r--r-- | src/common.hh | 4 | ||||
| -rw-r--r-- | src/header_parser.cc | 107 | ||||
| -rw-r--r-- | src/header_parser.hh | 24 | ||||
| -rw-r--r-- | src/multipart_formdata_parser.cc | 147 | ||||
| -rw-r--r-- | src/multipart_formdata_parser.hh | 25 | ||||
| -rw-r--r-- | src/query_parser.cc | 82 | ||||
| -rw-r--r-- | src/query_parser.hh | 23 | ||||
| -rw-r--r-- | src/strutils.cc | 29 | ||||
| -rw-r--r-- | src/strutils.hh | 12 |
13 files changed, 718 insertions, 1 deletions
diff --git a/src/.gitignore b/src/.gitignore index 0c0f75f..246841d 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -2,4 +2,5 @@ /config.h.in~ /config.h /stamp-h1 +/libcgi.la /libdb.la diff --git a/src/Makefile.am b/src/Makefile.am index 06a92ff..aaab9f9 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -2,7 +2,15 @@ MAINTAINERCLEANFILES = Makefile.in AM_CPPFLAGS = @DEFINES@ -noinst_LTLIBRARIES = libdb.la +noinst_LTLIBRARIES = libdb.la libcgi.la + +libcgi_la_SOURCES = cgi.hh common.hh cgi.cc \ + query_parser.hh query_parser.cc \ + header_parser.hh header_parser.cc \ + strutils.hh strutils.cc \ + multipart_formdata_parser.hh multipart_formdata_parser.cc +libcgi_la_CPPFLAGS = $(AM_CPPFLAGS) @FASTCGI_CFLAGS@ +libcgi_la_LIBADD = @FASTCGI_LIBS@ libdb_la_SOURCES = db.hh common.hh db.cc sqlite3_db.hh sqlite3_db.cc libdb_la_CPPFLAGS = $(AM_CPPFLAGS) @SQLITE3_CFLAGS@ diff --git a/src/cgi.cc b/src/cgi.cc new file mode 100644 index 0000000..5c9e97b --- /dev/null +++ b/src/cgi.cc @@ -0,0 +1,213 @@ +#include "common.hh" + +#include <cstdlib> +#if HAVE_FASTCGI +#include <fcgio.h> +#endif +#include <iostream> + +#include "cgi.hh" +#include "header_parser.hh" +#include "multipart_formdata_parser.hh" +#include "strutils.hh" +#include "query_parser.hh" + +namespace stuff { + +namespace { + +class CGIImpl : public CGI { +public: + void post_data(std::vector<char>* data) override { + if (!have_post_data_) { + fill_post_data(); + } + data->assign(post_data_.begin(), post_data_.end()); + } + + bool post_data(std::map<std::string, std::string>* data) override { + data->clear(); + if (!have_post_data_) { + fill_post_data(); + } + + auto ct = getparam("CONTENT_TYPE"); + if (!ct) return false; + + std::string type; + std::map<std::string, std::string> params; + if (!HeaderParser::parse(ct, &type, ¶ms)) { + return false; + } + if (type.compare("application/x-www-form-urlencoded") == 0) { + return QueryParser::parse(std::string(post_data_.data(), + post_data_.size()), data); + } else if (type.compare("multipart/form-data") == 0) { + auto it = params.find("boundary"); + if (it == params.end()) return false; + return MultipartFormDataParser::parse(post_data_, it->second, data); + } + return false; + } + + void query_data(std::map<std::string, std::string>* data) override { + data->clear(); + auto qs = getparam("QUERY_STRING"); + if (qs) { + QueryParser::parse(qs, data); + } + } + + std::string request_path() override { + auto path = getparam("PATH_INFO"); + return path ? path : ""; + } + + std::string content_type() override { + auto ct = getparam("CONTENT_TYPE"); + if (!ct) return ""; + std::string type; + if (!HeaderParser::parse(ct, &type, nullptr)) return ""; + return type; + } + + enum request_type request_type() override { + auto p = getparam("REQUEST_METHOD"); + if (p) { + auto method = ascii_tolower(p); + if (method.compare("GET") == 0) return GET; + if (method.compare("POST") == 0) return POST; + if (method.compare("HEAD") == 0) return HEAD; + if (method.compare("PUT") == 0) return PUT; + if (method.compare("TRACE") == 0) return TRACE; + } + return UNKNOWN; + } + + virtual void reset() { + have_post_data_ = false; + post_data_.clear(); + } + +protected: + virtual const char* getparam(const char* name) = 0; + +private: + void fill_post_data() { + have_post_data_ = true; + switch (request_type()) { + case POST: + case PUT: { + auto len = getparam("CONTENT_LENGTH"); + std::string tmpstr; + if (len && HeaderParser::parse(len, &tmpstr, nullptr)) { + char* end = nullptr; + errno = 0; + auto tmp = strtoul(tmpstr.c_str(), &end, 10); + if (errno == 0 && end && !*end) { + post_data_.resize(tmp); + std::cin.read(post_data_.data(), tmp); + post_data_.resize(std::cin.gcount()); + return; + } + } + while (std::cin.good()) { + char buf[1024]; + std::cin.read(buf, sizeof(buf)); + post_data_.insert(post_data_.end(), + buf, buf + std::cin.gcount()); + } + break; + } + case GET: + case HEAD: + case TRACE: + case UNKNOWN: + break; + } + } + + bool have_post_data_; + std::vector<char> post_data_; +}; + +class BasicCGIImpl : public CGIImpl { +public: + BasicCGIImpl() + : CGIImpl() { + } + ~BasicCGIImpl() override { + } +protected: + const char* getparam(const char* name) override { + return getenv(name); + } +}; + +#if HAVE_FASTCGI +class FastCGIImpl : public CGIImpl { +public: + FastCGIImpl() + : CGIImpl(), params_(nullptr) { + } + ~FastCGIImpl() override { + } + + void reset(FCGX_ParamArray params) { + CGIImpl::reset(); + params_ = params; + } +protected: + using CGIImpl::reset; + const char* getparam(const char* name) override { + return params_ ? FCGX_GetParam(name, params_) : nullptr; + } +private: + FCGX_ParamArray params_; +}; +#endif + +} // namespace + +int CGI::run(std::function<bool(CGI*)> handle_request) { +#if HAVE_FASTCGI + if (!FCGX_IsCGI()) { + std::unique_ptr<FastCGIImpl> cgi(new FastCGIImpl()); + auto cin_streambuf = std::cin.rdbuf(); + auto cout_streambuf = std::cout.rdbuf(); + auto cerr_streambuf = std::cerr.rdbuf(); + + FCGX_Stream* in; + FCGX_Stream* out; + FCGX_Stream* err; + FCGX_ParamArray params; + while (FCGX_Accept(&in, &out, &err, ¶ms) == 0) { + cgi->reset(params); + fcgi_streambuf cin_fcgi_streambuf(in); + fcgi_streambuf cout_fcgi_streambuf(out); + fcgi_streambuf cerr_fcgi_streambuf(err); + + std::cin.rdbuf(&cin_fcgi_streambuf); + std::cout.rdbuf(&cout_fcgi_streambuf); + std::cerr.rdbuf(&cerr_fcgi_streambuf); + + if (!handle_request(cgi.get())) { + FCGX_SetExitStatus(-1, out); + FCGX_Finish(); + return -1; + } + } + + std::cin.rdbuf(cin_streambuf); + std::cout.rdbuf(cout_streambuf); + std::cerr.rdbuf(cerr_streambuf); + + return 0; + } +#endif + std::unique_ptr<CGIImpl> cgi(new BasicCGIImpl()); + return handle_request(cgi.get()) ? 0 : -1; +} + +} // namespace stuff + diff --git a/src/cgi.hh b/src/cgi.hh new file mode 100644 index 0000000..5879694 --- /dev/null +++ b/src/cgi.hh @@ -0,0 +1,42 @@ +#ifndef CGI_HH +#define CGI_HH + +#include <functional> +#include <map> +#include <string> +#include <vector> + +namespace stuff { + +class CGI { +public: + enum request_type { + GET, + HEAD, + POST, + PUT, + TRACE, + + UNKNOWN, + }; + + virtual void post_data(std::vector<char>* data) = 0; + // Return true if post data is multipart, false otherwise + virtual bool post_data(std::map<std::string,std::string>* data) = 0; + virtual void query_data(std::map<std::string,std::string>* data) = 0; + virtual std::string request_path() = 0; + virtual request_type request_type() = 0; + virtual std::string content_type() = 0; + + static int run(std::function<bool(CGI*)> handle_request); + +protected: + CGI() {} + virtual ~CGI() {} + CGI(const CGI&) = delete; + CGI& operator=(const CGI&) = delete; +}; + +} // namespace stuff + +#endif /* CGI_HH */ diff --git a/src/common.hh b/src/common.hh index 3b69aee..d42e578 100644 --- a/src/common.hh +++ b/src/common.hh @@ -1,6 +1,10 @@ #ifndef COMMON_HH #define COMMON_HH +#if HAVE_CONFIG_H +# include "config.h" +#endif + #include <cassert> #endif /* COMMON_HH */ diff --git a/src/header_parser.cc b/src/header_parser.cc new file mode 100644 index 0000000..163d65f --- /dev/null +++ b/src/header_parser.cc @@ -0,0 +1,107 @@ +#include "common.hh" + +#include "header_parser.hh" + +namespace stuff { + +namespace { + +bool read_token(std::string::const_iterator* begin, + const std::string::const_iterator& end, + std::string* out) { + auto i = *begin; + if (i == end) return false; + auto last = i; + out->clear(); + do { + if ((*i >= '^' && *i <= 'z') || + (*i >= '0' && *i <= '9') || + (*i >= '#' && *i <= '\'') || + *i == '!' || *i == '*' || *i == '+' || *i == '-' || *i == '.' || + *i == '|' || *i == '~') { + ++i; + } else if (*i >= 'A' && *i <= 'Z') { + out->insert(out->end(), last, i); + out->push_back('a' + *i - 'A'); + last = ++i; + } else { + break; + } + } while (i != end); + out->insert(out->end(), last, i); + *begin = i; + return true; +} + +bool read_quoted(std::string::const_iterator* begin, + const std::string::const_iterator& end, + std::string* out) { + auto i = *begin; + if (i == end || *i != '"') return false; + auto last = ++i; + out->clear(); + while (true) { + if (*i == '"') { + out->insert(out->end(), last, i); + *begin = ++i; + return true; + } else if (*i == '\\') { + out->insert(out->end(), last, i); + if (++i == end) return false; + if ((*i >= ' ' && *i <= '~') || + *i == '\t' || + (*i & 0x80)) { + out->push_back(*i); + last = ++i; + } else { + return false; + } + } else if ((*i >= '^' && *i <= '~') || + (*i >= '#' && *i <= '[') || + *i == '\t' || *i == ' ' || + (*i & 0x80)) { + ++i; + } else { + return false; + } + } +} + +} // namespace + +bool HeaderParser::parse(const std::string& in, std::string* token, + std::map<std::string, std::string>* parameters) { + auto i = in.begin(); + while (i != in.end() && (*i == ' ' || *i == '\t')) ++i; + if (!read_token(&i, in.end(), token)) return false; + while (true) { + if (i == in.end() || *i != '/') break; + std::string tmp; + token->push_back('/'); + ++i; + if (!read_token(&i, in.end(), &tmp)) return false; + token->append(tmp); + } + if (parameters) parameters->clear(); + while (i != in.end()) { + while (i != in.end() && (*i == ' ' || *i == '\t')) ++i; + if (i == in.end()) break; + if (*i != ';') return false; + ++i; + while (i != in.end() && (*i == ' ' || *i == '\t')) ++i; + if (i == in.end()) return false; + std::string key, value; + if (!read_token(&i, in.end(), &key)) return false; + if (i == in.end() || *i != '=') return false; + ++i; + if (i != in.end() && *i == '"') { + if (!read_quoted(&i, in.end(), &value)) return false; + } else { + if (!read_token(&i, in.end(), &value)) return false; + } + if (parameters) (*parameters)[key] = value; + } + return true; +} + +} // namespace stuff diff --git a/src/header_parser.hh b/src/header_parser.hh new file mode 100644 index 0000000..b0bfd60 --- /dev/null +++ b/src/header_parser.hh @@ -0,0 +1,24 @@ +#ifndef HEADER_PARSER_HH +#define HEADER_PARSER_HH + +#include <map> +#include <string> + +namespace stuff { + +class HeaderParser { +public: + // token and keys to parameters will all be returned as lowercase ascii + static bool parse(const std::string& in, std::string* token, + std::map<std::string, std::string>* parameters); + +private: + HeaderParser() {} + ~HeaderParser() {} + HeaderParser(const HeaderParser&) = delete; + HeaderParser& operator=(const HeaderParser&) = delete; +}; + +} // namespace stuff + +#endif /* HEADER_PARSER_HH */ diff --git a/src/multipart_formdata_parser.cc b/src/multipart_formdata_parser.cc new file mode 100644 index 0000000..6802bc8 --- /dev/null +++ b/src/multipart_formdata_parser.cc @@ -0,0 +1,147 @@ +#include "common.hh" + +#include <algorithm> + +#include "header_parser.hh" +#include "multipart_formdata_parser.hh" +#include "strutils.hh" + +namespace stuff { + +namespace { + +template<typename Iterator> +Iterator find_boundary(Iterator begin, Iterator end, + const std::string& boundary, + bool* last) { + Iterator start, test; + for (auto it = begin; it != end; ++it) { + if (it == begin && *it == '-') { + start = it; + test = it + 1; + if (test == end || *test != '-') continue; + } else if (*it == '\r') { + start = it; + test = it + 1; + if (test == end || *test != '\n') continue; + ++test; + if (test == end || *test != '-') continue; + ++test; + if (test == end || *test != '-') continue; + } else { + continue; + } + ++test; + if (static_cast<size_t>(end - test) <= boundary.size()) return end; + if (boundary.compare(0, std::string::npos, + &(*test), boundary.size()) == 0) { + test += boundary.size(); + if (test == end) return end; + if (*test == '-') { + ++test; + if (test == end || *test != '-') continue; + *last = true; + return start; + } else if (*test == '\r') { + ++test; + if (test == end || *test != '\n') continue; + *last = false; + return start; + } + } + } + return end; +} + +template<typename Iterator> +bool parse_part(Iterator begin, Iterator end, + std::map<std::string, std::string>* out) { + static const char EOL[] = "\r\n"; + bool have_name = false, ok_contenttype = true, ok_encoding = true, + ok_content = true; + std::string name; + while (true) { + auto eol = std::search(begin, end, EOL, EOL + 2); + if (eol == end) return false; + if (eol == begin) { + begin += 2; + break; + } + auto colon = std::find(begin, eol, ':'); + if (colon == eol) return false; + std::string header = + ascii_tolower(std::string(&(*begin), colon - begin)); + ++colon; + if (colon == eol) return false; + if (header == "content-disposition" || header == "content-type" || + header == "content-transfer-encoding") { + std::string token; + std::map<std::string, std::string> params; + std::string value = std::string(&(*colon), eol - colon); + if (!HeaderParser::parse(std::string(&(*colon), eol - colon), + &token, ¶ms)) return false; + if (header[9] == 'i') { // content-disposition + if (token == "form-data") { + auto it = params.find("name"); + if (it == params.end()) return false; + have_name = true; + name = it->second; + ok_content = true; + } else { + ok_content = false; + } + } else if (header[9] == 'y') { // content-type + auto pos = token.find('/'); + if (pos == std::string::npos) return false; + ok_contenttype = true; + if (token.compare(0, pos, "text") != 0) ok_contenttype = false; + if (ok_contenttype) { + auto it = params.find("charset"); + if (it != params.end()) { + std::string charset = ascii_tolower(it->second); + ok_contenttype = charset == "ascii" || + charset == "us-ascii" || + charset == "utf-8"; + } + } + } else /* if (header[9] == 'r') */ { // content-transfer-encoding + ok_encoding = token == "7bit" || token == "8bit" || + token == "binary" || token == "identity"; + } + } + begin = eol + 2; + } + if (have_name && ok_contenttype && ok_encoding && ok_content) { + (*out)[name] = std::string(&(*begin), end - begin); + } + return true; +} + +} // namespace + +bool MultipartFormDataParser::parse(const std::vector<char>& in, + const std::string& boundary, + std::map<std::string, std::string>* out) { + bool last; + auto start = find_boundary(in.begin(), in.end(), boundary, &last); + out->clear(); + if (start == in.end()) return in.empty(); + if (last) return true; + if (*start != '-') start += 2; + while (true) + { + start += boundary.size() + 4; + auto end = find_boundary(start, in.end(), boundary, &last); + if (end == in.end()) return false; + + if (!parse_part(start, end, out)) { + return false; + } + + if (last) return true; + + start = end + 2; + } +} + +} // namespace stuff diff --git a/src/multipart_formdata_parser.hh b/src/multipart_formdata_parser.hh new file mode 100644 index 0000000..f6bfcd6 --- /dev/null +++ b/src/multipart_formdata_parser.hh @@ -0,0 +1,25 @@ +#ifndef MULTIPART_FORMDATA_PARSER_HH +#define MULTIPART_FORMDATA_PARSER_HH + +#include <map> +#include <string> +#include <vector> + +namespace stuff { + +class MultipartFormDataParser { +public: + static bool parse(const std::vector<char>& in, + const std::string& boundary, + std::map<std::string, std::string>* out); + +private: + MultipartFormDataParser() {} + ~MultipartFormDataParser() {} + MultipartFormDataParser(const MultipartFormDataParser&) = delete; + MultipartFormDataParser& operator=(const MultipartFormDataParser&) = delete; +}; + +} // namespace stuff + +#endif /* MULTIPART_FORMDATA_PARSER_HH */ diff --git a/src/query_parser.cc b/src/query_parser.cc new file mode 100644 index 0000000..d957063 --- /dev/null +++ b/src/query_parser.cc @@ -0,0 +1,82 @@ +#include "common.hh" + +#include "query_parser.hh" + +namespace stuff { + +namespace { + +bool hex(char in, uint8_t* out) { + if (in >= '0' && in <= '9') { + *out = in - '0'; + } else if (in >= 'A' && in <= 'F') { + *out = 10 + in - 'A'; + } else if (in >= 'a' && in <= 'f') { + *out = 10 + in - 'a'; + } else { + return false; + } + return true; +} + +} // namespace + +bool QueryParser::parse(const std::string& in, + std::map<std::string, std::string>* out) { + out->clear(); + if (in.empty()) return true; + auto i = in.begin(); + if (*i == '?' || *i == '&') { + ++i; + } + std::string key, value; + bool have_key = false; + while (true) { + char c; + if (i == in.end() || *i == '&') { + if (!have_key && key.empty()) return false; + (*out)[key] = value; + if (i == in.end()) return true; + have_key = false; + key.clear(); + value.clear(); + if (++i == in.end()) return true; + continue; + } else if (*i == '=') { + if (have_key) return false; + if (key.empty()) return false; + have_key = true; + ++i; + continue; + } else if (*i == '+') { + c = ' '; + } else if (*i == '%') { + if (++i == in.end()) return false; + uint8_t h, l; + if (!hex(*i, &h)) return false; + if (++i == in.end()) return false; + if (!hex(*i, &l)) return false; + c = (h << 4) | l; + } else if ((*i >= 'a' && *i <= 'z') || + (*i >= '@' && *i <= 'Z') || + (*i >= '0' && *i <= ';') || + (*i >= '\'' && *i <= '*') || + (*i >= ',' && *i <= '.') || + *i == '!' || *i == '$' || + *i == '_' || *i == '~') { + c = *i; + } else { + // Character not allowed + return false; + } + if (have_key) { + value.push_back(c); + } else { + key.push_back(c); + } + ++i; + } +} + + +} // namespace stuff diff --git a/src/query_parser.hh b/src/query_parser.hh new file mode 100644 index 0000000..4c12502 --- /dev/null +++ b/src/query_parser.hh @@ -0,0 +1,23 @@ +#ifndef QUERY_PARSER_HH +#define QUERY_PARSER_HH + +#include <map> +#include <string> + +namespace stuff { + +class QueryParser { +public: + static bool parse(const std::string& in, + std::map<std::string, std::string>* out); + +private: + QueryParser() {} + ~QueryParser() {} + QueryParser(const QueryParser&) = delete; + QueryParser& operator=(const QueryParser&) = delete; +}; + +} // namespace stuff + +#endif /* QUERY_PARSER_HH */ diff --git a/src/strutils.cc b/src/strutils.cc new file mode 100644 index 0000000..e7b7c93 --- /dev/null +++ b/src/strutils.cc @@ -0,0 +1,29 @@ +#include "common.hh" + +#include "strutils.hh" + +namespace stuff { + +std::string ascii_tolower(const std::string& str) { + for (auto it = str.begin(); it != str.end(); ++it) { + if (*it >= 'A' && *it <= 'Z') { + std::string ret(str.begin(), it); + ret.push_back('a' + *it - 'A'); + auto last = ++it; + while (it != str.end()) { + if (*it >= 'A' && *it <= 'Z') { + ret.append(last, it); + ret.push_back('a' + *it - 'A'); + last = ++it; + } else { + ++it; + } + } + ret.append(last, it); + return ret; + } + } + return str; +} + +} // namespace stuff diff --git a/src/strutils.hh b/src/strutils.hh new file mode 100644 index 0000000..76120a8 --- /dev/null +++ b/src/strutils.hh @@ -0,0 +1,12 @@ +#ifndef STRUTILS_HH +#define STRUTILS_HH + +#include <string> + +namespace stuff { + +std::string ascii_tolower(const std::string& str); + +} // namespace stuff + +#endif /* STRUTILS_HH */ |
