diff --git a/.gitignore b/.gitignore index 56041446..d225542e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,6 @@ /third_party/linux/.cipd /third_party/linux/clang /third_party/linux/sysroot -/third_party/go /third_party/gyp/gyp /third_party/mini_chromium/mini_chromium /third_party/zlib/zlib diff --git a/DEPS b/DEPS index ae5cc201..e6719e2a 100644 --- a/DEPS +++ b/DEPS @@ -114,45 +114,6 @@ hooks = [ 'buildtools/win/gn.exe.sha1', ], }, - { - 'name': 'go_toolchain_mac', - 'condition': 'host_os == "mac"', - 'pattern': '.', - 'action': [ - 'cipd', - 'install', - 'infra/go/mac-amd64', - 'latest', - '-root', 'crashpad/third_party/go/mac-amd64', - '-log-level', 'info', - ], - }, - { - 'name': 'go_toolchain_linux', - 'condition': 'host_os == "linux"', - 'pattern': '.', - 'action': [ - 'cipd', - 'install', - 'infra/go/linux-amd64', - 'latest', - '-root', 'crashpad/third_party/go/linux-amd64', - '-log-level', 'info', - ], - }, - { - 'name': 'go_toolchain_win', - 'condition': 'host_os == "win"', - 'pattern': '.', - 'action': [ - 'cipd', - 'install', - 'infra/go/windows-amd64', - 'latest', - '-root', 'crashpad/third_party/go/windows-amd64', - '-log-level', 'info', - ], - }, { # This uses “cipd install” so that mac-amd64 and linux-amd64 can coexist # peacefully. “cipd ensure” would remove the macOS package when running on a diff --git a/third_party/cpp-httplib/BUILD.gn b/third_party/cpp-httplib/BUILD.gn new file mode 100644 index 00000000..be2a6d74 --- /dev/null +++ b/third_party/cpp-httplib/BUILD.gn @@ -0,0 +1,21 @@ +# Copyright 2018 The Crashpad Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +source_set("cpp-httplib") { + testonly = true + include_dirs = [ "cpp-httplib" ] + sources = [ + "cpp-httplib/httplib.h", + ] +} diff --git a/third_party/cpp-httplib/README.crashpad b/third_party/cpp-httplib/README.crashpad new file mode 100644 index 00000000..e9a0bf1c --- /dev/null +++ b/third_party/cpp-httplib/README.crashpad @@ -0,0 +1,15 @@ +Name: cpp-httplib +Short Name: cpp-httplib +URL: https://github.com/yhirose/cpp-httplib +Revision: 4320d7ba3e8b7388e1443eb54c039a1304cf7a6b +License: MIT +License File: cpp-httplib/LICENSE +Security Critical: no (test only) + +Description: +A C++11 header-only HTTP library. + +Local Modifications: +- Exclude test/ and example/ subdirs. +- Patch httplib.h to use #include "third_party/zlib/zlib_crashpad.h" instead of + . diff --git a/third_party/cpp-httplib/cpp-httplib/LICENSE b/third_party/cpp-httplib/cpp-httplib/LICENSE new file mode 100644 index 00000000..3e5ed359 --- /dev/null +++ b/third_party/cpp-httplib/cpp-httplib/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2017 yhirose + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/third_party/cpp-httplib/cpp-httplib/README.md b/third_party/cpp-httplib/cpp-httplib/README.md new file mode 100644 index 00000000..291e4bd7 --- /dev/null +++ b/third_party/cpp-httplib/cpp-httplib/README.md @@ -0,0 +1,188 @@ +cpp-httplib +=========== + +A C++11 header-only HTTP library. + +It's extremely easy to setup. Just include **httplib.h** file in your code! + +Inspired by [Sinatra](http://www.sinatrarb.com/) and [express](https://github.com/visionmedia/express). + +Server Example +-------------- + +```c++ +#include + +int main(void) +{ + using namespace httplib; + + Server svr; + + svr.get("/hi", [](const Request& req, Response& res) { + res.set_content("Hello World!", "text/plain"); + }); + + svr.get(R"(/numbers/(\d+))", [&](const Request& req, Response& res) { + auto numbers = req.matches[1]; + res.set_content(numbers, "text/plain"); + }); + + svr.listen("localhost", 1234); +} +``` + +### Method Chain + +```cpp +svr.get("/get", [](const auto& req, auto& res) { + res.set_content("get", "text/plain"); + }) + .post("/post", [](const auto& req, auto& res) { + res.set_content(req.body(), "text/plain"); + }) + .listen("localhost", 1234); +``` + +### Static File Server + +```cpp +svr.set_base_dir("./www"); +``` + +### Logging + +```cpp +svr.set_logger([](const auto& req, const auto& res) { + your_logger(req, res); +}); +``` + +### Error Handler + +```cpp +svr.set_error_handler([](const auto& req, auto& res) { + const char* fmt = "

Error Status: %d

"; + char buf[BUFSIZ]; + snprintf(buf, sizeof(buf), fmt, res.status); + res.set_content(buf, "text/html"); +}); +``` + +### 'multipart/form-data' POST data + +```cpp +svr.post("/multipart", [&](const auto& req, auto& res) { + auto size = req.files.size(); + auto ret = req.has_file("name1")); + const auto& file = req.get_file_value("name1"); + // file.filename; + // file.content_type; + auto body = req.body.substr(file.offset, file.length)); +}) +``` + +Client Example +-------------- + +### GET + +```c++ +#include +#include + +int main(void) +{ + httplib::Client cli("localhost", 1234); + + auto res = cli.get("/hi"); + if (res && res->status == 200) { + std::cout << res->body << std::endl; + } +} +``` + +### POST + +```c++ +res = cli.post("/post", "text", "text/plain"); +res = cli.post("/person", "name=john1¬e=coder", "application/x-www-form-urlencoded"); +``` + +### POST with parameters + +```c++ +httplib::Map params; +params["name"] = "john"; +params["note"] = "coder"; +auto res = cli.post("/post", params); +``` + +### Connection Timeout + +```c++ +httplib::Client cli("localhost", 8080, 5); // timeouts in 5 seconds +``` +### With Progress Callback + +```cpp +httplib::Client client(url, port); + +// prints: 0 / 000 bytes => 50% complete +std::shared_ptr res = + cli.get("/", [](uint64_t len, uint64_t total) { + printf("%lld / %lld bytes => %d%% complete\n", + len, total, + (int)((len/total)*100)); + } +); +``` + +![progress](https://user-images.githubusercontent.com/236374/33138910-495c4ecc-cf86-11e7-8693-2fc6d09615c4.gif) + +This feature was contributed by [underscorediscovery](https://github.com/yhirose/cpp-httplib/pull/23). + +### Range + +```cpp +httplib::Client cli("httpbin.org", 80); + +// 'Range: bytes=1-10' +httplib::Headers headers = { httplib::make_range_header(1, 10) }; + +auto res = cli.get("/range/32", headers); +// res->status should be 206. +// res->body should be "bcdefghijk". +``` + +OpenSSL Support +--------------- + +SSL support is available with `CPPHTTPLIB_OPENSSL_SUPPORT`. `libssl` and `libcrypto` should be linked. + +```c++ +#define CPPHTTPLIB_OPENSSL_SUPPORT + +SSLServer svr("./cert.pem", "./key.pem"); + +SSLClient cli("localhost", 8080); +``` + +Zlib Support +------------ + +'gzip' compression is available with `CPPHTTPLIB_ZLIB_SUPPORT`. + +The server applies gzip compression to the following MIME type contents: + + * all text types + * image/svg+xml + * application/javascript + * application/json + * application/xml + * application/xhtml+xml + +License +------- + +MIT license (© 2017 Yuji Hirose) diff --git a/third_party/cpp-httplib/cpp-httplib/httplib.h b/third_party/cpp-httplib/cpp-httplib/httplib.h new file mode 100644 index 00000000..693f430a --- /dev/null +++ b/third_party/cpp-httplib/cpp-httplib/httplib.h @@ -0,0 +1,2207 @@ +// +// httplib.h +// +// Copyright (c) 2017 Yuji Hirose. All rights reserved. +// MIT License +// + +#ifndef _CPPHTTPLIB_HTTPLIB_H_ +#define _CPPHTTPLIB_HTTPLIB_H_ + +#ifdef _WIN32 +#ifndef _CRT_SECURE_NO_WARNINGS +#define _CRT_SECURE_NO_WARNINGS +#endif +#ifndef _CRT_NONSTDC_NO_DEPRECATE +#define _CRT_NONSTDC_NO_DEPRECATE +#endif + +#if defined(_MSC_VER) && _MSC_VER < 1900 +#define snprintf _snprintf_s +#endif + +#ifndef S_ISREG +#define S_ISREG(m) (((m)&S_IFREG)==S_IFREG) +#endif +#ifndef S_ISDIR +#define S_ISDIR(m) (((m)&S_IFDIR)==S_IFDIR) +#endif + +#include +#include +#include + +#undef min +#undef max + +#ifndef strcasecmp +#define strcasecmp _stricmp +#endif + +typedef SOCKET socket_t; +#else +#include +#include +#include +#include +#include +#include +#include +#include + +typedef int socket_t; +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#include +#endif + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT +#include "third_party/zlib/zlib_crashpad.h" +#endif + +/* + * Configuration + */ +#define CPPHTTPLIB_KEEPALIVE_MAX_COUNT 5 +#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND 5 +#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_USECOND 0 + +namespace httplib +{ + +namespace detail { + +struct ci { + bool operator() (const std::string & s1, const std::string & s2) const { + return std::lexicographical_compare( + s1.begin(), s1.end(), + s2.begin(), s2.end(), + [](char c1, char c2) { + return ::tolower(c1) < ::tolower(c2); + }); + } +}; + +} // namespace detail + +enum class HttpVersion { v1_0 = 0, v1_1 }; + +typedef std::multimap Headers; + +template +std::pair make_range_header(uint64_t value, Args... args); + +typedef std::multimap Params; +typedef std::smatch Match; +typedef std::function Progress; + +struct MultipartFile { + std::string filename; + std::string content_type; + size_t offset = 0; + size_t length = 0; +}; +typedef std::multimap MultipartFiles; + +struct Request { + std::string version; + std::string method; + std::string path; + Headers headers; + std::string body; + Params params; + MultipartFiles files; + Match matches; + + Progress progress; + + 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; + std::string get_param_value(const char* key) const; + + bool has_file(const char* key) const; + MultipartFile get_file_value(const char* key) const; +}; + +struct Response { + std::string version; + int status; + Headers 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 Stream { +public: + virtual ~Stream() {} + virtual int read(char* ptr, size_t size) = 0; + virtual int write(const char* ptr, size_t size1) = 0; + virtual int write(const char* ptr) = 0; + virtual std::string get_remote_addr() = 0; + + template + void write_format(const char* fmt, const Args& ...args); +}; + +class SocketStream : public Stream { +public: + SocketStream(socket_t sock); + virtual ~SocketStream(); + + virtual int read(char* ptr, size_t size); + virtual int write(const char* ptr, size_t size); + virtual int write(const char* ptr); + virtual std::string get_remote_addr(); + +private: + socket_t sock_; +}; + +class Server { +public: + typedef std::function Handler; + typedef std::function Logger; + + Server(HttpVersion http_version = HttpVersion::v1_0); + + virtual ~Server(); + + virtual bool is_valid() const; + + Server& get(const char* pattern, Handler handler); + Server& post(const char* pattern, Handler handler); + + bool set_base_dir(const char* path); + + void set_error_handler(Handler handler); + void set_logger(Logger logger); + + int bind_to_any_port(const char* host, int socket_flags = 0); + bool listen_after_bind(); + + bool listen(const char* host, int port, int socket_flags = 0); + + bool is_running() const; + void stop(); + +protected: + bool process_request(Stream& strm, bool last_connection); + + const HttpVersion http_version_; + +private: + typedef std::vector> Handlers; + + socket_t create_server_socket(const char* host, int port, int socket_flags) const; + int bind_internal(const char* host, int port, int socket_flags); + bool listen_internal(); + + bool routing(Request& req, Response& res); + bool handle_file_request(Request& req, Response& res); + bool dispatch_request(Request& req, Response& res, Handlers& handlers); + + bool parse_request_line(const char* s, Request& req); + void write_response(Stream& strm, bool last_connection, const Request& req, Response& res); + + virtual bool read_and_close_socket(socket_t sock); + + bool is_running_; + socket_t svr_sock_; + std::string base_dir_; + Handlers get_handlers_; + Handlers post_handlers_; + Handler error_handler_; + Logger logger_; + + // TODO: Use thread pool... + std::mutex running_threads_mutex_; + int running_threads_; +}; + +class Client { +public: + Client( + const char* host, + int port = 80, + size_t timeout_sec = 300, + HttpVersion http_version = HttpVersion::v1_0); + + virtual ~Client(); + + virtual bool is_valid() const; + + std::shared_ptr get(const char* path, Progress progress = nullptr); + std::shared_ptr get(const char* path, const Headers& headers, Progress progress = nullptr); + + std::shared_ptr head(const char* path); + std::shared_ptr head(const char* path, const Headers& headers); + + std::shared_ptr post(const char* path, const std::string& body, const char* content_type); + std::shared_ptr post(const char* path, const Headers& headers, const std::string& body, const char* content_type); + + std::shared_ptr post(const char* path, const Params& params); + std::shared_ptr post(const char* path, const Headers& headers, const Params& params); + + bool send(Request& req, Response& res); + +protected: + bool process_request(Stream& strm, Request& req, Response& res); + + const std::string host_; + const int port_; + size_t timeout_sec_; + const HttpVersion http_version_; + const std::string host_and_port_; + +private: + socket_t create_client_socket() const; + bool read_response_line(Stream& strm, Response& res); + void write_request(Stream& strm, Request& req); + + virtual bool read_and_close_socket(socket_t sock, Request& req, Response& res); +}; + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +class SSLSocketStream : public Stream { +public: + SSLSocketStream(socket_t sock, SSL* ssl); + virtual ~SSLSocketStream(); + + virtual int read(char* ptr, size_t size); + virtual int write(const char* ptr, size_t size); + virtual int write(const char* ptr); + virtual std::string get_remote_addr(); + +private: + socket_t sock_; + SSL* ssl_; +}; + +class SSLServer : public Server { +public: + SSLServer( + const char* cert_path, const char* private_key_path, + HttpVersion http_version = HttpVersion::v1_0); + + virtual ~SSLServer(); + + virtual bool is_valid() const; + +private: + virtual bool read_and_close_socket(socket_t sock); + + SSL_CTX* ctx_; +}; + +class SSLClient : public Client { +public: + SSLClient( + const char* host, + int port = 80, + size_t timeout_sec = 300, + HttpVersion http_version = HttpVersion::v1_0); + + virtual ~SSLClient(); + + virtual bool is_valid() const; + +private: + virtual bool read_and_close_socket(socket_t sock, Request& req, Response& res); + + SSL_CTX* ctx_; +}; +#endif + +/* + * Implementation + */ +namespace detail { + +static std::vector http_version_strings = { "HTTP/1.0", "HTTP/1.1" }; + +template +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]); + } +} + +// NOTE: until the read size reaches `fixed_buffer_size`, use `fixed_buffer` +// to store data. The call can set memory on stack for performance. +class stream_line_reader { +public: + stream_line_reader(Stream& strm, char* fixed_buffer, size_t fixed_buffer_size) + : strm_(strm) + , fixed_buffer_(fixed_buffer) + , fixed_buffer_size_(fixed_buffer_size) { + } + + const char* ptr() const { + if (glowable_buffer_.empty()) { + return fixed_buffer_; + } else { + return glowable_buffer_.data(); + } + } + + bool getline() { + fixed_buffer_used_size_ = 0; + glowable_buffer_.clear(); + + for (size_t i = 0; ; i++) { + char byte; + auto n = strm_.read(&byte, 1); + + if (n < 0) { + return false; + } else if (n == 0) { + if (i == 0) { + return false; + } else { + break; + } + } + + append(byte); + + if (byte == '\n') { + break; + } + } + + return true; + } + +private: + void append(char c) { + if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) { + fixed_buffer_[fixed_buffer_used_size_++] = c; + fixed_buffer_[fixed_buffer_used_size_] = '\0'; + } else { + if (glowable_buffer_.empty()) { + assert(fixed_buffer_[fixed_buffer_used_size_] == '\0'); + glowable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_); + } + glowable_buffer_ += c; + } + } + + Stream& strm_; + char* fixed_buffer_; + const size_t fixed_buffer_size_; + size_t fixed_buffer_used_size_; + std::string glowable_buffer_; +}; + +inline int close_socket(socket_t sock) +{ +#ifdef _WIN32 + return closesocket(sock); +#else + return close(sock); +#endif +} + +inline int select_read(socket_t sock, size_t sec, size_t usec) +{ + fd_set fds; + FD_ZERO(&fds); + FD_SET(sock, &fds); + + timeval tv; + tv.tv_sec = sec; + tv.tv_usec = usec; + + return select(sock + 1, &fds, NULL, NULL, &tv); +} + +inline bool wait_until_socket_is_ready(socket_t sock, size_t sec, size_t usec) +{ + fd_set fdsr; + FD_ZERO(&fdsr); + FD_SET(sock, &fdsr); + + auto fdsw = fdsr; + auto fdse = fdsr; + + timeval tv; + tv.tv_sec = sec; + tv.tv_usec = usec; + + if (select(sock + 1, &fdsr, &fdsw, &fdse, &tv) < 0) { + return false; + } else if (FD_ISSET(sock, &fdsr) || FD_ISSET(sock, &fdsw)) { + int error = 0; + socklen_t len = sizeof(error); + if (getsockopt(sock, SOL_SOCKET, SO_ERROR, (char*)&error, &len) < 0 || error) { + return false; + } + } else { + return false; + } + + return true; +} + +template +inline bool read_and_close_socket(socket_t sock, bool keep_alive, T callback) +{ + bool ret = false; + + if (keep_alive) { + auto count = CPPHTTPLIB_KEEPALIVE_MAX_COUNT; + while (count > 0 && + detail::select_read(sock, + CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND, + CPPHTTPLIB_KEEPALIVE_TIMEOUT_USECOND) > 0) { + auto last_connection = count == 1; + SocketStream strm(sock); + ret = callback(strm, last_connection); + if (!ret) { + break; + } + count--; + } + } else { + SocketStream strm(sock); + ret = callback(strm, true); + } + + close_socket(sock); + return ret; +} + +inline int shutdown_socket(socket_t sock) +{ +#ifdef _WIN32 + return shutdown(sock, SD_BOTH); +#else + return shutdown(sock, SHUT_RDWR); +#endif +} + +template +socket_t create_socket(const char* host, int port, Fn fn, int socket_flags = 0) +{ +#ifdef _WIN32 +#define SO_SYNCHRONOUS_NONALERT 0x20 +#define SO_OPENTYPE 0x7008 + + 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 = socket_flags; + 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 void set_nonblocking(socket_t sock, bool nonblocking) +{ +#ifdef _WIN32 + auto flags = nonblocking ? 1UL : 0UL; + ioctlsocket(sock, FIONBIO, &flags); +#else + auto flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, nonblocking ? (flags | O_NONBLOCK) : (flags & (~O_NONBLOCK))); +#endif +} + +inline bool is_connection_error() +{ +#ifdef _WIN32 + return WSAGetLastError() != WSAEWOULDBLOCK; +#else + return errno != EINPROGRESS; +#endif +} + +inline std::string get_remote_addr(socket_t sock) { + struct sockaddr_storage addr; + char ipstr[INET6_ADDRSTRLEN]; + socklen_t len = sizeof(addr); + getpeername(sock, (struct sockaddr*)&addr, &len); + + // deal with both IPv4 and IPv6: + if (addr.ss_family == AF_INET) { + auto s = (struct sockaddr_in *)&addr; + inet_ntop(AF_INET, &s->sin_addr, ipstr, sizeof(ipstr)); + } else { // AF_INET6 + auto s = (struct sockaddr_in6 *)&addr; + inet_ntop(AF_INET6, &s->sin6_addr, ipstr, sizeof(ipstr)); + } + + return ipstr; +} + +inline bool is_file(const std::string& path) +{ + struct stat st; + return stat(path.c_str(), &st) >= 0 && S_ISREG(st.st_mode); +} + +inline bool is_dir(const std::string& path) +{ + struct stat st; + return stat(path.c_str(), &st) >= 0 && S_ISDIR(st.st_mode); +} + +inline bool is_valid_path(const std::string& path) { + size_t level = 0; + size_t i = 0; + + // Skip slash + while (i < path.size() && path[i] == '/') { + i++; + } + + while (i < path.size()) { + // Read component + auto beg = i; + while (i < path.size() && path[i] != '/') { + i++; + } + + auto len = i - beg; + assert(len > 0); + + if (!path.compare(beg, len, ".")) { + ; + } else if (!path.compare(beg, len, "..")) { + if (level == 0) { + return false; + } + level--; + } else { + level++; + } + + // Skip slash + while (i < path.size() && path[i] == '/') { + i++; + } + } + + return true; +} + +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(static_cast(size)); + fs.read(&out[0], size); +} + +inline std::string file_extension(const std::string& path) +{ + std::smatch m; + auto pat = std::regex("\\.([a-zA-Z0-9]+)$"); + if (std::regex_search(path, m, pat)) { + return m[1].str(); + } + return std::string(); +} + +inline const char* find_content_type(const std::string& path) +{ + auto ext = file_extension(path); + if (ext == "txt") { + return "text/plain"; + } else if (ext == "html") { + return "text/html"; + } else if (ext == "css") { + return "text/css"; + } else if (ext == "jpeg" || ext == "jpg") { + return "image/jpg"; + } else if (ext == "png") { + return "image/png"; + } else if (ext == "gif") { + return "image/gif"; + } else if (ext == "svg") { + return "image/svg+xml"; + } else if (ext == "ico") { + return "image/x-icon"; + } else if (ext == "json") { + return "application/json"; + } else if (ext == "pdf") { + return "application/pdf"; + } else if (ext == "js") { + return "application/javascript"; + } else if (ext == "xml") { + return "application/xml"; + } else if (ext == "xhtml") { + return "application/xhtml+xml"; + } + return nullptr; +} + +inline const char* status_message(int status) +{ + switch (status) { + case 200: return "OK"; + case 400: return "Bad Request"; + case 404: return "Not Found"; + case 415: return "Unsupported Media Type"; + default: + case 500: return "Internal Server Error"; + } +} + +inline const char* get_header_value(const Headers& headers, const char* key, const char* def) +{ + auto it = headers.find(key); + if (it != headers.end()) { + return it->second.c_str(); + } + return def; +} + +inline int get_header_value_int(const Headers& headers, const char* key, int def) +{ + auto it = headers.find(key); + if (it != headers.end()) { + return std::stoi(it->second); + } + return def; +} + +inline bool read_headers(Stream& strm, Headers& headers) +{ + static std::regex re(R"((.+?):\s*(.+?)\s*\r\n)"); + + const auto bufsiz = 2048; + char buf[bufsiz]; + + stream_line_reader reader(strm, buf, bufsiz); + + for (;;) { + if (!reader.getline()) { + return false; + } + if (!strcmp(reader.ptr(), "\r\n")) { + break; + } + std::cmatch m; + if (std::regex_match(reader.ptr(), m, re)) { + auto key = std::string(m[1]); + auto val = std::string(m[2]); + headers.emplace(key, val); + } + } + + return true; +} + +bool read_content_with_length(Stream& strm, std::string& out, size_t len, Progress progress) +{ + out.assign(len, 0); + size_t r = 0; + while (r < len){ + auto n = strm.read(&out[r], len - r); + if (n <= 0) { + return false; + } + + r += n; + + if (progress) { + progress(r, len); + } + } + + return true; +} + +bool read_content_without_length(Stream& strm, std::string& out) +{ + for (;;) { + char byte; + auto n = strm.read(&byte, 1); + if (n < 0) { + return false; + } else if (n == 0) { + return true; + } + out += byte; + } + + return true; +} + +bool read_content_chunked(Stream& strm, std::string& out) +{ + const auto bufsiz = 16; + char buf[bufsiz]; + + stream_line_reader reader(strm, buf, bufsiz); + + if (!reader.getline()) { + return false; + } + + auto chunk_len = std::stoi(reader.ptr(), 0, 16); + + while (chunk_len > 0){ + std::string chunk; + if (!read_content_with_length(strm, chunk, chunk_len, nullptr)) { + return false; + } + + if (!reader.getline()) { + return false; + } + + if (strcmp(reader.ptr(), "\r\n")) { + break; + } + + out += chunk; + + if (!reader.getline()) { + return false; + } + + chunk_len = std::stoi(reader.ptr(), 0, 16); + } + + if (chunk_len == 0) { + // Reader terminator after chunks + if (!reader.getline() || strcmp(reader.ptr(), "\r\n")) + return false; + } + + return true; +} + +template +bool read_content(Stream& strm, T& x, Progress progress = Progress()) +{ + auto len = get_header_value_int(x.headers, "Content-Length", 0); + + if (len) { + return read_content_with_length(strm, x.body, len, progress); + } else { + const auto& encoding = get_header_value(x.headers, "Transfer-Encoding", ""); + + if (!strcasecmp(encoding, "chunked")) { + return read_content_chunked(strm, x.body); + } else { + return read_content_without_length(strm, x.body); + } + } + + return true; +} + +template +inline void write_headers(Stream& strm, const T& info) +{ + for (const auto& x: info.headers) { + strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str()); + } + strm.write("\r\n"); +} + +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) - 1, "%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 bool from_hex_to_i(const std::string& s, int i, int cnt, int& val) +{ + val = 0; + for (; cnt; i++, cnt--) { + if (!s[i]) { + return false; + } + int v = 0; + if (is_hex(s[i], v)) { + val = val * 16 + v; + } else { + return false; + } + } + return true; +} + +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] == '%') { + if (s[i + 1] && s[i + 1] == 'u') { + int val = 0; + if (from_hex_to_i(s, i + 2, 4, val)) { + // 4 digits Unicode codes + char buff[4]; + size_t len = to_utf8(val, buff); + if (len > 0) { + result.append(buff, len); + } + i += 5; // 'u0000' + } else { + result += s[i]; + } + } else { + int val = 0; + if (from_hex_to_i(s, i + 1, 2, val)) { + // 2 digits hex codes + result += val; + i += 2; // '00' + } else { + result += s[i]; + } + } + } else if (s[i] == '+') { + result += ' '; + } else { + result += s[i]; + } + } + + return result; +} + +inline void parse_query_text(const std::string& s, Params& 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.emplace(key, decode_url(val)); + }); +} + +inline bool parse_multipart_boundary(const std::string& content_type, std::string& boundary) +{ + auto pos = content_type.find("boundary="); + if (pos == std::string::npos) { + return false; + } + + boundary = content_type.substr(pos + 9); + return true; +} + +inline bool parse_multipart_formdata( + const std::string& boundary, const std::string& body, MultipartFiles& files) +{ + static std::string dash = "--"; + static std::string crlf = "\r\n"; + + static std::regex re_content_type( + "Content-Type: (.*?)", std::regex_constants::icase); + + static std::regex re_content_disposition( + "Content-Disposition: form-data; name=\"(.*?)\"(?:; filename=\"(.*?)\")?", + std::regex_constants::icase); + + auto dash_boundary = dash + boundary; + + auto pos = body.find(dash_boundary); + if (pos != 0) { + return false; + } + + pos += dash_boundary.size(); + + auto next_pos = body.find(crlf, pos); + if (next_pos == std::string::npos) { + return false; + } + + pos = next_pos + crlf.size(); + + while (pos < body.size()) { + next_pos = body.find(crlf, pos); + if (next_pos == std::string::npos) { + return false; + } + + std::string name; + MultipartFile file; + + auto header = body.substr(pos, (next_pos - pos)); + + while (pos != next_pos) { + std::smatch m; + if (std::regex_match(header, m, re_content_type)) { + file.content_type = m[1]; + } else if (std::regex_match(header, m, re_content_disposition)) { + name = m[1]; + file.filename = m[2]; + } + + pos = next_pos + crlf.size(); + + next_pos = body.find(crlf, pos); + if (next_pos == std::string::npos) { + return false; + } + + header = body.substr(pos, (next_pos - pos)); + } + + pos = next_pos + crlf.size(); + + next_pos = body.find(crlf + dash_boundary, pos); + + if (next_pos == std::string::npos) { + return false; + } + + file.offset = pos; + file.length = next_pos - pos; + + pos = next_pos + crlf.size() + dash_boundary.size(); + + next_pos = body.find(crlf, pos); + if (next_pos == std::string::npos) { + return false; + } + + files.emplace(name, file); + + pos = next_pos + crlf.size(); + } + + return true; +} + +inline std::string to_lower(const char* beg, const char* end) +{ + std::string out; + auto it = beg; + while (it != end) { + out += ::tolower(*it); + it++; + } + return out; +} + +inline void make_range_header_core(std::string&) {} + +template +inline void make_range_header_core(std::string& field, uint64_t value) +{ + if (!field.empty()) { + field += ", "; + } + field += std::to_string(value) + "-"; +} + +template +inline void make_range_header_core(std::string& field, uint64_t value1, uint64_t value2, Args... args) +{ + if (!field.empty()) { + field += ", "; + } + field += std::to_string(value1) + "-" + std::to_string(value2); + make_range_header_core(field, args...); +} + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT +inline bool can_compress(const std::string& content_type) { + return !content_type.find("text/") || + content_type == "image/svg+xml" || + content_type == "application/javascript" || + content_type == "application/json" || + content_type == "application/xml" || + content_type == "application/xhtml+xml"; +} + +inline void compress(const Request& req, Response& res) +{ + // TODO: Server version is HTTP/1.1 and 'Accpet-Encoding' has gzip, not gzip;q=0 + const auto& encodings = req.get_header_value("Accept-Encoding"); + if (encodings.find("gzip") == std::string::npos) { + return; + } + + if (!can_compress(res.get_header_value("Content-Type"))) { + return; + } + + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + + auto ret = deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY); + if (ret != Z_OK) { + return; + } + + strm.avail_in = res.body.size(); + strm.next_in = (Bytef *)res.body.data(); + + std::string compressed; + + const auto bufsiz = 16384; + char buff[bufsiz]; + do { + strm.avail_out = bufsiz; + strm.next_out = (Bytef *)buff; + deflate(&strm, Z_FINISH); + compressed.append(buff, bufsiz - strm.avail_out); + } while (strm.avail_out == 0); + + res.set_header("Content-Encoding", "gzip"); + res.body.swap(compressed); + + deflateEnd(&strm); +} + +inline void decompress_request_body(Request& req) +{ + if (req.get_header_value("Content-Encoding") != "gzip") + return; + + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + + // 15 is the value of wbits, which should be at the maximum possible value to ensure + // that any gzip stream can be decoded. The offset of 16 specifies that the stream + // to decompress will be formatted with a gzip wrapper. + auto ret = inflateInit2(&strm, 16 + 15); + if (ret != Z_OK) { + return; + } + + strm.avail_in = req.body.size(); + strm.next_in = (Bytef *)req.body.data(); + + std::string decompressed; + + const auto bufsiz = 16384; + char buff[bufsiz]; + do { + strm.avail_out = bufsiz; + strm.next_out = (Bytef *)buff; + inflate(&strm, Z_NO_FLUSH); + decompressed.append(buff, bufsiz - strm.avail_out); + } while (strm.avail_out == 0); + + req.body.swap(decompressed); + + inflateEnd(&strm); +} +#endif + +#ifdef _WIN32 +class WSInit { +public: + WSInit() { + WSADATA wsaData; + WSAStartup(0x0002, &wsaData); + } + + ~WSInit() { + WSACleanup(); + } +}; + +static WSInit wsinit_; +#endif + +} // namespace detail + +// Header utilities +template +inline std::pair make_range_header(uint64_t value, Args... args) +{ + std::string field; + detail::make_range_header_core(field, value, args...); + field.insert(0, "bytes="); + return std::make_pair("Range", field); +} + +// 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.emplace(key, val); +} + +inline bool Request::has_param(const char* key) const +{ + return params.find(key) != params.end(); +} + +inline std::string Request::get_param_value(const char* key) const +{ + auto it = params.find(key); + if (it != params.end()) { + return it->second; + } + return std::string(); +} + +inline bool Request::has_file(const char* key) const +{ + return files.find(key) != files.end(); +} + +inline MultipartFile Request::get_file_value(const char* key) const +{ + auto it = files.find(key); + if (it != files.end()) { + return it->second; + } + return MultipartFile(); +} + +// 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.emplace(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); +} + +// Rstream implementation +template +inline void Stream::write_format(const char* fmt, const Args& ...args) +{ + const auto bufsiz = 2048; + char buf[bufsiz]; + +#if defined(_MSC_VER) && _MSC_VER < 1900 + auto n = _snprintf_s(buf, bufsiz, bufsiz - 1, fmt, args...); +#else + auto n = snprintf(buf, bufsiz - 1, fmt, args...); +#endif + if (n > 0) { + if (n >= bufsiz - 1) { + std::vector glowable_buf(bufsiz); + + while (n >= static_cast(glowable_buf.size() - 1)) { + glowable_buf.resize(glowable_buf.size() * 2); +#if defined(_MSC_VER) && _MSC_VER < 1900 + n = _snprintf_s(&glowable_buf[0], glowable_buf.size(), glowable_buf.size() - 1, fmt, args...); +#else + n = snprintf(&glowable_buf[0], glowable_buf.size() - 1, fmt, args...); +#endif + } + write(&glowable_buf[0], n); + } else { + write(buf, n); + } + } +} + +// Socket stream implementation +inline SocketStream::SocketStream(socket_t sock): sock_(sock) +{ +} + +inline SocketStream::~SocketStream() +{ +} + +inline int SocketStream::read(char* ptr, size_t size) +{ + return recv(sock_, ptr, size, 0); +} + +inline int SocketStream::write(const char* ptr, size_t size) +{ + return send(sock_, ptr, size, 0); +} + +inline int SocketStream::write(const char* ptr) +{ + return write(ptr, strlen(ptr)); +} + +inline std::string SocketStream::get_remote_addr() { + return detail::get_remote_addr(sock_); +} + +// HTTP server implementation +inline Server::Server(HttpVersion http_version) + : http_version_(http_version) + , is_running_(false) + , svr_sock_(-1) + , running_threads_(0) +{ +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); +#endif +} + +inline Server::~Server() +{ +} + +inline Server& Server::get(const char* pattern, Handler handler) +{ + get_handlers_.push_back(std::make_pair(std::regex(pattern), handler)); + return *this; +} + +inline Server& Server::post(const char* pattern, Handler handler) +{ + post_handlers_.push_back(std::make_pair(std::regex(pattern), handler)); + return *this; +} + +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 int Server::bind_to_any_port(const char* host, int socket_flags) +{ + return bind_internal(host, 0, socket_flags); +} + +inline bool Server::listen_after_bind() { + return listen_internal(); +} + +inline bool Server::listen(const char* host, int port, int socket_flags) +{ + if (bind_internal(host, port, socket_flags) < 0) + return false; + return listen_internal(); +} + +inline bool Server::is_running() const +{ + return is_running_; +} + +inline void Server::stop() +{ + if (is_running_) { + assert(svr_sock_ != -1); + detail::shutdown_socket(svr_sock_); + detail::close_socket(svr_sock_); + svr_sock_ = -1; + } +} + +inline bool Server::parse_request_line(const char* s, Request& req) +{ + static std::regex re("(GET|HEAD|POST) ([^?]+)(?:\\?(.+?))? (HTTP/1\\.[01])\r\n"); + + std::cmatch m; + if (std::regex_match(s, m, re)) { + req.version = std::string(m[4]); + req.method = std::string(m[1]); + req.path = 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 void Server::write_response(Stream& strm, bool last_connection, const Request& req, Response& res) +{ + assert(res.status != -1); + + if (400 <= res.status && error_handler_) { + error_handler_(req, res); + } + + // Response line + strm.write_format("%s %d %s\r\n", + detail::http_version_strings[static_cast(http_version_)], + res.status, + detail::status_message(res.status)); + + // Headers + if (!res.has_header("Connection") && (last_connection || req.version == "HTTP/1.0")) { + res.set_header("Connection", "close"); + } + + if (!res.body.empty()) { +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + detail::compress(req, res); +#endif + + if (!res.has_header("Content-Type")) { + res.set_header("Content-Type", "text/plain"); + } + + auto length = std::to_string(res.body.size()); + res.set_header("Content-Length", length.c_str()); + } + + detail::write_headers(strm, res); + + // Body + if (!res.body.empty() && req.method != "HEAD") { + strm.write(res.body.c_str(), res.body.size()); + } + + // Log + if (logger_) { + logger_(req, res); + } +} + +inline bool Server::handle_file_request(Request& req, Response& res) +{ + if (!base_dir_.empty() && detail::is_valid_path(req.path)) { + std::string path = base_dir_ + req.path; + + if (!path.empty() && path.back() == '/') { + path += "index.html"; + } + + if (detail::is_file(path)) { + detail::read_file(path, res.body); + auto type = detail::find_content_type(path); + if (type) { + res.set_header("Content-Type", type); + } + res.status = 200; + return true; + } + } + + return false; +} + +inline socket_t Server::create_server_socket(const char* host, int port, int socket_flags) const +{ + return detail::create_socket(host, port, + [](socket_t sock, struct addrinfo& ai) -> bool { + if (::bind(sock, ai.ai_addr, ai.ai_addrlen)) { + return false; + } + if (::listen(sock, 5)) { // Listen through 5 channels + return false; + } + return true; + }, socket_flags); +} + +inline int Server::bind_internal(const char* host, int port, int socket_flags) +{ + if (!is_valid()) { + return -1; + } + + svr_sock_ = create_server_socket(host, port, socket_flags); + if (svr_sock_ == -1) { + return -1; + } + + if (port == 0) { + struct sockaddr_in sin; + socklen_t len = sizeof(sin); + if (getsockname(svr_sock_, reinterpret_cast(&sin), &len) == -1) { + return -1; + } + return ntohs(sin.sin_port); + } else { + return port; + } +} + +inline bool Server::listen_internal() +{ + auto ret = true; + + is_running_ = true; + + for (;;) { + auto val = detail::select_read(svr_sock_, 0, 100000); + + if (val == 0) { // Timeout + if (svr_sock_ == -1) { + // The server socket was closed by 'stop' method. + break; + } + continue; + } + + 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: Use thread pool... + std::thread([=]() { + { + std::lock_guard guard(running_threads_mutex_); + running_threads_++; + } + + read_and_close_socket(sock); + + { + std::lock_guard guard(running_threads_mutex_); + running_threads_--; + } + }).detach(); + } + + // TODO: Use thread pool... + for (;;) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::lock_guard guard(running_threads_mutex_); + if (!running_threads_) { + break; + } + } + + is_running_ = false; + + return ret; +} + +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.path, req.matches, pattern)) { + handler(req, res); + return true; + } + } + return false; +} + +inline bool Server::process_request(Stream& strm, bool last_connection) +{ + const auto bufsiz = 2048; + char buf[bufsiz]; + + detail::stream_line_reader reader(strm, buf, bufsiz); + + // Connection has been closed on client + if (!reader.getline()) { + return false; + } + + Request req; + Response res; + + res.version = detail::http_version_strings[static_cast(http_version_)]; + + // Request line and headers + if (!parse_request_line(reader.ptr(), req) || !detail::read_headers(strm, req.headers)) { + res.status = 400; + write_response(strm, last_connection, req, res); + return true; + } + + auto ret = true; + if (req.get_header_value("Connection") == "close") { + ret = false; + } + + req.set_header("REMOTE_ADDR", strm.get_remote_addr().c_str()); + + // Body + if (req.method == "POST") { + if (!detail::read_content(strm, req)) { + res.status = 400; + write_response(strm, last_connection, req, res); + return ret; + } + + const auto& content_type = req.get_header_value("Content-Type"); + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + detail::decompress_request_body(req); +#else + if (req.get_header_value("Content-Encoding") == "gzip") { + res.status = 415; + write_response(strm, last_connection, req, res); + return ret; + } +#endif + + if (!content_type.find("application/x-www-form-urlencoded")) { + detail::parse_query_text(req.body, req.params); + } else if(!content_type.find("multipart/form-data")) { + std::string boundary; + if (!detail::parse_multipart_boundary(content_type, boundary) || + !detail::parse_multipart_formdata(boundary, req.body, req.files)) { + res.status = 400; + write_response(strm, last_connection, req, res); + return ret; + } + } + } + + if (routing(req, res)) { + if (res.status == -1) { + res.status = 200; + } + } else { + res.status = 404; + } + + write_response(strm, last_connection, req, res); + return ret; +} + +inline bool Server::is_valid() const +{ + return true; +} + +inline bool Server::read_and_close_socket(socket_t sock) +{ + auto keep_alive = http_version_ == HttpVersion::v1_1; + + return detail::read_and_close_socket( + sock, + keep_alive, + [this](Stream& strm, bool last_connection) { + return process_request(strm, last_connection); + }); +} + +// HTTP client implementation +inline Client::Client( + const char* host, int port, size_t timeout_sec, HttpVersion http_version) + : host_(host) + , port_(port) + , timeout_sec_(timeout_sec) + , http_version_(http_version) + , host_and_port_(host_ + ":" + std::to_string(port_)) +{ +} + +inline Client::~Client() +{ +} + +inline bool Client::is_valid() const +{ + return true; +} + +inline socket_t Client::create_client_socket() const +{ + return detail::create_socket(host_.c_str(), port_, + [=](socket_t sock, struct addrinfo& ai) -> bool { + detail::set_nonblocking(sock, true); + + auto ret = connect(sock, ai.ai_addr, ai.ai_addrlen); + if (ret < 0) { + if (detail::is_connection_error() || + !detail::wait_until_socket_is_ready(sock, timeout_sec_, 0)) { + detail::close_socket(sock); + return false; + } + } + + detail::set_nonblocking(sock, false); + return true; + }); +} + +inline bool Client::read_response_line(Stream& strm, Response& res) +{ + const auto bufsiz = 2048; + char buf[bufsiz]; + + detail::stream_line_reader reader(strm, buf, bufsiz); + + if (!reader.getline()) { + return false; + } + + const static std::regex re("HTTP/1\\.[01] (\\d+?) .+\r\n"); + + std::cmatch m; + if (std::regex_match(reader.ptr(), m, re)) { + res.status = std::stoi(std::string(m[1])); + } + + return true; +} + +inline bool Client::send(Request& req, Response& res) +{ + if (req.path.empty()) { + return false; + } + + auto sock = create_client_socket(); + if (sock == -1) { + return false; + } + + return read_and_close_socket(sock, req, res); +} + +inline void Client::write_request(Stream& strm, Request& req) +{ + auto path = detail::encode_url(req.path); + + // Request line + strm.write_format("%s %s %s\r\n", + req.method.c_str(), + path.c_str(), + detail::http_version_strings[static_cast(http_version_)]); + + // Headers + req.set_header("Host", host_and_port_.c_str()); + + if (!req.has_header("Accept")) { + req.set_header("Accept", "*/*"); + } + + if (!req.has_header("User-Agent")) { + req.set_header("User-Agent", "cpp-httplib/0.2"); + } + + // TODO: if (!req.has_header("Connection") && + // (last_connection || http_version_ == detail::HttpVersion::v1_0)) { + if (!req.has_header("Connection")) { + req.set_header("Connection", "close"); + } + + if (!req.body.empty()) { + if (!req.has_header("Content-Type")) { + req.set_header("Content-Type", "text/plain"); + } + + auto length = std::to_string(req.body.size()); + req.set_header("Content-Length", length.c_str()); + } + + detail::write_headers(strm, req); + + // Body + if (!req.body.empty()) { + if (req.get_header_value("Content-Type") == "application/x-www-form-urlencoded") { + auto str = detail::encode_url(req.body); + strm.write(str.c_str(), str.size()); + } else { + strm.write(req.body.c_str(), req.body.size()); + } + } +} + +inline bool Client::process_request(Stream& strm, Request& req, Response& res) +{ + // Send request + write_request(strm, req); + + // Receive response and headers + if (!read_response_line(strm, res) || !detail::read_headers(strm, res.headers)) { + return false; + } + + // TODO: Check if 'Connection' header is 'close' or HTTP version is 1.0, then close socket... + + // Body + if (req.method != "HEAD") { + if (!detail::read_content(strm, res, req.progress)) { + return false; + } + } + + return true; +} + +inline bool Client::read_and_close_socket(socket_t sock, Request& req, Response& res) +{ + return detail::read_and_close_socket(sock, false, [&](Stream& strm, bool /*last_connection*/) { + return process_request(strm, req, res); + }); +} + +inline std::shared_ptr Client::get(const char* path, Progress progress) +{ + return get(path, Headers(), progress); +} + +inline std::shared_ptr Client::get(const char* path, const Headers& headers, Progress progress) +{ + Request req; + req.method = "GET"; + req.path = path; + req.headers = headers; + req.progress = progress; + + auto res = std::make_shared(); + + return send(req, *res) ? res : nullptr; +} + +inline std::shared_ptr Client::head(const char* path) +{ + return head(path, Headers()); +} + +inline std::shared_ptr Client::head(const char* path, const Headers& headers) +{ + Request req; + req.method = "HEAD"; + req.headers = headers; + req.path = path; + + auto res = std::make_shared(); + + return send(req, *res) ? res : nullptr; +} + +inline std::shared_ptr Client::post( + const char* path, const std::string& body, const char* content_type) +{ + return post(path, Headers(), body, content_type); +} + +inline std::shared_ptr Client::post( + const char* path, const Headers& headers, const std::string& body, const char* content_type) +{ + Request req; + req.method = "POST"; + req.headers = headers; + req.path = path; + + req.headers.emplace("Content-Type", content_type); + req.body = body; + + auto res = std::make_shared(); + + return send(req, *res) ? res : nullptr; +} + +inline std::shared_ptr Client::post(const char* path, const Params& params) +{ + return post(path, Headers(), params); +} + +inline std::shared_ptr Client::post(const char* path, const Headers& headers, const Params& 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(path, headers, query, "application/x-www-form-urlencoded"); +} + +/* + * SSL Implementation + */ +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +namespace detail { + +template +inline bool read_and_close_socket_ssl( + socket_t sock, bool keep_alive, + SSL_CTX* ctx, U SSL_connect_or_accept, V setup, + T callback) +{ + auto ssl = SSL_new(ctx); + if (!ssl) { + return false; + } + + auto bio = BIO_new_socket(sock, BIO_NOCLOSE); + SSL_set_bio(ssl, bio, bio); + + setup(ssl); + + SSL_connect_or_accept(ssl); + + bool ret = false; + + if (keep_alive) { + auto count = CPPHTTPLIB_KEEPALIVE_MAX_COUNT; + while (count > 0 && + detail::select_read(sock, + CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND, + CPPHTTPLIB_KEEPALIVE_TIMEOUT_USECOND) > 0) { + auto last_connection = count == 1; + SSLSocketStream strm(sock, ssl); + ret = callback(strm, last_connection); + if (!ret) { + break; + } + count--; + } + } else { + SSLSocketStream strm(sock, ssl); + ret = callback(strm, true); + } + + SSL_shutdown(ssl); + SSL_free(ssl); + close_socket(sock); + return ret; +} + +class SSLInit { +public: + SSLInit() { + SSL_load_error_strings(); + SSL_library_init(); + } +}; + +static SSLInit sslinit_; + +} // namespace detail + +// SSL socket stream implementation +inline SSLSocketStream::SSLSocketStream(socket_t sock, SSL* ssl) + : sock_(sock), ssl_(ssl) +{ +} + +inline SSLSocketStream::~SSLSocketStream() +{ +} + +inline int SSLSocketStream::read(char* ptr, size_t size) +{ + return SSL_read(ssl_, ptr, size); +} + +inline int SSLSocketStream::write(const char* ptr, size_t size) +{ + return SSL_write(ssl_, ptr, size); +} + +inline int SSLSocketStream::write(const char* ptr) +{ + return write(ptr, strlen(ptr)); +} + +inline std::string SSLSocketStream::get_remote_addr() { + return detail::get_remote_addr(sock_); +} + +// SSL HTTP server implementation +inline SSLServer::SSLServer(const char* cert_path, const char* private_key_path, HttpVersion http_version) + : Server(http_version) +{ + ctx_ = SSL_CTX_new(SSLv23_server_method()); + + if (ctx_) { + SSL_CTX_set_options(ctx_, + SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | + SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); + + // auto ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); + // SSL_CTX_set_tmp_ecdh(ctx_, ecdh); + // EC_KEY_free(ecdh); + + if (SSL_CTX_use_certificate_file(ctx_, cert_path, SSL_FILETYPE_PEM) != 1 || + SSL_CTX_use_PrivateKey_file(ctx_, private_key_path, SSL_FILETYPE_PEM) != 1) { + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } + } +} + +inline SSLServer::~SSLServer() +{ + if (ctx_) { + SSL_CTX_free(ctx_); + } +} + +inline bool SSLServer::is_valid() const +{ + return ctx_; +} + +inline bool SSLServer::read_and_close_socket(socket_t sock) +{ + auto keep_alive = http_version_ == HttpVersion::v1_1; + + return detail::read_and_close_socket_ssl( + sock, + keep_alive, + ctx_, + SSL_accept, + [](SSL* /*ssl*/) {}, + [this](Stream& strm, bool last_connection) { + return process_request(strm, last_connection); + }); +} + +// SSL HTTP client implementation +inline SSLClient::SSLClient( + const char* host, int port, size_t timeout_sec, HttpVersion http_version) + : Client(host, port, timeout_sec, http_version) +{ + ctx_ = SSL_CTX_new(SSLv23_client_method()); +} + +inline SSLClient::~SSLClient() +{ + if (ctx_) { + SSL_CTX_free(ctx_); + } +} + +inline bool SSLClient::is_valid() const +{ + return ctx_; +} + +inline bool SSLClient::read_and_close_socket(socket_t sock, Request& req, Response& res) +{ + return is_valid() && detail::read_and_close_socket_ssl( + sock, false, + ctx_, SSL_connect, + [&](SSL* ssl) { + SSL_set_tlsext_host_name(ssl, host_.c_str()); + }, + [&](Stream& strm, bool /*last_connection*/) { + return process_request(strm, req, res); + }); +} +#endif + +} // namespace httplib + +#endif + +// vim: et ts=4 sw=4 cin cino={1s ff=unix diff --git a/util/BUILD.gn b/util/BUILD.gn index bb0fd6fa..5f886f20 100644 --- a/util/BUILD.gn +++ b/util/BUILD.gn @@ -448,6 +448,29 @@ static_library("util") { } } +crashpad_executable("http_transport_test_server") { + testonly = true + sources = [ + "net/http_transport_test_server.cc", + ] + + deps = [ + ":util", + "../third_party/cpp-httplib", + "../third_party/mini_chromium:base", + "../third_party/zlib", + "../tools:tool_support", + ] + + remove_configs = [ + "//third_party/mini_chromium/mini_chromium/build:Wexit_time_destructors", + ] + + if (crashpad_is_win) { + libs = [ "ws2_32.lib" ] + } +} + source_set("util_test") { testonly = true @@ -589,7 +612,6 @@ source_set("util_test") { } data = [ - "net/http_transport_test_server.py", "net/testdata/", ] @@ -604,6 +626,10 @@ source_set("util_test") { "../third_party/zlib", ] + data_deps = [ + ":http_transport_test_server", + ] + if (crashpad_is_mac) { libs = [ "Foundation.framework" ] } @@ -613,7 +639,7 @@ source_set("util_test") { "rpcrt4.lib", "dbghelp.lib", ] - data_deps = [ + data_deps += [ ":crashpad_util_test_process_info_test_child", ":crashpad_util_test_safe_terminate_process_test_child", ] diff --git a/util/net/http_transport_test.cc b/util/net/http_transport_test.cc index fc02ba19..abaa1727 100644 --- a/util/net/http_transport_test.cc +++ b/util/net/http_transport_test.cc @@ -56,19 +56,13 @@ class HTTPTransportTestFixture : public MultiprocessExec { body_stream_(std::move(body_stream)), response_code_(http_response_code), request_validator_(request_validator) { - base::FilePath server_path = TestPaths::TestDataRoot().Append( - FILE_PATH_LITERAL("util/net/http_transport_test_server.py")); -#if defined(OS_POSIX) + base::FilePath server_path = TestPaths::Executable().DirName().Append( + FILE_PATH_LITERAL("http_transport_test_server") +#if defined(OS_WIN) + FILE_PATH_LITERAL(".exe") +#endif + ); SetChildCommand(server_path, nullptr); -#elif defined(OS_WIN) - // Explicitly invoke a shell and python so that python can be found in the - // path, and run the test script. - std::vector args; - args.push_back("/c"); - args.push_back("python"); - args.push_back(base::UTF16ToUTF8(server_path.value())); - SetChildCommand(base::FilePath(_wgetenv(L"COMSPEC")), &args); -#endif // OS_POSIX } const HTTPHeaders& headers() { return headers_; } diff --git a/util/net/http_transport_test_server.cc b/util/net/http_transport_test_server.cc new file mode 100644 index 00000000..2ac5ad60 --- /dev/null +++ b/util/net/http_transport_test_server.cc @@ -0,0 +1,114 @@ +// Copyright 2018 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A one-shot testing webserver. +// +// When invoked, this server will write a short integer to stdout, indicating on +// which port the server is listening. It will then read one integer from stdin, +// indicating the response code to be sent in response to a request. It also +// reads 16 characters from stdin, which, after having "\r\n" appended, will +// form the response body in a successful response (one with code 200). The +// server will process one HTTP request, deliver the prearranged response to the +// client, and write the entire request to stdout. It will then terminate. + +#include "base/logging.h" +#include "base/strings/stringprintf.h" +#include "build/build_config.h" +#include "tools/tool_support.h" +#include "util/file/file_io.h" + +#if COMPILER_MSVC +#pragma warning(push) +#pragma warning(disable: 4244 4245 4267 4702) +#endif + +#define CPPHTTPLIB_ZLIB_SUPPORT +#include "third_party/cpp-httplib/cpp-httplib/httplib.h" + +#if COMPILER_MSVC +#pragma warning(pop) +#endif + +namespace crashpad { +namespace { + +int HttpTransportTestServerMain(int argc, char* argv[]) { + httplib::Server svr(httplib::HttpVersion::v1_0); + + if (!svr.is_valid()) { + LOG(ERROR) << "server creation failed"; + return 1; + } + + uint16_t response_code; + char response[16]; + + std::string to_stdout; + + svr.post("/upload", + [&response, &response_code, &svr, &to_stdout]( + const httplib::Request& req, httplib::Response& res) { + res.status = response_code; + if (response_code == 200) { + res.set_content(std::string(response, 16) + "\r\n", + "text/plain"); + } else { + res.set_content("error", "text/plain"); + } + + for (const auto& h : req.headers) { + to_stdout += base::StringPrintf( + "%s: %s\r\n", h.first.c_str(), h.second.c_str()); + } + to_stdout += "\r\n"; + to_stdout += req.body; + + svr.stop(); + }); + + int port = svr.bind_to_any_port("127.0.0.1"); + + CheckedWriteFile( + StdioFileHandle(StdioStream::kStandardOutput), &port, sizeof(port)); + + CheckedReadFileExactly(StdioFileHandle(StdioStream::kStandardInput), + &response_code, + sizeof(response_code)); + + CheckedReadFileExactly(StdioFileHandle(StdioStream::kStandardInput), + &response, + sizeof(response)); + + svr.listen_after_bind(); + + LoggingWriteFile(StdioFileHandle(StdioStream::kStandardOutput), + to_stdout.data(), + to_stdout.size()); + + return 0; +} + +} // namespace +} // namespace crashpad + +#if defined(OS_POSIX) || defined(OS_FUCHSIA) +int main(int argc, char* argv[]) { + return crashpad::HttpTransportTestServerMain(argc, argv); +} +#elif defined(OS_WIN) +int wmain(int argc, wchar_t* argv[]) { + return crashpad::ToolSupport::Wmain( + argc, argv, crashpad::HttpTransportTestServerMain); +} +#endif // OS_POSIX diff --git a/util/net/http_transport_test_server.py b/util/net/http_transport_test_server.py deleted file mode 100755 index d9777cbc..00000000 --- a/util/net/http_transport_test_server.py +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -# Copyright 2014 The Crashpad Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""A one-shot testing webserver. - -When invoked, this server will write a short integer to stdout, indiciating on -which port the server is listening. It will then read one integer from stdin, -indiciating the response code to be sent in response to a request. It also reads -16 characters from stdin, which, after having "\r\n" appended, will form the -response body in a successful response (one with code 200). The server will -process one HTTP request, deliver the prearranged response to the client, and -write the entire request to stdout. It will then terminate. - -This server is written in Python since it provides a simple HTTP stack, and -because parsing chunked encoding is safer and easier in a memory-safe language. -This could easily have been written in C++ instead. -""" - -import os -import struct -import sys -import zlib - -if sys.platform == 'win32': - import msvcrt - -if sys.version_info[0] < 3: - import BaseHTTPServer as http_server -else: - import http.server as http_server - - -class BufferedReadFile(object): - """A File-like object that stores all read contents into a buffer.""" - - def __init__(self, real_file): - self.file = real_file - self.buffer = b'' - - def read(self, size=-1): - buf = self.file.read(size) - self.buffer += buf - return buf - - def readline(self, size=-1): - buf = self.file.readline(size) - self.buffer += buf - return buf - - def flush(self): - self.file.flush() - - def close(self): - self.file.close() - - -class RequestHandler(http_server.BaseHTTPRequestHandler): - # Everything to be written to stdout is collected into this string. It can’t - # be written to stdout until after the HTTP transaction is complete, because - # stdout is a pipe being read by a test program that’s also the HTTP client. - # The test program expects to complete the entire HTTP transaction before it - # even starts reading this script’s stdout. If the stdout pipe buffer fills up - # during an HTTP transaction, deadlock would result. - raw_request = b'' - - response_code = 500 - response_body = b'' - - def handle_one_request(self): - # Wrap the rfile in the buffering file object so that the raw header block - # can be written to stdout after it is parsed. - self.rfile = BufferedReadFile(self.rfile) - http_server.BaseHTTPRequestHandler.handle_one_request(self) - - def do_POST(self): - RequestHandler.raw_request = self.rfile.buffer - self.rfile.buffer = b'' - - if self.headers.get('Transfer-Encoding', '').lower() == 'chunked': - if 'Content-Length' in self.headers: - raise AssertionError - body = self.handle_chunked_encoding() - else: - length = int(self.headers.get('Content-Length', -1)) - body = self.rfile.read(length) - - if self.headers.get('Content-Encoding', '').lower() == 'gzip': - # 15 is the value of |wbits|, which should be at the maximum possible - # value to ensure that any gzip stream can be decoded. The offset of 16 - # specifies that the stream to decompress will be formatted with a gzip - # wrapper. - body = zlib.decompress(body, 16 + 15) - - RequestHandler.raw_request += body - - self.send_response(self.response_code) - self.end_headers() - if self.response_code == 200: - self.wfile.write(self.response_body) - self.wfile.write(b'\r\n') - - def handle_chunked_encoding(self): - """This parses a "Transfer-Encoding: Chunked" body in accordance with RFC - 7230 §4.1. This returns the result as a string. - """ - body = b'' - chunk_size = self.read_chunk_size() - while chunk_size > 0: - # Read the body. - data = self.rfile.read(chunk_size) - chunk_size -= len(data) - body += data - - # Finished reading this chunk. - if chunk_size == 0: - # Read through any trailer fields. - trailer_line = self.rfile.readline() - while trailer_line.strip() != b'': - trailer_line = self.rfile.readline() - - # Read the chunk size. - chunk_size = self.read_chunk_size() - return body - - def read_chunk_size(self): - # Read the whole line, including the \r\n. - chunk_size_and_ext_line = self.rfile.readline() - # Look for a chunk extension. - chunk_size_end = chunk_size_and_ext_line.find(b';') - if chunk_size_end == -1: - # No chunk extensions; just encounter the end of line. - chunk_size_end = chunk_size_and_ext_line.find(b'\r') - if chunk_size_end == -1: - self.send_response(400) # Bad request. - return -1 - return int(chunk_size_and_ext_line[:chunk_size_end], base=16) - - def log_request(self, code='-', size='-'): - # The default implementation logs these to sys.stderr, which is just noise. - pass - - -def StdioBinaryEquivalent(file): - """Return a file object equivalent to sys.stdin or sys.stdout capable of - reading or writing binary “bytes”. - - struct.unpack consumes “bytes”, and struct.pack produces “bytes”. These are - distinct from “str” in Python 3 (but not 2). In order to read and write these - from stdin and stdout, the underlying binary buffer must be used in place of - the upper-layer text wrapper. This function returns a suitable file. - - There is no underlying buffer in Python 2, but on Windows, the file mode must - still be set to binary in order to cleanly pass binary data. Note that in this - case, the mode of |file| itself is changed, as it’s not distinct from the - returned file. - """ - if hasattr(file, 'buffer'): - file = file.buffer - elif sys.platform == 'win32': - msvcrt.setmode(file.fileno(), os.O_BINARY) - return file - - -def Main(): - in_file = StdioBinaryEquivalent(sys.stdin) - out_file = StdioBinaryEquivalent(sys.stdout) - - # Start the server. - server = http_server.HTTPServer(('127.0.0.1', 0), RequestHandler) - - # Write the port as an unsigned short to the parent process. - out_file.write(struct.pack('=H', server.server_address[1])) - out_file.flush() - - # Read the desired test response code as an unsigned short and the desired - # response body as a 16-byte string from the parent process. - RequestHandler.response_code, RequestHandler.response_body = \ - struct.unpack('=H16s', in_file.read(struct.calcsize('=H16s'))) - - # Handle the request. - server.handle_request() - - # Share the entire request with the test program, which will validate it. - out_file.write(RequestHandler.raw_request) - out_file.flush() - -if __name__ == '__main__': - Main()