summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvo van Dongen <info@ivovandongen.nl>2016-07-05 11:26:56 +0200
committerBruno de Oliveira Abinader <bruno@mapbox.com>2016-11-17 01:04:16 +0100
commit483df75a9687eac48cc93568889eb3c0f93d2af9 (patch)
treebccda5745f3dd3588ec1a248e963b5914ef8e36d
parenta27bda98510f4a32c4ea7ea3c18489bd12e75cc6 (diff)
downloadqtlocation-mapboxgl-483df75a9687eac48cc93568889eb3c0f93d2af9.tar.gz
[android] Initial cpp http server
-rw-r--r--test/src/mbgl/test/httplib.h959
-rw-r--r--test/src/mbgl/test/server.cpp178
-rw-r--r--test/src/mbgl/test/server.hpp27
-rw-r--r--test/src/mbgl/test/test.cpp24
-rw-r--r--test/storage/http_file_source.test.cpp2
5 files changed, 1183 insertions, 7 deletions
diff --git a/test/src/mbgl/test/httplib.h b/test/src/mbgl/test/httplib.h
new file mode 100644
index 0000000000..a07511dcf2
--- /dev/null
+++ b/test/src/mbgl/test/httplib.h
@@ -0,0 +1,959 @@
+//
+// httplib.h
+//
+// Copyright (c) 2012 Yuji Hirose. All rights reserved.
+// The Boost Software License 1.0
+//
+
+#ifndef _CPPHTTPLIB_HTTPSLIB_H_
+#define _CPPHTTPLIB_HTTPSLIB_H_
+
+#ifdef _MSC_VER
+#define _CRT_SECURE_NO_WARNINGS
+#define _CRT_NONSTDC_NO_DEPRECATE
+
+#ifndef SO_SYNCHRONOUS_NONALERT
+#define SO_SYNCHRONOUS_NONALERT 0x20
+#endif
+#ifndef SO_OPENTYPE
+#define SO_OPENTYPE 0x7008
+#endif
+#if (_MSC_VER < 1900)
+#define snprintf _snprintf_s
+#endif
+
+#define S_ISREG(m) (((m)&S_IFREG)==S_IFREG)
+#define S_ISDIR(m) (((m)&S_IFDIR)==S_IFDIR)
+
+#include <fcntl.h>
+#include <io.h>
+#include <winsock2.h>
+#include <ws2tcpip.h>
+
+#undef min
+#undef max
+
+typedef SOCKET socket_t;
+#else
+#include <pthread.h>
+#include <unistd.h>
+#include <netdb.h>
+#include <cstring>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <sys/socket.h>
+
+typedef int socket_t;
+#endif
+
+#include <fstream>
+#include <functional>
+#include <map>
+#include <memory>
+#include <regex>
+#include <string>
+#include <sys/stat.h>
+#include <assert.h>
+
+namespace httplib
+{
+
+typedef std::map<std::string, std::string> Map;
+typedef std::multimap<std::string, std::string> MultiMap;
+typedef std::smatch Match;
+
+struct Request {
+ std::string method;
+ std::string url;
+ MultiMap headers;
+ std::string body;
+ Map params;
+ Match matches;
+
+ bool has_header(const char* key) const;
+ std::string get_header_value(const char* key) const;
+ void set_header(const char* key, const char* val);
+
+ bool has_param(const char* key) const;
+};
+
+struct Response {
+ int status;
+ MultiMap headers;
+ std::string body;
+
+ bool has_header(const char* key) const;
+ std::string get_header_value(const char* key) const;
+ void set_header(const char* key, const char* val);
+
+ void set_redirect(const char* url);
+ void set_content(const char* s, size_t n, const char* content_type);
+ void set_content(const std::string& s, const char* content_type);
+
+ Response() : status(-1) {}
+};
+
+class Server {
+public:
+ typedef std::function<void (const Request&, Response&)> Handler;
+ typedef std::function<void (const Request&, const Response&)> Logger;
+
+ Server();
+
+ void get(const char* pattern, Handler handler);
+ void post(const char* pattern, Handler handler);
+
+ bool set_base_dir(const char* path);
+
+ void set_error_handler(Handler handler);
+ void set_logger(Logger logger);
+
+ bool listen(const char* host, int port);
+ void stop();
+
+private:
+ typedef std::vector<std::pair<std::regex, Handler>> Handlers;
+
+ void process_request(FILE* fp_read, FILE* fp_write);
+ bool read_request_line(FILE* fp, Request& req);
+ bool routing(Request& req, Response& res);
+ bool handle_file_request(Request& req, Response& res);
+ bool dispatch_request(Request& req, Response& res, Handlers& handlers);
+
+ socket_t svr_sock_;
+ std::string base_dir_;
+ Handlers get_handlers_;
+ Handlers post_handlers_;
+ Handler error_handler_;
+ Logger logger_;
+};
+
+class Client {
+public:
+ Client(const char* host, int port);
+
+ std::shared_ptr<Response> get(const char* url);
+ std::shared_ptr<Response> head(const char* url);
+ std::shared_ptr<Response> post(const char* url, const std::string& body, const char* content_type);
+ std::shared_ptr<Response> post(const char* url, const Map& params);
+
+ bool send(const Request& req, Response& res);
+
+private:
+ bool read_response_line(FILE* fp, Response& res);
+
+ const std::string host_;
+ const int port_;
+};
+
+// Implementation
+namespace detail {
+
+template <class Fn>
+void split(const char* b, const char* e, char d, Fn fn)
+{
+ int i = 0;
+ int beg = 0;
+
+ while (e ? (b + i != e) : (b[i] != '\0')) {
+ if (b[i] == d) {
+ fn(&b[beg], &b[i]);
+ beg = i + 1;
+ }
+ i++;
+ }
+
+ if (i) {
+ fn(&b[beg], &b[i]);
+ }
+}
+
+template <typename T>
+inline bool read_and_close_socket(socket_t sock, T callback)
+{
+ FILE* fp_read;
+ FILE* fp_write;
+#ifdef _MSC_VER
+ int osfhandle = _open_osfhandle(sock, _O_RDONLY);
+ fp_read = _fdopen(osfhandle, "rb");
+ fp_write = _fdopen(osfhandle, "wb");
+#else
+ fp_read = fdopen(sock, "rb");
+ fp_write = fdopen(sock, "wb");
+#endif
+
+ auto ret = callback(fp_read, fp_write);
+
+#ifdef _MSC_VER
+ sock = osfhandle;
+#else
+ fclose(fp_read);
+ fclose(fp_write);
+#endif
+
+ return ret;
+}
+
+inline int shutdown_socket(socket_t sock)
+{
+#ifdef _MSC_VER
+ return shutdown(sock, SD_BOTH);
+#else
+ return shutdown(sock, SHUT_RDWR);
+#endif
+}
+
+inline int close_socket(socket_t sock)
+{
+#ifdef _MSC_VER
+ return closesocket(sock);
+#else
+ return close(sock);
+#endif
+}
+
+template <typename Fn>
+socket_t create_socket(const char* host, int port, Fn fn)
+{
+#ifdef _MSC_VER
+ int opt = SO_SYNCHRONOUS_NONALERT;
+ setsockopt(INVALID_SOCKET, SOL_SOCKET, SO_OPENTYPE, (char*)&opt, sizeof(opt));
+#endif
+
+ // Get address info
+ struct addrinfo hints;
+ struct addrinfo *result;
+
+ memset(&hints, 0, sizeof(struct addrinfo));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = 0;
+ hints.ai_protocol = 0;
+
+ auto service = std::to_string(port);
+
+ if (getaddrinfo(host, service.c_str(), &hints, &result)) {
+ return -1;
+ }
+
+ for (auto rp = result; rp; rp = rp->ai_next) {
+ // Create a socket
+ auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+ if (sock == -1) {
+ continue;
+ }
+
+ // Make 'reuse address' option available
+ int yes = 1;
+ setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)&yes, sizeof(yes));
+
+ // bind or connect
+ if (fn(sock, *rp)) {
+ freeaddrinfo(result);
+ return sock;
+ }
+
+ close_socket(sock);
+ }
+
+ freeaddrinfo(result);
+ return -1;
+}
+
+inline socket_t create_server_socket(const char* host, int port)
+{
+ return create_socket(host, port, [](socket_t sock, struct addrinfo& ai) -> socket_t {
+ if (::bind(sock, ai.ai_addr, ai.ai_addrlen)) {
+ return false;
+ }
+ if (listen(sock, 5)) { // Listen through 5 channels
+ return false;
+ }
+ return true;
+ });
+}
+
+inline socket_t create_client_socket(const char* host, int port)
+{
+ return create_socket(host, port, [](socket_t sock, struct addrinfo& ai) -> socket_t {
+ if (connect(sock, ai.ai_addr, ai.ai_addrlen)) {
+ return false;
+ }
+ return true;
+ });
+}
+
+inline bool is_file(const std::string& s)
+{
+ struct stat st;
+ return stat(s.c_str(), &st) >= 0 && S_ISREG(st.st_mode);
+}
+
+inline bool is_dir(const std::string& s)
+{
+ struct stat st;
+ return stat(s.c_str(), &st) >= 0 && S_ISDIR(st.st_mode);
+}
+
+inline void read_file(const std::string& path, std::string& out)
+{
+ std::ifstream fs(path, std::ios_base::binary);
+ fs.seekg(0, std::ios_base::end);
+ auto size = fs.tellg();
+ fs.seekg(0);
+ out.resize(size);
+ fs.read(&out[0], size);
+}
+
+inline std::string get_file_extention(const std::string& path)
+{
+ std::smatch m;
+ auto pat = std::regex("\\.([a-zA-Z0-9]+)$");
+ auto ret = std::regex_search(path, m, pat);
+ std::string content_type;
+ if (ret) {
+ return m[1].str();
+ }
+ return std::string();
+}
+
+inline const char* get_content_type_from_file_extention(const std::string& ext)
+{
+ if (ext == "html") {
+ return "text/html";
+ }
+ return "text/plain";
+}
+
+inline const char* status_message(int status)
+{
+ switch (status) {
+ case 200: return "OK";
+ case 400: return "Bad Request";
+ case 404: return "Not Found";
+ default:
+ case 500: return "Internal Server Error";
+ }
+}
+
+inline const char* get_header_value(const MultiMap& map, const char* key, const char* def)
+{
+ auto it = map.find(key);
+ if (it != map.end()) {
+ return it->second.c_str();
+ }
+ return def;
+}
+
+inline int get_header_value_int(const MultiMap& map, const char* key, int def)
+{
+ auto it = map.find(key);
+ if (it != map.end()) {
+ return std::stoi(it->second);
+ }
+ return def;
+}
+
+inline bool read_headers(FILE* fp, MultiMap& headers)
+{
+ static std::regex re("(.+?): (.+?)\r\n");
+
+ const auto BUFSIZ_HEADER = 2048;
+ char buf[BUFSIZ_HEADER];
+
+ for (;;) {
+ if (!fgets(buf, BUFSIZ_HEADER, fp)) {
+ return false;
+ }
+ if (!strcmp(buf, "\r\n")) {
+ break;
+ }
+ std::cmatch m;
+ if (std::regex_match(buf, m, re)) {
+ auto key = std::string(m[1]);
+ auto val = std::string(m[2]);
+ headers.insert(std::make_pair(key, val));
+ }
+ }
+
+ return true;
+}
+
+template <typename T>
+bool read_content(T& x, FILE* fp)
+{
+ auto len = get_header_value_int(x.headers, "Content-Length", 0);
+ if (len) {
+ x.body.assign(len, 0);
+ if (!fread(&x.body[0], x.body.size(), 1, fp)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+template <typename T>
+inline void write_headers(FILE* fp, const T& res)
+{
+ fprintf(fp, "Connection: close\r\n");
+
+ for (const auto& x: res.headers) {
+ if (x.first != "Content-Type" && x.first != "Content-Length") {
+ fprintf(fp, "%s: %s\r\n", x.first.c_str(), x.second.c_str());
+ }
+ }
+
+ auto t = get_header_value(res.headers, "Content-Type", "text/plain");
+ fprintf(fp, "Content-Type: %s\r\n", t);
+ fprintf(fp, "Content-Length: %ld\r\n", res.body.size());
+ fprintf(fp, "\r\n");
+}
+
+inline void write_response(FILE* fp, const Request& req, const Response& res)
+{
+ fprintf(fp, "HTTP/1.0 %d %s\r\n", res.status, status_message(res.status));
+
+ write_headers(fp, res);
+
+ if (!res.body.empty() && req.method != "HEAD") {
+ fprintf(fp, "%s", res.body.c_str());
+ }
+}
+
+inline std::string encode_url(const std::string& s)
+{
+ std::string result;
+
+ for (auto i = 0; s[i]; i++) {
+ switch (s[i]) {
+ case ' ': result += "+"; break;
+ case '\'': result += "%27"; break;
+ case ',': result += "%2C"; break;
+ case ':': result += "%3A"; break;
+ case ';': result += "%3B"; break;
+ default:
+ if (s[i] < 0) {
+ result += '%';
+ char hex[4];
+ size_t len = snprintf(hex, sizeof(hex), "%02X", (unsigned char)s[i]);
+ assert(len == 2);
+ result.append(hex, len);
+ } else {
+ result += s[i];
+ }
+ break;
+ }
+ }
+
+ return result;
+}
+
+inline bool is_hex(char c, int& v)
+{
+ if (0x20 <= c && isdigit(c)) {
+ v = c - '0';
+ return true;
+ } else if ('A' <= c && c <= 'F') {
+ v = c - 'A' + 10;
+ return true;
+ } else if ('a' <= c && c <= 'f') {
+ v = c - 'a' + 10;
+ return true;
+ }
+ return false;
+}
+
+inline int from_hex_to_i(const std::string& s, int i, int cnt, int& val)
+{
+ val = 0;
+ for (; s[i] && cnt; i++, cnt--) {
+ int v = 0;
+ if (is_hex(s[i], v)) {
+ val = val * 16 + v;
+ } else {
+ break;
+ }
+ }
+ return --i;
+}
+
+inline size_t to_utf8(int code, char* buff)
+{
+ if (code < 0x0080) {
+ buff[0] = (code & 0x7F);
+ return 1;
+ } else if (code < 0x0800) {
+ buff[0] = (0xC0 | ((code >> 6) & 0x1F));
+ buff[1] = (0x80 | (code & 0x3F));
+ return 2;
+ } else if (code < 0xD800) {
+ buff[0] = (0xE0 | ((code >> 12) & 0xF));
+ buff[1] = (0x80 | ((code >> 6) & 0x3F));
+ buff[2] = (0x80 | (code & 0x3F));
+ return 3;
+ } else if (code < 0xE000) { // D800 - DFFF is invalid...
+ return 0;
+ } else if (code < 0x10000) {
+ buff[0] = (0xE0 | ((code >> 12) & 0xF));
+ buff[1] = (0x80 | ((code >> 6) & 0x3F));
+ buff[2] = (0x80 | (code & 0x3F));
+ return 3;
+ } else if (code < 0x110000) {
+ buff[0] = (0xF0 | ((code >> 18) & 0x7));
+ buff[1] = (0x80 | ((code >> 12) & 0x3F));
+ buff[2] = (0x80 | ((code >> 6) & 0x3F));
+ buff[3] = (0x80 | (code & 0x3F));
+ return 4;
+ }
+
+ // NOTREACHED
+ return 0;
+}
+
+inline std::string decode_url(const std::string& s)
+{
+ std::string result;
+
+ for (int i = 0; s[i]; i++) {
+ if (s[i] == '%') {
+ i++;
+ assert(s[i]);
+
+ if (s[i] == '%') {
+ result += s[i];
+ } else if (s[i] == 'u') {
+ // Unicode
+ i++;
+ assert(s[i]);
+
+ int val = 0;
+ i = from_hex_to_i(s, i, 4, val);
+
+ char buff[4];
+ size_t len = to_utf8(val, buff);
+
+ if (len > 0) {
+ result.append(buff, len);
+ }
+ } else {
+ // HEX
+ int val = 0;
+ i = from_hex_to_i(s, i, 2, val);
+ result += val;
+ }
+ } else if (s[i] == '+') {
+ result += ' ';
+ } else {
+ result += s[i];
+ }
+ }
+
+ return result;
+}
+
+inline void write_request(FILE* fp, const Request& req)
+{
+ auto url = encode_url(req.url);
+ fprintf(fp, "%s %s HTTP/1.0\r\n", req.method.c_str(), url.c_str());
+
+ write_headers(fp, req);
+
+ if (!req.body.empty()) {
+ if (req.has_header("application/x-www-form-urlencoded")) {
+ fprintf(fp, "%s", encode_url(req.body).c_str());
+ } else {
+ fprintf(fp, "%s", req.body.c_str());
+ }
+ }
+}
+
+inline void parse_query_text(const std::string& s, Map& params)
+{
+ split(&s[0], &s[s.size()], '&', [&](const char* b, const char* e) {
+ std::string key;
+ std::string val;
+ split(b, e, '=', [&](const char* b, const char* e) {
+ if (key.empty()) {
+ key.assign(b, e);
+ } else {
+ val.assign(b, e);
+ }
+ });
+ params[key] = detail::decode_url(val);
+ });
+}
+
+#ifdef _MSC_VER
+class WSInit {
+public:
+ WSInit() {
+ WSADATA wsaData;
+ WSAStartup(0x0002, &wsaData);
+ }
+
+ ~WSInit() {
+ WSACleanup();
+ }
+};
+
+static WSInit wsinit_;
+#endif
+
+} // namespace detail
+
+// Request implementation
+inline bool Request::has_header(const char* key) const
+{
+ return headers.find(key) != headers.end();
+}
+
+inline std::string Request::get_header_value(const char* key) const
+{
+ return detail::get_header_value(headers, key, "");
+}
+
+inline void Request::set_header(const char* key, const char* val)
+{
+ headers.insert(std::make_pair(key, val));
+}
+
+inline bool Request::has_param(const char* key) const
+{
+ return params.find(key) != params.end();
+}
+
+// Response implementation
+inline bool Response::has_header(const char* key) const
+{
+ return headers.find(key) != headers.end();
+}
+
+inline std::string Response::get_header_value(const char* key) const
+{
+ return detail::get_header_value(headers, key, "");
+}
+
+inline void Response::set_header(const char* key, const char* val)
+{
+ headers.insert(std::make_pair(key, val));
+}
+
+inline void Response::set_redirect(const char* url)
+{
+ set_header("Location", url);
+ status = 302;
+}
+
+inline void Response::set_content(const char* s, size_t n, const char* content_type)
+{
+ body.assign(s, n);
+ set_header("Content-Type", content_type);
+}
+
+inline void Response::set_content(const std::string& s, const char* content_type)
+{
+ body = s;
+ set_header("Content-Type", content_type);
+}
+
+// HTTP server implementation
+inline Server::Server()
+ : svr_sock_(-1)
+{
+}
+
+inline void Server::get(const char* pattern, Handler handler)
+{
+ get_handlers_.push_back(std::make_pair(std::regex(pattern), handler));
+}
+
+inline void Server::post(const char* pattern, Handler handler)
+{
+ post_handlers_.push_back(std::make_pair(std::regex(pattern), handler));
+}
+
+inline bool Server::set_base_dir(const char* path)
+{
+ if (detail::is_dir(path)) {
+ base_dir_ = path;
+ return true;
+ }
+ return false;
+}
+
+inline void Server::set_error_handler(Handler handler)
+{
+ error_handler_ = handler;
+}
+
+inline void Server::set_logger(Logger logger)
+{
+ logger_ = logger;
+}
+
+inline bool Server::listen(const char* host, int port)
+{
+ svr_sock_ = detail::create_server_socket(host, port);
+ if (svr_sock_ == -1) {
+ return false;
+ }
+
+ auto ret = true;
+
+ for (;;) {
+ socket_t sock = accept(svr_sock_, NULL, NULL);
+
+ if (sock == -1) {
+ if (svr_sock_ != -1) {
+ detail::close_socket(svr_sock_);
+ ret = false;
+ } else {
+ ; // The server socket was closed by user.
+ }
+ break;
+ }
+
+ // TODO: should be async
+ detail::read_and_close_socket(sock, [this](FILE* fp_read, FILE* fp_write) {
+ process_request(fp_read, fp_write);
+ return true;
+ });
+ }
+
+ return ret;
+}
+
+inline void Server::stop()
+{
+ detail::shutdown_socket(svr_sock_);
+ detail::close_socket(svr_sock_);
+ svr_sock_ = -1;
+}
+
+inline bool Server::read_request_line(FILE* fp, Request& req)
+{
+ const auto BUFSIZ_REQUESTLINE = 2048;
+ char buf[BUFSIZ_REQUESTLINE];
+ if (!fgets(buf, BUFSIZ_REQUESTLINE, fp)) {
+ return false;
+ }
+
+ static std::regex re("(GET|HEAD|POST) ([^?]+)(?:\\?(.+?))? HTTP/1\\.[01]\r\n");
+
+ std::cmatch m;
+ if (std::regex_match(buf, m, re)) {
+ req.method = std::string(m[1]);
+ req.url = detail::decode_url(m[2]);
+
+ // Parse query text
+ auto len = std::distance(m[3].first, m[3].second);
+ if (len > 0) {
+ detail::parse_query_text(m[3], req.params);
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+inline bool Server::handle_file_request(Request& req, Response& res)
+{
+ if (!base_dir_.empty()) {
+ std::string path = base_dir_ + req.url;
+
+ if (!path.empty() && path.back() == '/') {
+ path += "index.html";
+ }
+
+ if (detail::is_file(path)) {
+ detail::read_file(path, res.body);
+ res.set_header("Content-Type",
+ detail::get_content_type_from_file_extention(
+ detail::get_file_extention(path)));
+ res.status = 200;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+inline bool Server::routing(Request& req, Response& res)
+{
+ if (req.method == "GET" && handle_file_request(req, res)) {
+ return true;
+ }
+
+ if (req.method == "GET" || req.method == "HEAD") {
+ return dispatch_request(req, res, get_handlers_);
+ } else if (req.method == "POST") {
+ return dispatch_request(req, res, post_handlers_);
+ }
+ return false;
+}
+
+inline bool Server::dispatch_request(Request& req, Response& res, Handlers& handlers)
+{
+ for (const auto& x: handlers) {
+ const auto& pattern = x.first;
+ const auto& handler = x.second;
+
+ if (std::regex_match(req.url, req.matches, pattern)) {
+ handler(req, res);
+ return true;
+ }
+ }
+ return false;
+}
+
+inline void Server::process_request(FILE* fp_read, FILE* fp_write)
+{
+ Request req;
+ Response res;
+
+ if (!read_request_line(fp_read, req) ||
+ !detail::read_headers(fp_read, req.headers)) {
+ return;
+ }
+
+ if (req.method == "POST") {
+ if (!detail::read_content(req, fp_read)) {
+ return;
+ }
+ static std::string type = "application/x-www-form-urlencoded";
+ if (!req.get_header_value("Content-Type").compare(0, type.size(), type)) {
+ detail::parse_query_text(req.body, req.params);
+ }
+ }
+
+ if (routing(req, res)) {
+ if (res.status == -1) {
+ res.status = 200;
+ }
+ } else {
+ res.status = 404;
+ }
+ assert(res.status != -1);
+
+ if (400 <= res.status && error_handler_) {
+ error_handler_(req, res);
+ }
+
+ detail::write_response(fp_write, req, res);
+ fflush(fp_write);
+
+ if (logger_) {
+ logger_(req, res);
+ }
+}
+
+// HTTP client implementation
+inline Client::Client(const char* host, int port)
+ : host_(host)
+ , port_(port)
+{
+}
+
+inline bool Client::read_response_line(FILE* fp, Response& res)
+{
+ const auto BUFSIZ_RESPONSELINE = 2048;
+ char buf[BUFSIZ_RESPONSELINE];
+ if (!fgets(buf, BUFSIZ_RESPONSELINE, fp)) {
+ return false;
+ }
+
+ const static std::regex re("HTTP/1\\.[01] (\\d+?) .+\r\n");
+
+ std::cmatch m;
+ if (std::regex_match(buf, m, re)) {
+ res.status = std::stoi(std::string(m[1]));
+ }
+
+ return true;
+}
+
+inline bool Client::send(const Request& req, Response& res)
+{
+ auto sock = detail::create_client_socket(host_.c_str(), port_);
+ if (sock == -1) {
+ return false;
+ }
+
+ return detail::read_and_close_socket(sock, [&](FILE* fp_read, FILE* fp_write) {
+ // Send request
+ detail::write_request(fp_write, req);
+ fflush(fp_write);
+
+ // Receive response
+ if (!read_response_line(fp_read, res) ||
+ !detail::read_headers(fp_read, res.headers)) {
+ return false;
+ }
+ if (req.method != "HEAD") {
+ if (!detail::read_content(res, fp_read)) {
+ return false;
+ }
+ }
+
+ return true;
+ });
+}
+
+inline std::shared_ptr<Response> Client::get(const char* url)
+{
+ Request req;
+ req.method = "GET";
+ req.url = url;
+
+ auto res = std::make_shared<Response>();
+
+ return send(req, *res) ? res : nullptr;
+}
+
+inline std::shared_ptr<Response> Client::head(const char* url)
+{
+ Request req;
+ req.method = "HEAD";
+ req.url = url;
+
+ auto res = std::make_shared<Response>();
+
+ return send(req, *res) ? res : nullptr;
+}
+
+inline std::shared_ptr<Response> Client::post(
+ const char* url, const std::string& body, const char* content_type)
+{
+ Request req;
+ req.method = "POST";
+ req.url = url;
+ req.set_header("Content-Type", content_type);
+ req.body = body;
+
+ auto res = std::make_shared<Response>();
+
+ return send(req, *res) ? res : nullptr;
+}
+
+inline std::shared_ptr<Response> Client::post(
+ const char* url, const Map& params)
+{
+ std::string query;
+ for (auto it = params.begin(); it != params.end(); ++it) {
+ if (it != params.begin()) {
+ query += "&";
+ }
+ query += it->first;
+ query += "=";
+ query += it->second;
+ }
+
+ return post(url, query, "application/x-www-form-urlencoded");
+}
+
+} // namespace httplib
+
+#endif
+
+// vim: et ts=4 sw=4 cin cino={1s ff=unix
diff --git a/test/src/mbgl/test/server.cpp b/test/src/mbgl/test/server.cpp
new file mode 100644
index 0000000000..f2bebc15c1
--- /dev/null
+++ b/test/src/mbgl/test/server.cpp
@@ -0,0 +1,178 @@
+#include <mbgl/test/server.hpp>
+#include <mbgl/platform/log.hpp>
+#include <iostream>
+#include <string>
+#include <thread>
+#include <future>
+#include <mbgl/util/chrono.hpp>
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wshadow"
+#pragma GCC diagnostic ignored "-Wformat"
+#include "httplib.h"
+#pragma GCC diagnostic pop
+
+#include <memory>
+
+namespace mbgl {
+namespace test {
+
+std::string timestampToISO(const time_t rawtime){
+ struct tm * dt;
+ char buffer [30];
+ dt = localtime(&rawtime);
+ //Tue, 05 Jul 2016 19:52:50 GMT
+ strftime(buffer, sizeof(buffer), "%a, %d %b %Y %T %Z", dt);
+ return std::string(buffer);
+}
+
+Server::Server(bool) {
+ svr = std::make_unique<httplib::Server>();
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+
+ svr->get("/test", [](const auto& req, auto& res) {
+ if(req.has_param("modified")) {
+ res.set_header("Last-Modified", timestampToISO(std::stoi(req.params.at("modified"))).c_str());
+ }
+ if (req.has_param("expires")) {
+ res.set_header("Expires", timestampToISO(std::stoi(req.params.at("expires"))).c_str());
+ }
+ if (req.has_param("etag")) {
+ res.set_header("ETag", req.params.find("etag")->second.c_str());
+ }
+ if (req.has_param("cachecontrol")) {
+ res.set_header("Cache-Control", req.params.find("cachecontrol")->second.c_str());
+ }
+ res.set_content("Hello World!", "text/plain");
+ });
+
+ svr->get("/stale", [] (const auto& req, auto& res) {
+ //Don't respond
+ });
+
+ svr->get("/cache", [](const auto& req, auto& res) {
+ static int cacheCounter;
+ res.set_header("Cache-Control", "max-age=30");
+ res.set_content("Response " + std::to_string(++cacheCounter), "text/plain");
+ });
+
+ svr->get("/revalidate-same", [](const auto& req, auto& res) {
+ if (req.has_header("If-None-Match") && req.headers.find("If-None-Match")->second == "snowfall") {
+ // Second request can be cached for 30 seconds.
+ res.set_header("Cache-Control", "max-age=30");
+ res.status = 304;
+ } else {
+ // First request must always be revalidated.
+ res.set_header("ETag", "snowfall");
+ res.set_header("Cache-Control", "must-revalidate");
+ res.status = 200;
+ res.set_content("Response", "text/plain");
+ }
+ });
+
+ svr->get("/clockskew", [](const auto& req, auto& res) {
+ static int expiresCounter = 0;
+ char dateString[25];
+ sprintf(dateString, "Feb 01 2010 10:0%d:00", ++expiresCounter);
+ res.set_header("Expires", dateString);
+ res.status = 200;
+ res.set_content("Response", "text/plain");
+ });
+
+ svr->get("/revalidate-modified", [](const auto& req, auto& res) {
+ Timestamp jan1 = util::parseTimestamp("Jan 01 2015");
+
+ if (req.has_header("If-Modified-Since")) {
+ Timestamp modified_since = util::parseTimestamp(req.headers.find("If-Modified-Since")->second.c_str());
+ if (modified_since >= jan1) {
+ res.set_header("Cache-Control", "max-age=30");
+ res.status = 304;
+ return;
+ }
+ }
+
+ // First request must always be revalidated.
+ res.set_header("Last-Modified", timestampToISO(std::chrono::system_clock::to_time_t(jan1)).c_str());
+ res.set_header("Cache-Control", "must-revalidate");
+ res.status = 200;
+ res.set_content("Response", "text/plain");
+ });
+
+
+ svr->get("/revalidate-etag", [](const auto& req, auto& res) {
+ static int revalidateEtagCounter = 1;
+ res.set_header("ETag", std::string("response-").append(std::to_string(revalidateEtagCounter)).c_str());
+ res.set_header("Cache-Control", "must-revalidate");
+
+ res.status = 200;
+ res.set_content(std::string("Response ").append(std::to_string(revalidateEtagCounter)), "text/plain");
+ revalidateEtagCounter++;
+ });
+
+
+ svr->get("/empty-data", [](const auto& req, auto& res) {
+ res.status = 200;
+ });
+
+ svr->get("/no-content", [](const auto& req, auto& res) {
+ res.status = 204;
+ });
+
+ svr->get("/not-found", [](const auto& req, auto& res) {
+ res.status = 404;
+ res.set_content("Not Found", "text/plain");
+ });
+
+ svr->get("/permanent-error", [](const auto& req, auto& res) {
+ res.status = 500;
+ res.set_content("Server Error", "text/plain");
+ });
+
+ svr->get("/temporary-error", [](const auto& req, auto& res) {
+ static int temporaryErrorCounter = 0;
+ if (temporaryErrorCounter == 0) {
+ res.status = 500;
+ } else {
+ res.status = 200;
+ res.set_content("Hello World!", "text/plain");
+ }
+
+ temporaryErrorCounter++;
+ });
+
+ svr->get("/delayed", [](const auto& req, auto& res) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(200));
+ res.status = 200;
+ res.set_content("Response", "text/plain");
+ });
+
+ svr->get(R"(/load/(\d+))", [](const auto& req, auto& res) {
+ std::string number = req.matches.str(1);
+ res.set_content(std::string("Request ").append(number), "text/plain");
+ });
+
+#pragma GCC diagnostic pop
+}
+
+Server::~Server() {
+ svr->stop();
+}
+
+void Server::start() {
+ const int port = 3000;
+ std::cout << "Starting server on port: " << port << "\n";
+ f_ = std::async(std::launch::async, [&](){
+ up_ = true;
+ svr->listen("127.0.0.1", port);
+ });
+
+ while (!up_) {
+ usleep(1000);
+ }
+ std::cout << "Server should be up\n";
+}
+
+} // namespace test
+} // namespace mbgl
diff --git a/test/src/mbgl/test/server.hpp b/test/src/mbgl/test/server.hpp
new file mode 100644
index 0000000000..4b36abcee0
--- /dev/null
+++ b/test/src/mbgl/test/server.hpp
@@ -0,0 +1,27 @@
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wshadow"
+#pragma GCC diagnostic ignored "-Wformat"
+#include "httplib.h"
+#pragma GCC diagnostic pop
+
+#include <memory>
+#include <future>
+
+namespace mbgl {
+
+class Map;
+
+namespace test {
+
+class Server {
+public:
+ Server(bool);
+ ~Server();
+ void start();
+private:
+ std::unique_ptr<httplib::Server> svr;
+ std::future<void> f_;
+ bool up_ = false;
+};
+} // namespace test
+} // namespace mbgl
diff --git a/test/src/mbgl/test/test.cpp b/test/src/mbgl/test/test.cpp
index 4c3dad1727..db123ebedf 100644
--- a/test/src/mbgl/test/test.cpp
+++ b/test/src/mbgl/test/test.cpp
@@ -1,17 +1,29 @@
#include <mbgl/test.hpp>
#include <mbgl/test/util.hpp>
-
+#include <mbgl/test/server.hpp>
+#include <mbgl/util/thread.hpp>
#include <gtest/gtest.h>
namespace mbgl {
-int runTests(int argc, char *argv[]) {
+ int runTests(int argc, char *argv[]) {
#if TEST_HAS_SERVER
- auto server = std::make_unique<test::Server>("test/storage/server.js");
+ std::cout << "Starting server\n";
+ util::Thread<test::Server> server({"Webserver"}, true);
+ server.invokeSync(&test::Server::start);
+
+ //Make sure the server is up before continueing
+ httplib::Client cli("127.0.0.1", 3000);
+ int resCode = cli.get("/test")->status;
+ while(resCode !=200) {
+ std::cout << "No response yet\n";
+ usleep(1000*1000);
+ resCode = cli.get("/test")->status;
+ }
#endif
- testing::InitGoogleTest(&argc, argv);
- return RUN_ALL_TESTS();
-}
+ testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+ }
} // namespace mbgl
diff --git a/test/storage/http_file_source.test.cpp b/test/storage/http_file_source.test.cpp
index 5b081d7d57..7f8a02fbb5 100644
--- a/test/storage/http_file_source.test.cpp
+++ b/test/storage/http_file_source.test.cpp
@@ -143,7 +143,7 @@ TEST(HTTPFileSource, TEST_REQUIRES_SERVER(CacheControlParsing)) {
util::RunLoop loop;
HTTPFileSource fs;
- auto req = fs.request({ Resource::Unknown, "http://127.0.0.1:3000/test?cachecontrol=max-age=120" }, [&](Response res) {
+ auto req = fs.request({ Resource::Unknown, "http://127.0.0.1:3000/test?cachecontrol=max-age%3D120" }, [&](Response res) {
EXPECT_EQ(nullptr, res.error);
ASSERT_TRUE(res.data.get());
EXPECT_EQ("Hello World!", *res.data);