summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile.am2
-rw-r--r--configure.ac15
-rw-r--r--src/.gitignore1
-rw-r--r--src/Makefile.am10
-rw-r--r--src/cgi.cc213
-rw-r--r--src/cgi.hh42
-rw-r--r--src/common.hh4
-rw-r--r--src/header_parser.cc107
-rw-r--r--src/header_parser.hh24
-rw-r--r--src/multipart_formdata_parser.cc147
-rw-r--r--src/multipart_formdata_parser.hh25
-rw-r--r--src/query_parser.cc82
-rw-r--r--src/query_parser.hh23
-rw-r--r--src/strutils.cc29
-rw-r--r--src/strutils.hh12
-rw-r--r--test/.gitignore5
-rw-r--r--test/Makefile.am16
-rw-r--r--test/test-header-parser.cc83
-rw-r--r--test/test-multipart-formdata-parser.cc109
-rw-r--r--test/test-query-parser.cc88
20 files changed, 1034 insertions, 3 deletions
diff --git a/Makefile.am b/Makefile.am
index 62391b5..1107628 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -4,4 +4,4 @@ MAINTAINERCLEANFILES = Makefile.in aclocal.m4 config.guess config.h.in \
config.sub configure depcomp install-sh ltmain.sh \
missing config.rpath mkinstalldirs compile
-SUBDIRS = src
+SUBDIRS = src test
diff --git a/configure.ac b/configure.ac
index 9f1a200..ea86ca8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -59,7 +59,20 @@ AC_SUBST([DEFINES])
# 3.6.5 so that sqlite3_changes() return correct values for DELETE
PKG_CHECK_MODULES([SQLITE3],[sqlite3 >= 3.6.5])
+# FastCGI
+
+have_fastcgi=1
+AC_CHECK_HEADER([fcgiapp.h],[],[have_fastcgi=0])
+AC_CHECK_LIB([fcgi],[FCGX_Accept],[],[have_fastcgi=0],[-lfcgi++])
+if test "x$have_fastcgi" = "x1"; then
+ FASTCGI_CFLAGS=
+ FASTCGI_LIBS="-lfcgi++ -lfcgi"
+fi
+AC_SUBST(FASTCGI_CFLAGS)
+AC_SUBST(FASTCGI_LIBS)
+AC_DEFINE_UNQUOTED([HAVE_FASTCGI],[$have_fastcgi],[define to 1 if FastCGI is available])
+
# Finish up
AC_CONFIG_HEADERS([src/config.h])
-AC_OUTPUT([Makefile src/Makefile])
+AC_OUTPUT([Makefile src/Makefile test/Makefile])
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, &params)) {
+ 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, &params) == 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, &params)) 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 */
diff --git a/test/.gitignore b/test/.gitignore
new file mode 100644
index 0000000..8ab7990
--- /dev/null
+++ b/test/.gitignore
@@ -0,0 +1,5 @@
+/test-*.log
+/*.trs
+/test-header-parser
+/test-multipart-formdata-parser
+/test-query-parser
diff --git a/test/Makefile.am b/test/Makefile.am
new file mode 100644
index 0000000..3017b6f
--- /dev/null
+++ b/test/Makefile.am
@@ -0,0 +1,16 @@
+MAINTAINERCLEANFILES = Makefile.in
+
+AM_CPPFLAGS = @DEFINES@ -I$(top_builddir)/src
+
+TESTS = test-query-parser test-header-parser test-multipart-formdata-parser
+
+check_PROGRAMS = $(TESTS)
+
+test_query_parser_SOURCES = test-query-parser.cc
+test_query_parser_LDADD = $(top_srcdir)/src/libcgi.la
+
+test_header_parser_SOURCES = test-header-parser.cc
+test_header_parser_LDADD = $(top_srcdir)/src/libcgi.la
+
+test_multipart_formdata_parser_SOURCES = test-multipart-formdata-parser.cc
+test_multipart_formdata_parser_LDADD = $(top_srcdir)/src/libcgi.la
diff --git a/test/test-header-parser.cc b/test/test-header-parser.cc
new file mode 100644
index 0000000..c2b78f7
--- /dev/null
+++ b/test/test-header-parser.cc
@@ -0,0 +1,83 @@
+#include "common.hh"
+
+#include <cstdarg>
+#include <cstdlib>
+#include <iostream>
+
+#include "header_parser.hh"
+
+using namespace stuff;
+
+namespace {
+
+bool test(const char* in, const char* token, ...) {
+ std::string tmp_token;
+ std::map<std::string, std::string> tmp;
+ if (!HeaderParser::parse(in, &tmp_token, &tmp)) {
+ std::cerr << "expected success: " << in << std::endl;
+ return false;
+ }
+ if (tmp_token.compare(token) != 0) {
+ std::cerr << "expected token not to be " << tmp_token << " in: "
+ << in << std::endl;
+ return false;
+ }
+ va_list args;
+ va_start(args, token);
+ while (true) {
+ const char* key = va_arg(args, const char*);
+ if (!key) break;
+ const char* value = va_arg(args, const char*);
+ auto it = tmp.find(key);
+ if (it == tmp.end()) {
+ std::cerr << "expected a value for " << key
+ << " in: " << in << std::endl;
+ return false;
+ }
+ if (it->second.compare(value) != 0) {
+ std::cerr << "expected value for " << key << " not to be "
+ << it->second << " in: " << in << std::endl;
+ return false;
+ }
+ tmp.erase(it);
+ }
+ va_end(args);
+ for (auto& pair : tmp) {
+ std::cerr << "unexpected value for " << pair.first << ": "
+ << pair.second << " in: " << in << std::endl;
+ return false;
+ }
+ return true;
+}
+
+bool test_fail(const std::string& in) {
+ std::string token;
+ if (!HeaderParser::parse(in, &token, nullptr)) return true;
+ std::cerr << "expected fail: " << in << std::endl;
+ return false;
+}
+
+} // namespace
+
+int main() {
+ unsigned int ok = 0, tot = 0;
+
+ tot++; if (test("text/html", "text/html", NULL)) ok++;
+ tot++; if (test("text/html;charset=utf-8",
+ "text/html", "charset", "utf-8", NULL)) ok++;
+ tot++; if (test("text/html;charset=UTF-8",
+ "text/html", "charset", "utf-8", NULL)) ok++;
+ tot++; if (test("Text/HTML;Charset=\"utf-8\"",
+ "text/html", "charset", "utf-8", NULL)) ok++;
+ tot++; if (test("text/html; charset=\"utf-8\"",
+ "text/html", "charset", "utf-8", NULL)) ok++;
+ tot++; if (test("a/b; key=\"\\\"\"", "a/b", "key", "\"", NULL)) ok++;
+ tot++; if (test_fail("")) ok++;
+ tot++; if (test_fail("text/html;charset = utf-8")) ok++;
+ tot++; if (test_fail("a/b;key=\"")) ok++;
+ tot++; if (test_fail("a/b;key=\"\\")) ok++;
+ tot++; if (test_fail("a/b;key=\"\\\"")) ok++;
+
+ std::cout << "OK " << ok << "/" << tot << std::endl;
+ return ok == tot ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/test/test-multipart-formdata-parser.cc b/test/test-multipart-formdata-parser.cc
new file mode 100644
index 0000000..a5b57e0
--- /dev/null
+++ b/test/test-multipart-formdata-parser.cc
@@ -0,0 +1,109 @@
+#include "common.hh"
+
+#include <cstdarg>
+#include <cstdlib>
+#include <iostream>
+
+#include "multipart_formdata_parser.hh"
+
+using namespace stuff;
+
+namespace {
+
+bool test(const std::string& in, const char* boundary, ...) {
+ std::map<std::string, std::string> tmp;
+ std::vector<char> data(in.begin(), in.end());
+ if (!MultipartFormDataParser::parse(data, boundary, &tmp)) {
+ std::cerr << "expected success: " << in << std::endl;
+ return false;
+ }
+ va_list args;
+ va_start(args, boundary);
+ while (true) {
+ const char* key = va_arg(args, const char*);
+ if (!key) break;
+ const char* value = va_arg(args, const char*);
+ auto it = tmp.find(key);
+ if (it == tmp.end()) {
+ std::cerr << "expected a value for " << key
+ << " in: " << in << std::endl;
+ return false;
+ }
+ if (it->second.compare(value) != 0) {
+ std::cerr << "expected value for " << key << " not to be "
+ << it->second << " in: " << in << std::endl;
+ return false;
+ }
+ tmp.erase(it);
+ }
+ va_end(args);
+ for (auto& pair : tmp) {
+ std::cerr << "unexpected value for " << pair.first << ": "
+ << pair.second << " in: " << in << std::endl;
+ return false;
+ }
+ return true;
+}
+
+bool test_fail(const std::string& in, const std::string& boundary) {
+ std::map<std::string, std::string> tmp;
+ std::vector<char> data(in.begin(), in.end());
+ if (!MultipartFormDataParser::parse(data, boundary, &tmp)) return true;
+ std::cerr << "expected fail: " << in << std::endl;
+ return false;
+}
+
+} // namespace
+
+int main() {
+ unsigned int ok = 0, tot = 0;
+
+ tot++; if (test("", "AaB03x", NULL)) ok++;
+
+ tot++; if (test(
+"--AaB03x\r\n"
+"Content-Disposition: form-data; name=\"submit-name\"\r\n"
+"\r\n"
+"Larry\r\n"
+"--AaB03x\r\n"
+"Content-Disposition: form-data; name=\"files\"; filename=\"file1.txt\"\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+"... contents of file1.txt ...\r\n"
+"--AaB03x--\r\n",
+"AaB03x",
+"submit-name", "Larry",
+"files", "... contents of file1.txt ...",
+NULL)) ok++;
+
+ tot++; if (test(
+"--AaB03x\r\n"
+"Content-Disposition: form-data; name=\"submit-name\"\r\n"
+"\r\n"
+"Larry\r\n"
+"--AaB03x\r\n"
+"Content-Disposition: form-data; name=\"files\"\r\n"
+"Content-Type: multipart/mixed; boundary=BbC04y\r\n"
+"\r\n"
+"--BbC04y\r\n"
+"Content-Disposition: file; filename=\"file1.txt\"\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+"... contents of file1.txt ...\r\n"
+"--BbC04y\r\n"
+"Content-Disposition: file; filename=\"file2.gif\"\r\n"
+"Content-Type: image/gif\r\n"
+"Content-Transfer-Encoding: binary\r\n"
+"\r\n"
+"...contents of file2.gif...\r\n"
+"--BbC04y--\r\n"
+"--AaB03x--\r\n",
+"AaB03x",
+"submit-name", "Larry",
+NULL)) ok++;
+
+ tot++; if (test_fail("--X", "X")) ok++;
+
+ std::cout << "OK " << ok << "/" << tot << std::endl;
+ return ok == tot ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/test/test-query-parser.cc b/test/test-query-parser.cc
new file mode 100644
index 0000000..d6d10cd
--- /dev/null
+++ b/test/test-query-parser.cc
@@ -0,0 +1,88 @@
+#include "common.hh"
+
+#include <cstdarg>
+#include <cstdlib>
+#include <iostream>
+
+#include "query_parser.hh"
+
+using namespace stuff;
+
+namespace {
+
+bool test(const char* in, ...) {
+ std::map<std::string, std::string> tmp;
+ if (!QueryParser::parse(in, &tmp)) {
+ std::cerr << "expected success: " << in << std::endl;
+ return false;
+ }
+ va_list args;
+ va_start(args, in);
+ while (true) {
+ const char* key = va_arg(args, const char*);
+ if (!key) break;
+ const char* value = va_arg(args, const char*);
+ auto it = tmp.find(key);
+ if (it == tmp.end()) {
+ std::cerr << "expected a value for " << key
+ << " in: " << in << std::endl;
+ return false;
+ }
+ if (it->second.compare(value) != 0) {
+ std::cerr << "expected value for " << key << " not to be "
+ << it->second << " in: " << in << std::endl;
+ return false;
+ }
+ tmp.erase(it);
+ }
+ va_end(args);
+ for (auto& pair : tmp) {
+ std::cerr << "unexpected value for " << pair.first << ": "
+ << pair.second << " in: " << in << std::endl;
+ return false;
+ }
+ return true;
+}
+
+bool test_fail(const std::string& in) {
+ std::map<std::string, std::string> tmp;
+ if (!QueryParser::parse(in, &tmp)) return true;
+ std::cerr << "expected fail: " << in << std::endl;
+ return false;
+}
+
+} // namespace
+
+int main() {
+ unsigned int ok = 0, tot = 0;
+
+ tot++; if (test("", NULL)) ok++;
+ tot++; if (test("key=value", "key", "value", NULL)) ok++;
+ tot++; if (test("?key=value", "key", "value", NULL)) ok++;
+ tot++; if (test("?key=value&", "key", "value", NULL)) ok++;
+ tot++; if (test("key=value&foo=bar",
+ "key", "value", "foo", "bar", NULL)) ok++;
+ tot++; if (test("key=value&key=bar", "key", "bar", NULL)) ok++;
+ tot++; if (test("key=", "key", "", NULL)) ok++;
+ tot++; if (test("key=%c3%A5", "key", "\xc3\xa5", NULL)) ok++;
+ tot++; if (test("key=foo%20bar", "key", "foo bar", NULL)) ok++;
+ tot++; if (test("key=foo+bar", "key", "foo bar", NULL)) ok++;
+ tot++;
+ if (test("first=this+is+a+field&second=was+it+clear+%28already%29%3F",
+ "first", "this is a field",
+ "second", "was it clear (already)?",
+ NULL)) ok++;
+ tot++; if (test_fail("=value")) ok++;
+ tot++; if (test_fail("=")) ok++;
+ tot++; if (test_fail("\xc3\xa5")) ok++;
+ tot++; if (test_fail("&&")) ok++;
+ tot++; if (test_fail("key=&&")) ok++;
+ tot++; if (test_fail("==")) ok++;
+ tot++; if (test_fail("=&=")) ok++;
+ tot++; if (test_fail("%")) ok++;
+ tot++; if (test_fail("%A")) ok++;
+ tot++; if (test_fail("%1z")) ok++;
+
+ std::cout << "OK " << ok << "/" << tot << std::endl;
+ return ok == tot ? EXIT_SUCCESS : EXIT_FAILURE;
+}