From 92830537014b8057389c5731ed9994dad5405488 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 25 Mar 2018 06:19:35 +1100 Subject: [PATCH] Move WebServices to use LibreSSL + cpp-httplib (#3501) Move WebServices to use LibreSSL + cpp-httplib Remove curl + openssl build dependencies --- .gitmodules | 6 +- .travis/linux-frozen/docker.sh | 5 +- .travis/linux/docker.sh | 4 +- .travis/macos/build.sh | 2 +- CMakeLists.txt | 10 - appveyor.yml | 11 +- externals/CMakeLists.txt | 29 +- externals/cpr | 1 - externals/httplib/README.md | 15 + externals/httplib/httplib.h | 2041 ++++++++++++++++++++++++ externals/libressl | 1 + externals/lurlparser/CMakeLists.txt | 8 + externals/lurlparser/LUrlParser.cpp | 265 +++ externals/lurlparser/LUrlParser.h | 78 + externals/lurlparser/README.md | 19 + src/common/announce_multiplayer_room.h | 2 +- src/web_service/CMakeLists.txt | 6 +- src/web_service/web_backend.cpp | 280 ++-- 18 files changed, 2633 insertions(+), 150 deletions(-) delete mode 160000 externals/cpr create mode 100644 externals/httplib/README.md create mode 100644 externals/httplib/httplib.h create mode 160000 externals/libressl create mode 100644 externals/lurlparser/CMakeLists.txt create mode 100644 externals/lurlparser/LUrlParser.cpp create mode 100644 externals/lurlparser/LUrlParser.h create mode 100644 externals/lurlparser/README.md diff --git a/.gitmodules b/.gitmodules index fb48727ba9..bce7d2220a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,9 +25,9 @@ [submodule "enet"] path = externals/enet url = https://github.com/lsalzman/enet.git -[submodule "cpr"] - path = externals/cpr - url = https://github.com/whoshuu/cpr.git [submodule "inih"] path = externals/inih/inih url = https://github.com/benhoyt/inih.git +[submodule "libressl"] + path = externals/libressl + url = https://github.com/citra-emu/ext-libressl-portable.git diff --git a/.travis/linux-frozen/docker.sh b/.travis/linux-frozen/docker.sh index 659277870b..39c2cd2f41 100755 --- a/.travis/linux-frozen/docker.sh +++ b/.travis/linux-frozen/docker.sh @@ -3,7 +3,7 @@ cd /citra apt-get update -apt-get install -y build-essential wget git python-launchpadlib libssl-dev +apt-get install -y build-essential wget git python-launchpadlib # Install specific versions of packages with their dependencies # The apt repositories remove older versions regularly, so we can't use @@ -12,7 +12,6 @@ apt-get install -y build-essential wget git python-launchpadlib libssl-dev libsdl2-dev 2.0.7+dfsg1-3ubuntu1 bionic \ qtbase5-dev 5.9.3+dfsg-0ubuntu2 bionic \ libqt5opengl5-dev 5.9.3+dfsg-0ubuntu2 bionic \ - libcurl4-openssl-dev 7.58.0-2ubuntu1 bionic \ libicu57 57.1-6ubuntu0.2 bionic # Get a recent version of CMake @@ -21,7 +20,7 @@ echo y | sh cmake-3.10.1-Linux-x86_64.sh --prefix=cmake export PATH=/citra/cmake/cmake-3.10.1-Linux-x86_64/bin:$PATH mkdir build && cd build -cmake .. -DUSE_SYSTEM_CURL=ON -DCMAKE_BUILD_TYPE=Release -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} +cmake .. -DCMAKE_BUILD_TYPE=Release -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} make -j4 ctest -VV -C Release diff --git a/.travis/linux/docker.sh b/.travis/linux/docker.sh index 5cbf580678..626510827e 100755 --- a/.travis/linux/docker.sh +++ b/.travis/linux/docker.sh @@ -3,7 +3,7 @@ cd /citra apt-get update -apt-get install -y build-essential libsdl2-dev qtbase5-dev libqt5opengl5-dev qttools5-dev qttools5-dev-tools libcurl4-openssl-dev libssl-dev wget git +apt-get install -y build-essential libsdl2-dev qtbase5-dev libqt5opengl5-dev qttools5-dev qttools5-dev-tools wget git # Get a recent version of CMake wget https://cmake.org/files/v3.10/cmake-3.10.1-Linux-x86_64.sh @@ -11,7 +11,7 @@ echo y | sh cmake-3.10.1-Linux-x86_64.sh --prefix=cmake export PATH=/citra/cmake/cmake-3.10.1-Linux-x86_64/bin:$PATH mkdir build && cd build -cmake .. -DUSE_SYSTEM_CURL=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} +cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} make -j4 ctest -VV -C Release diff --git a/.travis/macos/build.sh b/.travis/macos/build.sh index 83513d9229..d763b20f42 100755 --- a/.travis/macos/build.sh +++ b/.travis/macos/build.sh @@ -6,7 +6,7 @@ export MACOSX_DEPLOYMENT_TARGET=10.12 export Qt5_DIR=$(brew --prefix)/opt/qt5 mkdir build && cd build -cmake .. -DUSE_SYSTEM_CURL=ON -DCMAKE_OSX_ARCHITECTURES="x86_64;x86_64h" -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} +cmake .. -DCMAKE_OSX_ARCHITECTURES="x86_64;x86_64h" -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} make -j4 ctest -VV -C Release diff --git a/CMakeLists.txt b/CMakeLists.txt index 686405118f..f88e078a88 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,16 +17,6 @@ option(ENABLE_QT_TRANSLATION "Enable translations for the Qt frontend" OFF) CMAKE_DEPENDENT_OPTION(CITRA_USE_BUNDLED_QT "Download bundled Qt binaries" ON "ENABLE_QT;MSVC" OFF) option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON) -option(CITRA_USE_BUNDLED_CURL "FOR MINGW ONLY: Download curl configured against winssl instead of openssl" OFF) - -if (ENABLE_WEB_SERVICE AND CITRA_USE_BUNDLED_CURL AND WINDOWS AND MSVC) - message("Turning off use bundled curl as msvc can compile curl on cpr") - SET(CITRA_USE_BUNDLED_CURL OFF CACHE BOOL "" FORCE) -endif() -if (ENABLE_WEB_SERVICE AND NOT CITRA_USE_BUNDLED_CURL AND MINGW) - message(AUTHOR_WARNING "Turning on CITRA_USE_BUNDLED_CURL. Override it only if you know what you are doing.") - SET(CITRA_USE_BUNDLED_CURL ON CACHE BOOL "" FORCE) -endif() if(NOT EXISTS ${CMAKE_SOURCE_DIR}/.git/hooks/pre-commit) message(STATUS "Copying pre-commit hook") diff --git a/appveyor.yml b/appveyor.yml index df98040d90..c2d1bf92ad 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -26,8 +26,7 @@ install: - git submodule update --init --recursive - ps: | if ($env:BUILD_TYPE -eq 'mingw') { - $dependencies = "mingw64/mingw-w64-x86_64-qt5", - "mingw64/mingw-w64-x86_64-curl" + $dependencies = "mingw64/mingw-w64-x86_64-qt5" # redirect err to null to prevent warnings from becoming errors # workaround to prevent pacman from failing due to cyclical dependencies C:\msys64\usr\bin\bash -lc "pacman --noconfirm -S mingw64/mingw-w64-x86_64-freetype mingw64/mingw-w64-x86_64-fontconfig" 2> $null @@ -44,9 +43,9 @@ before_build: $COMPAT = if ($env:ENABLE_COMPATIBILITY_REPORTING -eq $null) {0} else {$env:ENABLE_COMPATIBILITY_REPORTING} if ($env:BUILD_TYPE -eq 'msvc') { # redirect stderr and change the exit code to prevent powershell from cancelling the build if cmake prints a warning - cmd /C 'cmake -G "Visual Studio 15 2017 Win64" -DCITRA_USE_BUNDLED_QT=1 -DCITRA_USE_BUNDLED_SDL2=1 -DCMAKE_USE_OPENSSL=0 -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} .. 2>&1 && exit 0' + cmd /C 'cmake -G "Visual Studio 15 2017 Win64" -DCITRA_USE_BUNDLED_QT=1 -DCITRA_USE_BUNDLED_SDL2=1 -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} .. 2>&1 && exit 0' } else { - C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DUSE_SYSTEM_CURL=1 -DCITRA_USE_BUNDLED_CURL=1 -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} .. 2>&1" + C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} .. 2>&1" } - cd .. @@ -114,14 +113,12 @@ after_build: # copy the compiled binaries and other release files to the release folder Get-ChildItem "$CMAKE_BINARY_DIR" -Recurse -Filter "citra*.exe" | Copy-Item -destination $RELEASE_DIST - # copy the libcurl dll - Get-ChildItem "$CMAKE_BINARY_DIR" -Recurse -Filter "libcurl.dll" | Copy-Item -destination $RELEASE_DIST Copy-Item -path "$CMAKE_SOURCE_DIR/license.txt" -destination $RELEASE_DIST Copy-Item -path "$CMAKE_SOURCE_DIR/README.md" -destination $RELEASE_DIST # copy all the dll dependencies to the release folder . "./.appveyor/UtilityFunctions.ps1" - $DLLSearchPath = "$CMAKE_BINARY_DIR\externals\curl-7_55_1\lib;C:\msys64\mingw64\bin;$env:PATH" + $DLLSearchPath = "C:\msys64\mingw64\bin;$env:PATH" $MingwDLLs = RecursivelyGetDeps $DLLSearchPath "$RELEASE_DIST\citra.exe" $MingwDLLs += RecursivelyGetDeps $DLLSearchPath "$RELEASE_DIST\citra-qt.exe" Write-Host "Detected the following dependencies:" diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index b6909ce15e..e0f42cb081 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -60,23 +60,18 @@ add_subdirectory(enet) target_include_directories(enet INTERFACE ./enet/include) if (ENABLE_WEB_SERVICE) - # msys installed curl is configured to use openssl, but that isn't portable - # since it relies on having the bundled certs install in the home folder for SSL - # by default on mingw, download the precompiled curl thats linked against windows native ssl - if (MINGW AND CITRA_USE_BUNDLED_CURL) - download_bundled_external("curl/" "curl-7_55_1" CURL_PREFIX) - set(CURL_PREFIX "${CMAKE_BINARY_DIR}/externals/curl-7_55_1") - set(CURL_FOUND YES) - set(CURL_INCLUDE_DIR "${CURL_PREFIX}/include" CACHE PATH "Path to curl headers") - set(CURL_LIBRARY "${CURL_PREFIX}/lib/libcurldll.a" CACHE PATH "Path to curl library") - set(CURL_DLL_DIR "${CURL_PREFIX}/lib/" CACHE PATH "Path to curl.dll") - set(USE_SYSTEM_CURL ON CACHE BOOL "") - endif() - # CPR - set(BUILD_TESTING OFF CACHE BOOL "") - set(BUILD_CPR_TESTS OFF CACHE BOOL "") - add_subdirectory(cpr) - target_include_directories(cpr INTERFACE ./cpr/include) + # LibreSSL + set(LIBRESSL_SKIP_INSTALL ON CACHE BOOL "") + add_definitions(-DHAVE_INET_NTOP) + add_subdirectory(libressl) + target_include_directories(ssl INTERFACE ./libressl/include) + + # lurlparser + add_subdirectory(lurlparser) + + # httplib + add_library(httplib INTERFACE) + target_include_directories(httplib INTERFACE ./httplib) # JSON add_library(json-headers INTERFACE) diff --git a/externals/cpr b/externals/cpr deleted file mode 160000 index b5758fbc88..0000000000 --- a/externals/cpr +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b5758fbc88021437f968fe5174f121b8b92f5d5c diff --git a/externals/httplib/README.md b/externals/httplib/README.md new file mode 100644 index 0000000000..f6ef20acb0 --- /dev/null +++ b/externals/httplib/README.md @@ -0,0 +1,15 @@ +From https://github.com/yhirose/cpp-httplib/commit/25aa0b34c3c43ad51fc60c09e2e420c4ebda75cd + +MIT License + +=== + +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 and express. + +© 2017 Yuji Hirose diff --git a/externals/httplib/httplib.h b/externals/httplib/httplib.h new file mode 100644 index 0000000000..85a8ffedcf --- /dev/null +++ b/externals/httplib/httplib.h @@ -0,0 +1,2041 @@ +// +// 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 + +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 + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#include +#endif + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT +#include +#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; + + 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); + +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); + + 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; + + 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); + + 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 = 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(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); + +private: + 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 is_socket_writable(socket_t sock, size_t sec, size_t usec) +{ + fd_set fdsw; + FD_ZERO(&fdsw); + FD_SET(sock, &fdsw); + + fd_set fdse; + FD_ZERO(&fdse); + FD_SET(sock, &fdse); + + timeval tv; + tv.tv_sec = sec; + tv.tv_usec = usec; + + if (select(sock + 1, NULL, &fdsw, &fdse, &tv) <= 0) { + return false; + } + + return FD_ISSET(sock, &fdsw) != 0; +} + +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 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"; + 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; +} + +template +bool read_content_with_length(Stream& strm, T& x, size_t len, Progress progress) +{ + x.body.assign(len, 0); + size_t r = 0; + while (r < len){ + auto n = strm.read(&x.body[r], len - r); + if (n <= 0) { + return false; + } + + r += n; + + if (progress) { + progress(r, len); + } + } + + return true; +} + +template +bool read_content_without_length(Stream& strm, T& x) +{ + for (;;) { + char byte; + auto n = strm.read(&byte, 1); + if (n < 0) { + return false; + } else if (n == 0) { + return true; + } + x.body += byte; + } + + return true; +} + +template +bool read_content_chunked(Stream& strm, T& x) +{ + 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(chunk_len, 0); + + auto n = strm.read(&chunk[0], chunk_len); + if (n <= 0) { + return false; + } + + if (!reader.getline()) { + return false; + } + + if (strcmp(reader.ptr(), "\r\n")) { + break; + } + + x.body += chunk; + + if (!reader.getline()) { + return false; + } + + chunk_len = std::stoi(reader.ptr(), 0, 16); + } + + 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, len, progress); + } else { + const auto& encoding = get_header_value(x.headers, "Transfer-Encoding", ""); + + if (!strcmp(encoding, "chunked")) { + return read_content_chunked(strm, x); + } else { + return read_content_without_length(strm, x); + } + } + + 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: (.*?)"); + + static std::regex re_content_disposition( + "Content-Disposition: form-data; name=\"(.*?)\"(?:; filename=\"(.*?)\")?"); + + 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); +} +#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)); +} + +// HTTP server implementation +inline Server::Server(HttpVersion http_version) + : http_version_(http_version) + , svr_sock_(-1) +{ +#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 bool Server::listen(const char* host, int port, int socket_flags) +{ + if (!is_valid()) { + return false; + } + + svr_sock_ = create_server_socket(host, port, socket_flags); + if (svr_sock_ == -1) { + return false; + } + + auto ret = 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([=]() { + read_and_close_socket(sock); + }).detach(); + } + + return ret; +} + +inline bool Server::is_running() const +{ + return svr_sock_ != -1; +} + +inline void Server::stop() +{ + 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 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; + } + + // 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"); + + 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 == -1 && detail::is_connection_error()) { + return false; + } + + detail::set_nonblocking(sock, false); + + return detail::is_socket_writable(sock, timeout_sec_, 0); + }); +} + +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(ssl); + ret = callback(strm, last_connection); + if (!ret) { + break; + } + count--; + } + } else { + SSLSocketStream strm(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(SSL* ssl): 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)); +} + +// 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/externals/libressl b/externals/libressl new file mode 160000 index 0000000000..49f073a705 --- /dev/null +++ b/externals/libressl @@ -0,0 +1 @@ +Subproject commit 49f073a705ac0d0f5593fb2e5a6f081a08106e20 diff --git a/externals/lurlparser/CMakeLists.txt b/externals/lurlparser/CMakeLists.txt new file mode 100644 index 0000000000..45046ffd36 --- /dev/null +++ b/externals/lurlparser/CMakeLists.txt @@ -0,0 +1,8 @@ +add_library(lurlparser + LUrlParser.cpp + LUrlParser.h +) + +create_target_directory_groups(lurlparser) + +target_include_directories(lurlparser INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/externals/lurlparser/LUrlParser.cpp b/externals/lurlparser/LUrlParser.cpp new file mode 100644 index 0000000000..9c134e3303 --- /dev/null +++ b/externals/lurlparser/LUrlParser.cpp @@ -0,0 +1,265 @@ +/* + * Lightweight URL & URI parser (RFC 1738, RFC 3986) + * https://github.com/corporateshark/LUrlParser + * + * The MIT License (MIT) + * + * Copyright (C) 2015 Sergey Kosarevsky (sk@linderdaum.com) + * + * 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. + */ + +#include "LUrlParser.h" + +#include +#include +#include + +// check if the scheme name is valid +static bool IsSchemeValid( const std::string& SchemeName ) +{ + for ( auto c : SchemeName ) + { + if ( !isalpha( c ) && c != '+' && c != '-' && c != '.' ) return false; + } + + return true; +} + +bool LUrlParser::clParseURL::GetPort( int* OutPort ) const +{ + if ( !IsValid() ) { return false; } + + int Port = atoi( m_Port.c_str() ); + + if ( Port <= 0 || Port > 65535 ) { return false; } + + if ( OutPort ) { *OutPort = Port; } + + return true; +} + +// based on RFC 1738 and RFC 3986 +LUrlParser::clParseURL LUrlParser::clParseURL::ParseURL( const std::string& URL ) +{ + LUrlParser::clParseURL Result; + + const char* CurrentString = URL.c_str(); + + /* + * : + * := [a-z\+\-\.]+ + * For resiliency, programs interpreting URLs should treat upper case letters as equivalent to lower case in scheme names + */ + + // try to read scheme + { + const char* LocalString = strchr( CurrentString, ':' ); + + if ( !LocalString ) + { + return clParseURL( LUrlParserError_NoUrlCharacter ); + } + + // save the scheme name + Result.m_Scheme = std::string( CurrentString, LocalString - CurrentString ); + + if ( !IsSchemeValid( Result.m_Scheme ) ) + { + return clParseURL( LUrlParserError_InvalidSchemeName ); + } + + // scheme should be lowercase + std::transform( Result.m_Scheme.begin(), Result.m_Scheme.end(), Result.m_Scheme.begin(), ::tolower ); + + // skip ':' + CurrentString = LocalString+1; + } + + /* + * //:@:/ + * any ":", "@" and "/" must be normalized + */ + + // skip "//" + if ( *CurrentString++ != '/' ) return clParseURL( LUrlParserError_NoDoubleSlash ); + if ( *CurrentString++ != '/' ) return clParseURL( LUrlParserError_NoDoubleSlash ); + + // check if the user name and password are specified + bool bHasUserName = false; + + const char* LocalString = CurrentString; + + while ( *LocalString ) + { + if ( *LocalString == '@' ) + { + // user name and password are specified + bHasUserName = true; + break; + } + else if ( *LocalString == '/' ) + { + // end of : specification + bHasUserName = false; + break; + } + + LocalString++; + } + + // user name and password + LocalString = CurrentString; + + if ( bHasUserName ) + { + // read user name + while ( *LocalString && *LocalString != ':' && *LocalString != '@' ) LocalString++; + + Result.m_UserName = std::string( CurrentString, LocalString - CurrentString ); + + // proceed with the current pointer + CurrentString = LocalString; + + if ( *CurrentString == ':' ) + { + // skip ':' + CurrentString++; + + // read password + LocalString = CurrentString; + + while ( *LocalString && *LocalString != '@' ) LocalString++; + + Result.m_Password = std::string( CurrentString, LocalString - CurrentString ); + + CurrentString = LocalString; + } + + // skip '@' + if ( *CurrentString != '@' ) + { + return clParseURL( LUrlParserError_NoAtSign ); + } + + CurrentString++; + } + + bool bHasBracket = ( *CurrentString == '[' ); + + // go ahead, read the host name + LocalString = CurrentString; + + while ( *LocalString ) + { + if ( bHasBracket && *LocalString == ']' ) + { + // end of IPv6 address + LocalString++; + break; + } + else if ( !bHasBracket && ( *LocalString == ':' || *LocalString == '/' ) ) + { + // port number is specified + break; + } + + LocalString++; + } + + Result.m_Host = std::string( CurrentString, LocalString - CurrentString ); + + CurrentString = LocalString; + + // is port number specified? + if ( *CurrentString == ':' ) + { + CurrentString++; + + // read port number + LocalString = CurrentString; + + while ( *LocalString && *LocalString != '/' ) LocalString++; + + Result.m_Port = std::string( CurrentString, LocalString - CurrentString ); + + CurrentString = LocalString; + } + + // end of string + if ( !*CurrentString ) + { + Result.m_ErrorCode = LUrlParserError_Ok; + + return Result; + } + + // skip '/' + if ( *CurrentString != '/' ) + { + return clParseURL( LUrlParserError_NoSlash ); + } + + CurrentString++; + + // parse the path + LocalString = CurrentString; + + while ( *LocalString && *LocalString != '#' && *LocalString != '?' ) LocalString++; + + Result.m_Path = std::string( CurrentString, LocalString - CurrentString ); + + CurrentString = LocalString; + + // check for query + if ( *CurrentString == '?' ) + { + // skip '?' + CurrentString++; + + // read query + LocalString = CurrentString; + + while ( *LocalString && *LocalString != '#' ) LocalString++; + + Result.m_Query = std::string( CurrentString, LocalString - CurrentString ); + + CurrentString = LocalString; + } + + // check for fragment + if ( *CurrentString == '#' ) + { + // skip '#' + CurrentString++; + + // read fragment + LocalString = CurrentString; + + while ( *LocalString ) LocalString++; + + Result.m_Fragment = std::string( CurrentString, LocalString - CurrentString ); + + CurrentString = LocalString; + } + + Result.m_ErrorCode = LUrlParserError_Ok; + + return Result; +} diff --git a/externals/lurlparser/LUrlParser.h b/externals/lurlparser/LUrlParser.h new file mode 100644 index 0000000000..25d2109811 --- /dev/null +++ b/externals/lurlparser/LUrlParser.h @@ -0,0 +1,78 @@ +/* + * Lightweight URL & URI parser (RFC 1738, RFC 3986) + * https://github.com/corporateshark/LUrlParser + * + * The MIT License (MIT) + * + * Copyright (C) 2015 Sergey Kosarevsky (sk@linderdaum.com) + * + * 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. + */ + +#pragma once + +#include + +namespace LUrlParser +{ +enum LUrlParserError +{ + LUrlParserError_Ok = 0, + LUrlParserError_Uninitialized = 1, + LUrlParserError_NoUrlCharacter = 2, + LUrlParserError_InvalidSchemeName = 3, + LUrlParserError_NoDoubleSlash = 4, + LUrlParserError_NoAtSign = 5, + LUrlParserError_UnexpectedEndOfLine = 6, + LUrlParserError_NoSlash = 7, +}; + +class clParseURL +{ +public: + LUrlParserError m_ErrorCode; + std::string m_Scheme; + std::string m_Host; + std::string m_Port; + std::string m_Path; + std::string m_Query; + std::string m_Fragment; + std::string m_UserName; + std::string m_Password; + + clParseURL() + : m_ErrorCode( LUrlParserError_Uninitialized ) + {} + + /// return 'true' if the parsing was successful + bool IsValid() const { return m_ErrorCode == LUrlParserError_Ok; } + + /// helper to convert the port number to int, return 'true' if the port is valid (within the 0..65535 range) + bool GetPort( int* OutPort ) const; + + /// parse the URL + static clParseURL ParseURL( const std::string& URL ); + +private: + explicit clParseURL( LUrlParserError ErrorCode ) + : m_ErrorCode( ErrorCode ) + {} +}; + +} // namespace LUrlParser diff --git a/externals/lurlparser/README.md b/externals/lurlparser/README.md new file mode 100644 index 0000000000..be7f0135a8 --- /dev/null +++ b/externals/lurlparser/README.md @@ -0,0 +1,19 @@ +From https://github.com/corporateshark/LUrlParser/commit/455d5e2d27e3946f11ad0328fee9ee2628e6a8e2 + +MIT License + +=== + +Lightweight URL & URI parser (RFC 1738, RFC 3986) + +(C) Sergey Kosarevsky, 2015 + +@corporateshark sk@linderdaum.com + +http://www.linderdaum.com + +http://blog.linderdaum.com + +============================= + +A tiny and lightweight URL & URI parser (RFC 1738, RFC 3986) written in C++. diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h index 60d7933f7b..00f115ca16 100644 --- a/src/common/announce_multiplayer_room.h +++ b/src/common/announce_multiplayer_room.h @@ -17,7 +17,7 @@ struct WebResult { Success, InvalidURL, CredentialsMissing, - CprError, + LibError, HttpError, WrongContent, NoWebservice, diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt index 911fbe26bb..fc311cb817 100644 --- a/src/web_service/CMakeLists.txt +++ b/src/web_service/CMakeLists.txt @@ -11,4 +11,8 @@ add_library(web_service STATIC create_target_directory_groups(web_service) -target_link_libraries(web_service PUBLIC common cpr json-headers) +get_directory_property(OPENSSL_LIBS + DIRECTORY ${CMAKE_SOURCE_DIR}/externals/libressl + DEFINITION OPENSSL_LIBS) +add_definitions(-DCPPHTTPLIB_OPENSSL_SUPPORT) +target_link_libraries(web_service PUBLIC common json-headers ${OPENSSL_LIBS} httplib lurlparser) diff --git a/src/web_service/web_backend.cpp b/src/web_service/web_backend.cpp index 64f6a69bd0..ce92dae481 100644 --- a/src/web_service/web_backend.cpp +++ b/src/web_service/web_backend.cpp @@ -2,13 +2,11 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#ifdef _WIN32 -#include -#endif - #include +#include #include -#include +#include +#include #include "common/announce_multiplayer_room.h" #include "common/logging/log.h" #include "web_service/web_backend.h" @@ -17,25 +15,45 @@ namespace WebService { static constexpr char API_VERSION[]{"1"}; -static std::unique_ptr g_session; +constexpr int HTTP_PORT = 80; +constexpr int HTTPS_PORT = 443; -void Win32WSAStartup() { -#ifdef _WIN32 - // On Windows, CPR/libcurl does not properly initialize Winsock. The below code is used to - // initialize Winsock globally, which fixes this problem. Without this, only the first CPR - // session will properly be created, and subsequent ones will fail. - WSADATA wsa_data; - const int wsa_result{WSAStartup(MAKEWORD(2, 2), &wsa_data)}; - if (wsa_result) { - LOG_CRITICAL(WebService, "WSAStartup failed: %d", wsa_result); +constexpr int TIMEOUT_SECONDS = 30; + +std::unique_ptr GetClientFor(const LUrlParser::clParseURL& parsedUrl) { + namespace hl = httplib; + + int port; + + std::unique_ptr cli; + + if (parsedUrl.m_Scheme == "http") { + if (!parsedUrl.GetPort(&port)) { + port = HTTP_PORT; + } + return std::make_unique(parsedUrl.m_Host.c_str(), port, TIMEOUT_SECONDS, + hl::HttpVersion::v1_1); + } else if (parsedUrl.m_Scheme == "https") { + if (!parsedUrl.GetPort(&port)) { + port = HTTPS_PORT; + } + return std::make_unique(parsedUrl.m_Host.c_str(), port, TIMEOUT_SECONDS, + hl::HttpVersion::v1_1); + } else { + LOG_ERROR(WebService, "Bad URL scheme %s", parsedUrl.m_Scheme.c_str()); + return nullptr; } -#endif } std::future PostJson(const std::string& url, const std::string& data, bool allow_anonymous, const std::string& username, const std::string& token) { - if (url.empty()) { + using lup = LUrlParser::clParseURL; + namespace hl = httplib; + + lup parsedUrl = lup::ParseURL(url); + + if (url.empty() || !parsedUrl.IsValid()) { LOG_ERROR(WebService, "URL is invalid"); return std::async(std::launch::deferred, []() { return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"}; @@ -51,51 +69,71 @@ std::future PostJson(const std::string& url, const std::strin }); } - Win32WSAStartup(); - // Built request header - cpr::Header header; + hl::Headers params; if (are_credentials_provided) { // Authenticated request if credentials are provided - header = {{"Content-Type", "application/json"}, - {"x-username", username.c_str()}, - {"x-token", token.c_str()}, - {"api-version", API_VERSION}}; + params = {{std::string("x-username"), username}, + {std::string("x-token"), token}, + {std::string("api-version"), std::string(API_VERSION)}, + {std::string("Content-Type"), std::string("application/json")}}; } else { // Otherwise, anonymous request - header = cpr::Header{{"Content-Type", "application/json"}, {"api-version", API_VERSION}}; + params = {{std::string("api-version"), std::string(API_VERSION)}, + {std::string("Content-Type"), std::string("application/json")}}; } // Post JSON asynchronously - return cpr::PostCallback( - [](cpr::Response r) { - if (r.error) { - LOG_ERROR(WebService, "POST to %s returned cpr error: %u:%s", r.url.c_str(), - static_cast(r.error.code), r.error.message.c_str()); - return Common::WebResult{Common::WebResult::Code::CprError, r.error.message}; - } - if (r.status_code >= 400) { - LOG_ERROR(WebService, "POST to %s returned error status code: %u", r.url.c_str(), - r.status_code); - return Common::WebResult{Common::WebResult::Code::HttpError, - std::to_string(r.status_code)}; - } - if (r.header["content-type"].find("application/json") == std::string::npos) { - LOG_ERROR(WebService, "POST to %s returned wrong content: %s", r.url.c_str(), - r.header["content-type"].c_str()); - return Common::WebResult{Common::WebResult::Code::WrongContent, - r.header["content-type"]}; - } - return Common::WebResult{Common::WebResult::Code::Success, ""}; - }, - cpr::Url{url}, cpr::Body{data}, header); + return std::async(std::launch::async, [url, parsedUrl, params, data] { + std::unique_ptr cli = GetClientFor(parsedUrl); + + if (cli == nullptr) { + return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"}; + } + + hl::Request request; + request.method = "POST"; + request.path = "/" + parsedUrl.m_Path; + request.headers = params; + request.body = data; + + hl::Response response; + + if (!cli->send(request, response)) { + LOG_ERROR(WebService, "POST to %s returned null", url.c_str()); + return Common::WebResult{Common::WebResult::Code::LibError, "Null response"}; + } + + if (response.status >= 400) { + LOG_ERROR(WebService, "POST to %s returned error status code: %u", url.c_str(), + response.status); + return Common::WebResult{Common::WebResult::Code::HttpError, + std::to_string(response.status)}; + } + + auto content_type = response.headers.find("content-type"); + + if (content_type == response.headers.end() || + content_type->second.find("application/json") == std::string::npos) { + LOG_ERROR(WebService, "POST to %s returned wrong content: %s", url.c_str(), + content_type->second.c_str()); + return Common::WebResult{Common::WebResult::Code::WrongContent, content_type->second}; + } + + return Common::WebResult{Common::WebResult::Code::Success, ""}; + }); } template std::future GetJson(std::function func, const std::string& url, bool allow_anonymous, const std::string& username, const std::string& token) { - if (url.empty()) { + using lup = LUrlParser::clParseURL; + namespace hl = httplib; + + lup parsedUrl = lup::ParseURL(url); + + if (url.empty() || !parsedUrl.IsValid()) { LOG_ERROR(WebService, "URL is invalid"); return std::async(std::launch::deferred, [func{std::move(func)}]() { return func(""); }); } @@ -106,42 +144,55 @@ std::future GetJson(std::function func, const std::str return std::async(std::launch::deferred, [func{std::move(func)}]() { return func(""); }); } - Win32WSAStartup(); - // Built request header - cpr::Header header; + hl::Headers params; if (are_credentials_provided) { // Authenticated request if credentials are provided - header = {{"Content-Type", "application/json"}, - {"x-username", username.c_str()}, - {"x-token", token.c_str()}, - {"api-version", API_VERSION}}; + params = {{std::string("x-username"), username}, + {std::string("x-token"), token}, + {std::string("api-version"), std::string(API_VERSION)}}; } else { // Otherwise, anonymous request - header = cpr::Header{{"Content-Type", "application/json"}, {"api-version", API_VERSION}}; + params = {{std::string("api-version"), std::string(API_VERSION)}}; } // Get JSON asynchronously - return cpr::GetCallback( - [func{std::move(func)}](cpr::Response r) { - if (r.error) { - LOG_ERROR(WebService, "GET to %s returned cpr error: %u:%s", r.url.c_str(), - static_cast(r.error.code), r.error.message.c_str()); - return func(""); - } - if (r.status_code >= 400) { - LOG_ERROR(WebService, "GET to %s returned error code: %u", r.url.c_str(), - r.status_code); - return func(""); - } - if (r.header["content-type"].find("application/json") == std::string::npos) { - LOG_ERROR(WebService, "GET to %s returned wrong content: %s", r.url.c_str(), - r.header["content-type"].c_str()); - return func(""); - } - return func(r.text); - }, - cpr::Url{url}, header); + return std::async(std::launch::async, [func, url, parsedUrl, params] { + std::unique_ptr cli = GetClientFor(parsedUrl); + + if (cli == nullptr) { + return func(""); + } + + hl::Request request; + request.method = "GET"; + request.path = "/" + parsedUrl.m_Path; + request.headers = params; + + hl::Response response; + + if (!cli->send(request, response)) { + LOG_ERROR(WebService, "GET to %s returned null", url.c_str()); + return func(""); + } + + if (response.status >= 400) { + LOG_ERROR(WebService, "GET to %s returned error status code: %u", url.c_str(), + response.status); + return func(""); + } + + auto content_type = response.headers.find("content-type"); + + if (content_type == response.headers.end() || + content_type->second.find("application/json") == std::string::npos) { + LOG_ERROR(WebService, "GET to %s returned wrong content: %s", url.c_str(), + content_type->second.c_str()); + return func(""); + } + + return func(response.body); + }); } template std::future GetJson(std::function func, @@ -154,45 +205,66 @@ template std::future GetJson( void DeleteJson(const std::string& url, const std::string& data, const std::string& username, const std::string& token) { - if (url.empty()) { + using lup = LUrlParser::clParseURL; + namespace hl = httplib; + + lup parsedUrl = lup::ParseURL(url); + + if (url.empty() || !parsedUrl.IsValid()) { LOG_ERROR(WebService, "URL is invalid"); return; } - if (token.empty() || username.empty()) { + const bool are_credentials_provided{!token.empty() && !username.empty()}; + if (!are_credentials_provided) { LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); return; } - Win32WSAStartup(); - // Built request header - cpr::Header header = {{"Content-Type", "application/json"}, - {"x-username", username.c_str()}, - {"x-token", token.c_str()}, - {"api-version", API_VERSION}}; + hl::Headers params = {{std::string("x-username"), username}, + {std::string("x-token"), token}, + {std::string("api-version"), std::string(API_VERSION)}, + {std::string("Content-Type"), std::string("application/json")}}; // Delete JSON asynchronously - static std::future future; - future = cpr::DeleteCallback( - [](cpr::Response r) { - if (r.error) { - LOG_ERROR(WebService, "Delete to %s returned cpr error: %u:%s", r.url.c_str(), - static_cast(r.error.code), r.error.message.c_str()); - return; - } - if (r.status_code >= 400) { - LOG_ERROR(WebService, "Delete to %s returned error status code: %u", r.url.c_str(), - r.status_code); - return; - } - if (r.header["content-type"].find("application/json") == std::string::npos) { - LOG_ERROR(WebService, "Delete to %s returned wrong content: %s", r.url.c_str(), - r.header["content-type"].c_str()); - return; - } - }, - cpr::Url{url}, cpr::Body{data}, header); + std::async(std::launch::async, [url, parsedUrl, params, data] { + std::unique_ptr cli = GetClientFor(parsedUrl); + + if (cli == nullptr) { + return; + } + + hl::Request request; + request.method = "DELETE"; + request.path = "/" + parsedUrl.m_Path; + request.headers = params; + request.body = data; + + hl::Response response; + + if (!cli->send(request, response)) { + LOG_ERROR(WebService, "DELETE to %s returned null", url.c_str()); + return; + } + + if (response.status >= 400) { + LOG_ERROR(WebService, "DELETE to %s returned error status code: %u", url.c_str(), + response.status); + return; + } + + auto content_type = response.headers.find("content-type"); + + if (content_type == response.headers.end() || + content_type->second.find("application/json") == std::string::npos) { + LOG_ERROR(WebService, "DELETE to %s returned wrong content: %s", url.c_str(), + content_type->second.c_str()); + return; + } + + return; + }); } } // namespace WebService