From 70007ee1f9481a47bb2280544557504652296cf9 Mon Sep 17 00:00:00 2001 From: Chee Yee Date: Fri, 14 Jul 2023 01:09:58 -0700 Subject: [PATCH] make some web functions work --- data/assets/favicon.ico | Bin 0 -> 88907 bytes data/assets/index.html | 13 + source/fs.cpp | 33 +- source/fs.h | 3 +- source/http/httplib.cpp | 684 +++++++++++++++++++++++++--------- source/http/httplib.h | 208 +++++++++-- source/server/http_server.cpp | 461 ++++++++++++++++++++++- 7 files changed, 1188 insertions(+), 214 deletions(-) create mode 100644 data/assets/favicon.ico diff --git a/data/assets/favicon.ico b/data/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..90e1da4a4b0f4c9c0184f58d69baabe92cd82ed3 GIT binary patch literal 88907 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:HYr3l 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 GNdE%YM1WEN literal 0 HcmV?d00001 diff --git a/data/assets/index.html b/data/assets/index.html index 185f06c..7162632 100644 --- a/data/assets/index.html +++ b/data/assets/index.html @@ -31,6 +31,19 @@ config.set({ appName: 'ezRemote Client', listUrl: '/__local__/list', + uploadUrl: '/__local__/upload', + renameUrl: '/__local__/rename', + copyUrl: '/__local__/copy', + moveUrl: '/__local__/move', + removeUrl: '/__local__/remove', + editUrl: '/__local__/edit', + getContentUrl: '/__local__/getContent', + createFolderUrl: '/__local__/createFolder', + downloadFileUrl: '/__local__/downloadFile', + downloadMultipleUrl: '/__local__/downloadMultiple', + compressUrl: '/__local__/compress', + extractUrl: '/__local__/extract', + permissionsUrl: '/__local__/permission', pickCallback: function(item) { var msg = 'Picked %s "%s" for external use' .replace('%s', item.type) diff --git a/source/fs.cpp b/source/fs.cpp index 10ae16d..9925f43 100644 --- a/source/fs.cpp +++ b/source/fs.cpp @@ -77,6 +77,15 @@ namespace FS return (stat(path.c_str(), &dir_stat) == 0); } + int IsFolder(const std::string &path) + { + struct stat dir_stat = {0}; + if (stat(path.c_str(), &dir_stat) != 0) + return -1; + if (S_ISDIR(dir_stat.st_mode)) + return 1; + return 0; + } void Rename(const std::string &from, const std::string &to) { int res = rename(from.c_str(), to.c_str()); @@ -121,7 +130,7 @@ namespace FS int Write(FILE *f, const void *buffer, uint32_t size) { - int write = fwrite(buffer, size, 1, f); + int write = fwrite(buffer, 1, size, f); return write; } @@ -136,14 +145,17 @@ namespace FS if (fd == nullptr) return std::vector(0); const auto size = GetSize(path); + dbglogger_log("size=%lld", size); std::vector data(size); const auto read = fread(data.data(), 1, data.size(), fd); + dbglogger_log("read=%lld", size); fclose(fd); if (read < 0) return std::vector(0); - data.resize(read); + data.resize(read+1); + data[data.size()-1]=0; return data; } @@ -209,22 +221,23 @@ namespace FS return true; } - void Save(const std::string &path, const void *data, uint32_t size) + bool Save(const std::string &path, const void *data, uint32_t size) { FILE *fd = fopen(path.c_str(), "w+"); if (fd == nullptr) - return; + return false; const char *data8 = static_cast(data); while (size != 0) { - int written = fwrite(data8, size, 1, fd); + int written = fwrite(data8, 1, size, fd); fclose(fd); if (written <= 0) - return; + return false; data8 += written; size -= written; } + return true; } std::vector ListDir(const std::string &ppath, int *err) @@ -304,9 +317,7 @@ namespace FS entry.modified.seconds = lt.second; entry.file_size = file_stat.st_size; - dbglogger_log("%04d-%02d-%02d %02d:%02d:%02d", lt.year, lt.month, lt.day, lt.hour, lt.minute, lt.second); sprintf(entry.display_date, "%04d-%02d-%02d %02d:%02d:%02d", lt.year, lt.month, lt.day, lt.hour, lt.minute, lt.second); - dbglogger_log("display_date=%s", entry.display_date); if (dirent->d_type & DT_DIR) { @@ -478,6 +489,9 @@ namespace FS bool Copy(const std::string &from, const std::string &to) { MkDirs(to, true); + if (from.compare(to) == 0) + return true; + FILE *src = fopen(from.c_str(), "rb"); if (!src) { @@ -535,6 +549,9 @@ namespace FS bool Move(const std::string &from, const std::string &to) { + if (from.compare(to) == 0) + return true; + bool res = Copy(from, to); if (res) Rm(from); diff --git a/source/fs.h b/source/fs.h index 04649bb..bac39e0 100644 --- a/source/fs.h +++ b/source/fs.h @@ -25,6 +25,7 @@ namespace FS bool FileExists(const std::string &path); bool FolderExists(const std::string &path); + int IsFolder(const std::string &path); void Rename(const std::string &from, const std::string &to); @@ -54,7 +55,7 @@ namespace FS bool LoadText(std::vector *lines, const std::string &path); bool SaveText(std::vector *lines, const std::string &path); - void Save(const std::string &path, const void *data, uint32_t size); + bool Save(const std::string &path, const void *data, uint32_t size); std::vector ListFiles(const std::string &path); std::vector ListDir(const std::string &path, int *err); diff --git a/source/http/httplib.cpp b/source/http/httplib.cpp index 5e9fb0e..ed1f2ab 100644 --- a/source/http/httplib.cpp +++ b/source/http/httplib.cpp @@ -1,4 +1,5 @@ #include "httplib.h" +#include "dbglogger.h" namespace httplib { /* @@ -28,7 +29,7 @@ bool from_hex_to_i(const std::string &s, size_t i, size_t cnt, val = 0; for (; cnt; i++, cnt--) { if (!s[i]) { return false; } - int v = 0; + auto v = 0; if (is_hex(s[i], v)) { val = val * 16 + v; } else { @@ -39,7 +40,7 @@ bool from_hex_to_i(const std::string &s, size_t i, size_t cnt, } std::string from_i_to_hex(size_t n) { - const char *charset = "0123456789abcdef"; + static const auto charset = "0123456789abcdef"; std::string ret; do { ret = charset[n & 15] + ret; @@ -89,8 +90,8 @@ std::string base64_encode(const std::string &in) { std::string out; out.reserve(in.size()); - int val = 0; - int valb = -6; + auto val = 0; + auto valb = -6; for (auto c : in) { val = (val << 8) + static_cast(c); @@ -221,7 +222,7 @@ std::string decode_url(const std::string &s, for (size_t i = 0; i < s.size(); i++) { if (s[i] == '%' && i + 1 < s.size()) { if (s[i + 1] == 'u') { - int val = 0; + auto val = 0; if (from_hex_to_i(s, i + 2, 4, val)) { // 4 digits Unicode codes char buff[4]; @@ -232,7 +233,7 @@ std::string decode_url(const std::string &s, result += s[i]; } } else { - int val = 0; + auto val = 0; if (from_hex_to_i(s, i + 1, 2, val)) { // 2 digits hex codes result += static_cast(val); @@ -379,7 +380,7 @@ int close_socket(socket_t sock) { } template ssize_t handle_EINTR(T fn) { - ssize_t res = false; + ssize_t res = 0; while (true) { res = fn(); if (res < 0 && errno == EINTR) { continue; } @@ -483,7 +484,7 @@ Error wait_until_socket_is_ready(socket_t sock, time_t sec, if (poll_res == 0) { return Error::ConnectionTimeout; } if (poll_res > 0 && pfd_read.revents & (POLLIN | POLLOUT)) { - int error = 0; + auto error = 0; socklen_t len = sizeof(error); auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR, reinterpret_cast(&error), &len); @@ -515,7 +516,7 @@ Error wait_until_socket_is_ready(socket_t sock, time_t sec, if (ret == 0) { return Error::ConnectionTimeout; } if (ret > 0 && (FD_ISSET(sock, &fdsr) || FD_ISSET(sock, &fdsw))) { - int error = 0; + auto error = 0; socklen_t len = sizeof(error); auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR, reinterpret_cast(&error), &len); @@ -695,7 +696,7 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port, auto sock = socket(hints.ai_family, hints.ai_socktype, hints.ai_protocol); if (sock != INVALID_SOCKET) { - sockaddr_un addr; + sockaddr_un addr{}; addr.sun_family = AF_UNIX; std::copy(host.begin(), host.end(), addr.sun_path); @@ -753,21 +754,34 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port, if (sock == INVALID_SOCKET) { continue; } #ifndef _WIN32 - if (fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) { continue; } + if (fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) { + close_socket(sock); + continue; + } #endif if (tcp_nodelay) { - int yes = 1; - setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&yes), - sizeof(yes)); + auto yes = 1; +#ifdef _WIN32 + setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, + reinterpret_cast(&yes), sizeof(yes)); +#else + setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, + reinterpret_cast(&yes), sizeof(yes)); +#endif } if (socket_options) { socket_options(sock); } if (rp->ai_family == AF_INET6) { - int no = 0; - setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&no), - sizeof(no)); + auto no = 0; +#ifdef _WIN32 + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, + reinterpret_cast(&no), sizeof(no)); +#else + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, + reinterpret_cast(&no), sizeof(no)); +#endif } // bind or connect @@ -826,7 +840,7 @@ bool bind_ip_address(socket_t sock, const std::string &host) { return ret; } -#if !defined _WIN32 && !defined ANDROID && !defined _AIX +#if !defined _WIN32 && !defined ANDROID && !defined _AIX && !defined __MVS__ #define USE_IF2IP #endif @@ -910,13 +924,14 @@ socket_t create_client_socket( #ifdef _WIN32 auto timeout = static_cast(read_timeout_sec * 1000 + read_timeout_usec / 1000); - setsockopt(sock2, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, - sizeof(timeout)); + setsockopt(sock2, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&timeout), sizeof(timeout)); #else timeval tv; tv.tv_sec = static_cast(read_timeout_sec); tv.tv_usec = static_cast(read_timeout_usec); - setsockopt(sock2, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv)); + setsockopt(sock2, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&tv), sizeof(tv)); #endif } { @@ -924,18 +939,19 @@ socket_t create_client_socket( #ifdef _WIN32 auto timeout = static_cast(write_timeout_sec * 1000 + write_timeout_usec / 1000); - setsockopt(sock2, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, - sizeof(timeout)); + setsockopt(sock2, SOL_SOCKET, SO_SNDTIMEO, + reinterpret_cast(&timeout), sizeof(timeout)); #else timeval tv; tv.tv_sec = static_cast(write_timeout_sec); tv.tv_usec = static_cast(write_timeout_usec); - setsockopt(sock2, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv)); + setsockopt(sock2, SOL_SOCKET, SO_SNDTIMEO, + reinterpret_cast(&tv), sizeof(tv)); #endif - int const size = 1048576; - setsockopt(sock2, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)); - setsockopt(sock2, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size)); + //int const size = 1048576; + //setsockopt(sock2, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)); + //setsockopt(sock2, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size)); } error = Error::Success; @@ -1012,9 +1028,14 @@ void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) { constexpr unsigned int str2tag_core(const char *s, size_t l, unsigned int h) { - return (l == 0) ? h - : str2tag_core(s + 1, l - 1, - (h * 33) ^ static_cast(*s)); + return (l == 0) + ? h + : str2tag_core( + s + 1, l - 1, + // Unsets the 6 high bits of h, therefore no overflow happens + (((std::numeric_limits::max)() >> 6) & + h * 33) ^ + static_cast(*s)); } unsigned int str2tag(const std::string &s) { @@ -1239,7 +1260,7 @@ bool gzip_compressor::compress(const char *data, size_t data_length, data += strm_.avail_in; auto flush = (last && data_length == 0) ? Z_FINISH : Z_NO_FLUSH; - int ret = Z_OK; + auto ret = Z_OK; std::array buff{}; do { @@ -1283,7 +1304,7 @@ bool gzip_decompressor::decompress(const char *data, size_t data_length, Callback callback) { assert(is_valid_); - int ret = Z_OK; + auto ret = Z_OK; do { constexpr size_t max_avail_in = @@ -1297,16 +1318,12 @@ bool gzip_decompressor::decompress(const char *data, size_t data_length, data += strm_.avail_in; std::array buff{}; - while (strm_.avail_in > 0) { + while (strm_.avail_in > 0 && ret == Z_OK) { strm_.avail_out = static_cast(buff.size()); strm_.next_out = reinterpret_cast(buff.data()); - auto prev_avail_in = strm_.avail_in; - ret = inflate(&strm_, Z_NO_FLUSH); - if (prev_avail_in - strm_.avail_in == 0) { return false; } - assert(ret != Z_STREAM_ERROR); switch (ret) { case Z_NEED_DICT: @@ -1388,7 +1405,7 @@ bool brotli_decompressor::decompress(const char *data, return 0; } - const uint8_t *next_in = (const uint8_t *)data; + auto next_in = reinterpret_cast(data); size_t avail_in = data_length; size_t total_out; @@ -1427,6 +1444,14 @@ const char *get_header_value(const Headers &headers, return def; } +bool compare_case_ignore(const std::string &a, const std::string &b) { + if (a.size() != b.size()) { return false; } + for (size_t i = 0; i < b.size(); i++) { + if (::tolower(a[i]) != ::tolower(b[i])) { return false; } + } + return true; +} + template bool parse_header(const char *beg, const char *end, T fn) { // Skip trailing spaces and tabs. @@ -1450,7 +1475,11 @@ bool parse_header(const char *beg, const char *end, T fn) { } if (p < end) { - fn(std::string(beg, key_end), decode_url(std::string(p, end), false)); + auto key = std::string(beg, key_end); + auto val = compare_case_ignore(key, "Location") + ? std::string(p, end) + : decode_url(std::string(p, end), false); + fn(std::move(key), std::move(val)); return true; } @@ -1548,7 +1577,8 @@ bool read_content_without_length(Stream &strm, return true; } -bool read_content_chunked(Stream &strm, +template +bool read_content_chunked(Stream &strm, T &x, ContentReceiverWithProgress out) { const auto bufsiz = 16; char buf[bufsiz]; @@ -1574,15 +1604,29 @@ bool read_content_chunked(Stream &strm, if (!line_reader.getline()) { return false; } - if (strcmp(line_reader.ptr(), "\r\n")) { break; } + if (strcmp(line_reader.ptr(), "\r\n")) { return false; } if (!line_reader.getline()) { return false; } } - if (chunk_len == 0) { - // Reader terminator after chunks - if (!line_reader.getline() || strcmp(line_reader.ptr(), "\r\n")) - return false; + assert(chunk_len == 0); + + // Trailer + if (!line_reader.getline()) { return false; } + + while (strcmp(line_reader.ptr(), "\r\n")) { + if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } + + // Exclude line terminator + constexpr auto line_terminator_len = 2; + auto end = line_reader.ptr() + line_reader.size() - line_terminator_len; + + parse_header(line_reader.ptr(), end, + [&](std::string &&key, std::string &&val) { + x.headers.emplace(std::move(key), std::move(val)); + }); + + if (!line_reader.getline()) { return false; } } return true; @@ -1652,16 +1696,20 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status, auto exceed_payload_max_length = false; if (is_chunked_transfer_encoding(x.headers)) { - ret = read_content_chunked(strm, out); + dbglogger_log("read_content_chunked"); + ret = read_content_chunked(strm, x, out); } else if (!has_header(x.headers, "Content-Length")) { + dbglogger_log("read_content_without_length"); ret = read_content_without_length(strm, out); } else { + dbglogger_log("skip_content_with_length"); auto len = get_header_value(x.headers, "Content-Length"); if (len > payload_max_length) { exceed_payload_max_length = true; skip_content_with_length(strm, len); ret = false; } else if (len > 0) { + dbglogger_log("read_content_with_length %llu", len); ret = read_content_with_length(strm, len, std::move(progress), out); } } @@ -1808,7 +1856,7 @@ write_content_chunked(Stream &strm, const ContentProvider &content_provider, return ok; }; - data_sink.done = [&](void) { + auto done_with_trailer = [&](const Headers *trailer) { if (!ok) { return; } data_available = false; @@ -1826,16 +1874,36 @@ write_content_chunked(Stream &strm, const ContentProvider &content_provider, if (!payload.empty()) { // Emit chunked response header and footer for each chunk auto chunk = from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n"; - if (!write_data(strm, chunk.data(), chunk.size())) { + if (!strm.is_writable() || + !write_data(strm, chunk.data(), chunk.size())) { ok = false; return; } } - static const std::string done_marker("0\r\n\r\n"); + static const std::string done_marker("0\r\n"); if (!write_data(strm, done_marker.data(), done_marker.size())) { ok = false; } + + // Trailer + if (trailer) { + for (const auto &kv : *trailer) { + std::string field_line = kv.first + ": " + kv.second + "\r\n"; + if (!write_data(strm, field_line.data(), field_line.size())) { + ok = false; + } + } + } + + static const std::string crlf("\r\n"); + if (!write_data(strm, crlf.data(), crlf.size())) { ok = false; } + }; + + data_sink.done = [&](void) { done_with_trailer(nullptr); }; + + data_sink.done_with_trailer = [&](const Headers &trailer) { + done_with_trailer(&trailer); }; while (data_available && !is_shutting_down()) { @@ -1884,7 +1952,8 @@ bool redirect(T &cli, Request &req, Response &res, if (ret) { req = new_req; res = new_res; - res.location = location; + + if (res.location.empty()) res.location = location; } return ret; } @@ -1926,9 +1995,12 @@ void parse_query_text(const std::string &s, Params ¶ms) { bool parse_multipart_boundary(const std::string &content_type, std::string &boundary) { - auto pos = content_type.find("boundary="); + auto boundary_keyword = "boundary="; + auto pos = content_type.find(boundary_keyword); if (pos == std::string::npos) { return false; } - boundary = content_type.substr(pos + 9); + auto end = content_type.find(';', pos); + auto beg = pos + strlen(boundary_keyword); + boundary = content_type.substr(beg, end - beg); if (boundary.length() >= 2 && boundary.front() == '"' && boundary.back() == '"') { boundary = boundary.substr(1, boundary.size() - 2); @@ -2442,7 +2514,7 @@ std::string message_digest(const std::string &s, const EVP_MD *algo) { std::stringstream ss; for (auto i = 0u; i < hash_length; ++i) { ss << std::hex << std::setw(2) << std::setfill('0') - << (unsigned int)hash[i]; + << static_cast(hash[i]); } return ss.str(); @@ -2461,15 +2533,15 @@ std::string SHA_512(const std::string &s) { } #endif -#ifdef _WIN32 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef _WIN32 // NOTE: This code came up with the following stackoverflow post: // https://stackoverflow.com/questions/9507184/can-openssl-on-windows-use-the-system-certificate-store bool load_system_certs_on_windows(X509_STORE *store) { auto hStore = CertOpenSystemStoreW((HCRYPTPROV_LEGACY)NULL, L"ROOT"); - if (!hStore) { return false; } + auto result = false; PCCERT_CONTEXT pContext = NULL; while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) != nullptr) { @@ -2480,16 +2552,109 @@ bool load_system_certs_on_windows(X509_STORE *store) { if (x509) { X509_STORE_add_cert(store, x509); X509_free(x509); + result = true; } } CertFreeCertificateContext(pContext); CertCloseStore(hStore, 0); + return result; +} +#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && defined(__APPLE__) +#if TARGET_OS_OSX +template +using CFObjectPtr = + std::unique_ptr::type, void (*)(CFTypeRef)>; + +void cf_object_ptr_deleter(CFTypeRef obj) { + if (obj) { CFRelease(obj); } +} + +bool retrieve_certs_from_keychain(CFObjectPtr &certs) { + CFStringRef keys[] = {kSecClass, kSecMatchLimit, kSecReturnRef}; + CFTypeRef values[] = {kSecClassCertificate, kSecMatchLimitAll, + kCFBooleanTrue}; + + CFObjectPtr query( + CFDictionaryCreate(nullptr, reinterpret_cast(keys), values, + sizeof(keys) / sizeof(keys[0]), + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks), + cf_object_ptr_deleter); + + if (!query) { return false; } + + CFTypeRef security_items = nullptr; + if (SecItemCopyMatching(query.get(), &security_items) != errSecSuccess || + CFArrayGetTypeID() != CFGetTypeID(security_items)) { + return false; + } + + certs.reset(reinterpret_cast(security_items)); return true; } -#endif +bool retrieve_root_certs_from_keychain(CFObjectPtr &certs) { + CFArrayRef root_security_items = nullptr; + if (SecTrustCopyAnchorCertificates(&root_security_items) != errSecSuccess) { + return false; + } + + certs.reset(root_security_items); + return true; +} + +bool add_certs_to_x509_store(CFArrayRef certs, X509_STORE *store) { + auto result = false; + for (auto i = 0; i < CFArrayGetCount(certs); ++i) { + const auto cert = reinterpret_cast( + CFArrayGetValueAtIndex(certs, i)); + + if (SecCertificateGetTypeID() != CFGetTypeID(cert)) { continue; } + + CFDataRef cert_data = nullptr; + if (SecItemExport(cert, kSecFormatX509Cert, 0, nullptr, &cert_data) != + errSecSuccess) { + continue; + } + + CFObjectPtr cert_data_ptr(cert_data, cf_object_ptr_deleter); + + auto encoded_cert = static_cast( + CFDataGetBytePtr(cert_data_ptr.get())); + + auto x509 = + d2i_X509(NULL, &encoded_cert, CFDataGetLength(cert_data_ptr.get())); + + if (x509) { + X509_STORE_add_cert(store, x509); + X509_free(x509); + result = true; + } + } + + return result; +} + +bool load_system_certs_on_macos(X509_STORE *store) { + auto result = false; + CFObjectPtr certs(nullptr, cf_object_ptr_deleter); + if (retrieve_certs_from_keychain(certs) && certs) { + result = add_certs_to_x509_store(certs.get(), store); + } + + if (retrieve_root_certs_from_keychain(certs) && certs) { + result = add_certs_to_x509_store(certs.get(), store) || result; + } + + return result; +} +#endif // TARGET_OS_OSX +#endif // _WIN32 +#endif // CPPHTTPLIB_OPENSSL_SUPPORT + +#ifdef _WIN32 class WSInit { public: WSInit() { @@ -2660,7 +2825,7 @@ void hosted_at(const std::string &hostname, const auto &addr = *reinterpret_cast(rp->ai_addr); std::string ip; - int dummy = -1; + auto dummy = -1; if (detail::get_ip_and_port(addr, sizeof(struct sockaddr_storage), ip, dummy)) { addrs.push_back(ip); @@ -2764,13 +2929,14 @@ MultipartFormData Request::get_file_value(const std::string &key) const { return MultipartFormData(); } -std::vector Request::get_file_values(const std::string &key) const { - std::vector values; - auto rng = files.equal_range(key); - for (auto it = rng.first; it != rng.second; it++) { - values.push_back(it->second); - } - return values; +std::vector +Request::get_file_values(const std::string &key) const { + std::vector values; + auto rng = files.equal_range(key); + for (auto it = rng.first; it != rng.second; it++) { + values.push_back(it->second); + } + return values; } // Response implementation @@ -2823,10 +2989,9 @@ void Response::set_content(const std::string &s, void Response::set_content_provider( size_t in_length, const std::string &content_type, ContentProvider provider, ContentProviderResourceReleaser resource_releaser) { - assert(in_length > 0); set_header("Content-Type", content_type); content_length_ = in_length; - content_provider_ = std::move(provider); + if (in_length > 0) { content_provider_ = std::move(provider); } content_provider_resource_releaser_ = resource_releaser; is_chunked_content_provider_ = false; } @@ -2998,13 +3163,105 @@ socket_t BufferStream::socket() const { return 0; } const std::string &BufferStream::get_buffer() const { return buffer; } +PathParamsMatcher::PathParamsMatcher(const std::string &pattern) { + // One past the last ending position of a path param substring + std::size_t last_param_end = 0; + +#ifndef CPPHTTPLIB_NO_EXCEPTIONS + // Needed to ensure that parameter names are unique during matcher + // construction + // If exceptions are disabled, only last duplicate path + // parameter will be set + std::unordered_set param_name_set; +#endif + + while (true) { + const auto marker_pos = pattern.find(marker, last_param_end); + if (marker_pos == std::string::npos) { break; } + + static_fragments_.push_back( + pattern.substr(last_param_end, marker_pos - last_param_end)); + + const auto param_name_start = marker_pos + 1; + + auto sep_pos = pattern.find(separator, param_name_start); + if (sep_pos == std::string::npos) { sep_pos = pattern.length(); } + + auto param_name = + pattern.substr(param_name_start, sep_pos - param_name_start); + +#ifndef CPPHTTPLIB_NO_EXCEPTIONS + if (param_name_set.find(param_name) != param_name_set.cend()) { + std::string msg = "Encountered path parameter '" + param_name + + "' multiple times in route pattern '" + pattern + "'."; + throw std::invalid_argument(msg); + } +#endif + + param_names_.push_back(std::move(param_name)); + + last_param_end = sep_pos + 1; + } + + if (last_param_end < pattern.length()) { + static_fragments_.push_back(pattern.substr(last_param_end)); + } +} + +bool PathParamsMatcher::match(Request &request) const { + request.matches = std::smatch(); + request.path_params.clear(); + request.path_params.reserve(param_names_.size()); + + // One past the position at which the path matched the pattern last time + std::size_t starting_pos = 0; + for (size_t i = 0; i < static_fragments_.size(); ++i) { + const auto &fragment = static_fragments_[i]; + + if (starting_pos + fragment.length() > request.path.length()) { + return false; + } + + // Avoid unnecessary allocation by using strncmp instead of substr + + // comparison + if (std::strncmp(request.path.c_str() + starting_pos, fragment.c_str(), + fragment.length()) != 0) { + return false; + } + + starting_pos += fragment.length(); + + // Should only happen when we have a static fragment after a param + // Example: '/users/:id/subscriptions' + // The 'subscriptions' fragment here does not have a corresponding param + if (i >= param_names_.size()) { continue; } + + auto sep_pos = request.path.find(separator, starting_pos); + if (sep_pos == std::string::npos) { sep_pos = request.path.length(); } + + const auto ¶m_name = param_names_[i]; + + request.path_params.emplace( + param_name, request.path.substr(starting_pos, sep_pos - starting_pos)); + + // Mark everythin up to '/' as matched + starting_pos = sep_pos + 1; + } + // Returns false if the path is longer than the pattern + return starting_pos >= request.path.length(); +} + +bool RegexMatcher::match(Request &request) const { + request.path_params.clear(); + return std::regex_match(request.path, request.matches, regex_); +} + } // namespace detail // HTTP server implementation Server::Server() : new_task_queue( - [] { return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT); }), - svr_sock_(INVALID_SOCKET), is_running_(false) { + [] { return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT); }) { #ifndef _WIN32 signal(SIGPIPE, SIG_IGN); #endif @@ -3012,67 +3269,76 @@ Server::Server() Server::~Server() {} +std::unique_ptr +Server::make_matcher(const std::string &pattern) { + if (pattern.find("/:") != std::string::npos) { + return detail::make_unique(pattern); + } else { + return detail::make_unique(pattern); + } +} + Server &Server::Get(const std::string &pattern, Handler handler) { get_handlers_.push_back( - std::make_pair(std::regex(pattern), std::move(handler))); + std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } Server &Server::Post(const std::string &pattern, Handler handler) { post_handlers_.push_back( - std::make_pair(std::regex(pattern), std::move(handler))); + std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } Server &Server::Post(const std::string &pattern, HandlerWithContentReader handler) { post_handlers_for_content_reader_.push_back( - std::make_pair(std::regex(pattern), std::move(handler))); + std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } Server &Server::Put(const std::string &pattern, Handler handler) { put_handlers_.push_back( - std::make_pair(std::regex(pattern), std::move(handler))); + std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } Server &Server::Put(const std::string &pattern, HandlerWithContentReader handler) { put_handlers_for_content_reader_.push_back( - std::make_pair(std::regex(pattern), std::move(handler))); + std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } Server &Server::Patch(const std::string &pattern, Handler handler) { patch_handlers_.push_back( - std::make_pair(std::regex(pattern), std::move(handler))); + std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } Server &Server::Patch(const std::string &pattern, HandlerWithContentReader handler) { patch_handlers_for_content_reader_.push_back( - std::make_pair(std::regex(pattern), std::move(handler))); + std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } Server &Server::Delete(const std::string &pattern, Handler handler) { delete_handlers_.push_back( - std::make_pair(std::regex(pattern), std::move(handler))); + std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } Server &Server::Delete(const std::string &pattern, HandlerWithContentReader handler) { delete_handlers_for_content_reader_.push_back( - std::make_pair(std::regex(pattern), std::move(handler))); + std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } Server &Server::Options(const std::string &pattern, Handler handler) { options_handlers_.push_back( - std::make_pair(std::regex(pattern), std::move(handler))); + std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } @@ -3151,7 +3417,6 @@ Server &Server::set_logger(Logger logger) { Server & Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) { expect_100_continue_handler_ = std::move(handler); - return *this; } @@ -3217,15 +3482,25 @@ int Server::bind_to_any_port(const std::string &host, int socket_flags) { return bind_internal(host, 0, socket_flags); } -bool Server::listen_after_bind() { return listen_internal(); } +bool Server::listen_after_bind() { + auto se = detail::scope_exit([&]() { done_ = true; }); + return listen_internal(); +} bool Server::listen(const std::string &host, int port, int socket_flags) { + auto se = detail::scope_exit([&]() { done_ = true; }); return bind_to_port(host, port, socket_flags) && listen_internal(); } bool Server::is_running() const { return is_running_; } +void Server::wait_until_ready() const { + while (!is_running() && !done_) { + std::this_thread::sleep_for(std::chrono::milliseconds{1}); + } +} + void Server::stop() { if (is_running_) { assert(svr_sock_ != INVALID_SOCKET); @@ -3488,7 +3763,7 @@ bool Server::read_content_with_content_receiver( bool Server::read_content_core(Stream &strm, Request &req, Response &res, ContentReceiver receiver, - MultipartContentHeader mulitpart_header, + MultipartContentHeader multipart_header, ContentReceiver multipart_receiver) { detail::MultipartFormDataParser multipart_form_data_parser; ContentReceiverWithProgress out; @@ -3508,14 +3783,14 @@ bool Server::read_content_core(Stream &strm, Request &req, Response &res, while (pos < n) { auto read_size = (std::min)(1, n - pos); auto ret = multipart_form_data_parser.parse( - buf + pos, read_size, multipart_receiver, mulitpart_header); + buf + pos, read_size, multipart_receiver, multipart_header); if (!ret) { return false; } pos += read_size; } return true; */ return multipart_form_data_parser.parse(buf, n, multipart_receiver, - mulitpart_header); + multipart_header); }; } else { out = [receiver](const char *buf, size_t n, uint64_t /*off*/, @@ -3616,6 +3891,7 @@ int Server::bind_internal(const std::string &host, int port, bool Server::listen_internal() { auto ret = true; is_running_ = true; + auto se = detail::scope_exit([&]() { is_running_ = false; }); { std::unique_ptr task_queue(new_task_queue()); @@ -3657,13 +3933,14 @@ bool Server::listen_internal() { #ifdef _WIN32 auto timeout = static_cast(read_timeout_sec_ * 1000 + read_timeout_usec_ / 1000); - setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, - sizeof(timeout)); + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&timeout), sizeof(timeout)); #else timeval tv; tv.tv_sec = static_cast(read_timeout_sec_); tv.tv_usec = static_cast(read_timeout_usec_); - setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv)); + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&tv), sizeof(tv)); #endif } { @@ -3671,13 +3948,14 @@ bool Server::listen_internal() { #ifdef _WIN32 auto timeout = static_cast(write_timeout_sec_ * 1000 + write_timeout_usec_ / 1000); - setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, - sizeof(timeout)); + setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, + reinterpret_cast(&timeout), sizeof(timeout)); #else timeval tv; tv.tv_sec = static_cast(write_timeout_sec_); tv.tv_usec = static_cast(write_timeout_usec_); - setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv)); + setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, + reinterpret_cast(&tv), sizeof(tv)); #endif } @@ -3687,7 +3965,6 @@ bool Server::listen_internal() { task_queue->shutdown(); } - is_running_ = false; return ret; } @@ -3698,7 +3975,7 @@ bool Server::routing(Request &req, Response &res, Stream &strm) { } // File handler - bool is_head_request = req.method == "HEAD"; + auto is_head_request = req.method == "HEAD"; if ((req.method == "GET" || is_head_request) && handle_file_request(req, res, is_head_request)) { return true; @@ -3771,10 +4048,10 @@ bool Server::routing(Request &req, Response &res, Stream &strm) { bool Server::dispatch_request(Request &req, Response &res, const Handlers &handlers) { for (const auto &x : handlers) { - const auto &pattern = x.first; + const auto &matcher = x.first; const auto &handler = x.second; - if (std::regex_match(req.path, req.matches, pattern)) { + if (matcher->match(req)) { handler(req, res); return true; } @@ -3794,8 +4071,8 @@ void Server::apply_ranges(const Request &req, Response &res, res.headers.erase(it); } - res.headers.emplace("Content-Type", - "multipart/byteranges; boundary=" + boundary); + res.set_header("Content-Type", + "multipart/byteranges; boundary=" + boundary); } auto type = detail::encoding_type(req, res); @@ -3896,10 +4173,10 @@ bool Server::dispatch_request_for_content_reader( Request &req, Response &res, ContentReader content_reader, const HandlersForContentReader &handlers) { for (const auto &x : handlers) { - const auto &pattern = x.first; + const auto &matcher = x.first; const auto &handler = x.second; - if (std::regex_match(req.path, req.matches, pattern)) { + if (matcher->match(req)) { handler(req, res, content_reader); return true; } @@ -3919,15 +4196,10 @@ Server::process_request(Stream &strm, bool close_connection, if (!line_reader.getline()) { return false; } Request req; + Response res; - res.version = "HTTP/1.1"; - - for (const auto &header : default_headers_) { - if (res.headers.find(header.first) == res.headers.end()) { - res.headers.insert(header); - } - } + res.headers = default_headers_; #ifdef _WIN32 // TODO: Increase FD_SETSIZE statically (libzmq), dynamically (MySQL). @@ -4228,7 +4500,15 @@ bool ClientImpl::read_response_line(Stream &strm, const Request &req, bool ClientImpl::send(Request &req, Response &res, Error &error) { std::lock_guard request_mutex_guard(request_mutex_); + auto ret = send_(req, res, error); + if (error == Error::SSLPeerCouldBeClosed_) { + assert(!ret); + ret = send_(req, res, error); + } + return ret; +} +bool ClientImpl::send_(Request &req, Response &res, Error &error) { { std::lock_guard guard(socket_mutex_); @@ -4259,7 +4539,7 @@ bool ClientImpl::send(Request &req, Response &res, Error &error) { if (is_ssl()) { auto &scli = static_cast(*this); if (!proxy_host_.empty() && proxy_port_ != -1) { - bool success = false; + auto success = false; if (!scli.connect_with_proxy(socket_, res, success, error)) { return success; } @@ -4286,13 +4566,11 @@ bool ClientImpl::send(Request &req, Response &res, Error &error) { } } + auto ret = false; auto close_connection = !keep_alive_; - auto ret = process_socket(socket_, [&](Stream &strm) { - return handle_request(strm, req, res, close_connection, error); - }); - // Briefly lock mutex in order to mark that a request is no longer ongoing - { + auto se = detail::scope_exit([&]() { + // Briefly lock mutex in order to mark that a request is no longer ongoing std::lock_guard guard(socket_mutex_); socket_requests_in_flight_ -= 1; if (socket_requests_in_flight_ <= 0) { @@ -4306,7 +4584,11 @@ bool ClientImpl::send(Request &req, Response &res, Error &error) { shutdown_socket(socket_); close_socket(socket_); } - } + }); + + ret = process_socket(socket_, [&](Stream &strm) { + return handle_request(strm, req, res, close_connection, error); + }); if (!ret) { if (error == Error::Success) { error = Error::Unknown; } @@ -4394,11 +4676,11 @@ bool ClientImpl::redirect(Request &req, Response &res, Error &error) { return false; } - auto location = detail::decode_url(res.get_header_value("location"), true); + auto location = res.get_header_value("location"); if (location.empty()) { return false; } const static std::regex re( - R"((?:(https?):)?(?://(?:\[([\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*(?:\?[^#]*)?)(?:#.*)?)"); + R"((?:(https?):)?(?://(?:\[([\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*)(\?[^#]*)?(?:#.*)?)"); std::smatch m; if (!std::regex_match(location, m, re)) { return false; } @@ -4410,6 +4692,7 @@ bool ClientImpl::redirect(Request &req, Response &res, Error &error) { if (next_host.empty()) { next_host = m[3].str(); } auto port_str = m[4].str(); auto next_path = m[5].str(); + auto next_query = m[6].str(); auto next_port = port_; if (!port_str.empty()) { @@ -4422,22 +4705,24 @@ bool ClientImpl::redirect(Request &req, Response &res, Error &error) { if (next_host.empty()) { next_host = host_; } if (next_path.empty()) { next_path = "/"; } + auto path = detail::decode_url(next_path, true) + next_query; + if (next_scheme == scheme && next_host == host_ && next_port == port_) { - return detail::redirect(*this, req, res, next_path, location, error); + return detail::redirect(*this, req, res, path, location, error); } else { if (next_scheme == "https") { #ifdef CPPHTTPLIB_OPENSSL_SUPPORT SSLClient cli(next_host.c_str(), next_port); cli.copy_settings(*this); if (ca_cert_store_) { cli.set_ca_cert_store(ca_cert_store_); } - return detail::redirect(cli, req, res, next_path, location, error); + return detail::redirect(cli, req, res, path, location, error); #else return false; #endif } else { ClientImpl cli(next_host.c_str(), next_port); cli.copy_settings(*this); - return detail::redirect(cli, req, res, next_path, location, error); + return detail::redirect(cli, req, res, path, location, error); } } } @@ -4448,7 +4733,7 @@ bool ClientImpl::write_content_with_provider(Stream &strm, auto is_shutting_down = []() { return false; }; if (req.is_chunked_content_provider_) { - // TODO: Brotli suport + // TODO: Brotli support std::unique_ptr compressor; #ifdef CPPHTTPLIB_ZLIB_SUPPORT if (compress_) { @@ -4472,32 +4757,32 @@ bool ClientImpl::write_request(Stream &strm, Request &req, // Prepare additional headers if (close_connection) { if (!req.has_header("Connection")) { - req.headers.emplace("Connection", "close"); + req.set_header("Connection", "close"); } } if (!req.has_header("Host")) { if (is_ssl()) { if (port_ == 443) { - req.headers.emplace("Host", host_); + req.set_header("Host", host_); } else { - req.headers.emplace("Host", host_and_port_); + req.set_header("Host", host_and_port_); } } else { if (port_ == 80) { - req.headers.emplace("Host", host_); + req.set_header("Host", host_); } else { - req.headers.emplace("Host", host_and_port_); + req.set_header("Host", host_and_port_); } } } - if (!req.has_header("Accept")) { req.headers.emplace("Accept", "*/*"); } + if (!req.has_header("Accept")) { req.set_header("Accept", "*/*"); } #ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT if (!req.has_header("User-Agent")) { auto agent = std::string("cpp-httplib/") + CPPHTTPLIB_VERSION; - req.headers.emplace("User-Agent", agent); + req.set_header("User-Agent", agent); } #endif @@ -4506,23 +4791,23 @@ bool ClientImpl::write_request(Stream &strm, Request &req, if (!req.is_chunked_content_provider_) { if (!req.has_header("Content-Length")) { auto length = std::to_string(req.content_length_); - req.headers.emplace("Content-Length", length); + req.set_header("Content-Length", length); } } } else { if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH") { - req.headers.emplace("Content-Length", "0"); + req.set_header("Content-Length", "0"); } } } else { if (!req.has_header("Content-Type")) { - req.headers.emplace("Content-Type", "text/plain"); + req.set_header("Content-Type", "text/plain"); } if (!req.has_header("Content-Length")) { auto length = std::to_string(req.body.size()); - req.headers.emplace("Content-Length", length); + req.set_header("Content-Length", length); } } @@ -4590,12 +4875,10 @@ std::unique_ptr ClientImpl::send_with_content_provider( ContentProvider content_provider, ContentProviderWithoutLength content_provider_without_length, const std::string &content_type, Error &error) { - if (!content_type.empty()) { - req.headers.emplace("Content-Type", content_type); - } + if (!content_type.empty()) { req.set_header("Content-Type", content_type); } #ifdef CPPHTTPLIB_ZLIB_SUPPORT - if (compress_) { req.headers.emplace("Content-Encoding", "gzip"); } + if (compress_) { req.set_header("Content-Encoding", "gzip"); } #endif #ifdef CPPHTTPLIB_ZLIB_SUPPORT @@ -4656,10 +4939,9 @@ std::unique_ptr ClientImpl::send_with_content_provider( req.content_provider_ = detail::ContentProviderAdapter( std::move(content_provider_without_length)); req.is_chunked_content_provider_ = true; - req.headers.emplace("Transfer-Encoding", "chunked"); + req.set_header("Transfer-Encoding", "chunked"); } else { req.body.assign(body, content_length); - ; } } @@ -4698,6 +4980,20 @@ bool ClientImpl::process_request(Stream &strm, Request &req, // Send request if (!write_request(strm, req, close_connection, error)) { return false; } +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if (is_ssl()) { + auto is_proxy_enabled = !proxy_host_.empty() && proxy_port_ != -1; + if (!is_proxy_enabled) { + char buf[1]; + if (SSL_peek(socket_.ssl, buf, 1) == 0 && + SSL_get_error(socket_.ssl, 0) == SSL_ERROR_ZERO_RETURN) { + error = Error::SSLPeerCouldBeClosed_; + return false; + } + } + } +#endif + // Receive response and headers if (!read_response_line(strm, req, res) || !detail::read_headers(strm, res.headers)) { @@ -5265,9 +5561,7 @@ Result ClientImpl::Delete(const std::string &path, req.headers = headers; req.path = path; - if (!content_type.empty()) { - req.headers.emplace("Content-Type", content_type); - } + if (!content_type.empty()) { req.set_header("Content-Type", content_type); } req.body.assign(body, content_length); return send_(std::move(req)); @@ -5300,13 +5594,6 @@ Result ClientImpl::Options(const std::string &path, return send_(std::move(req)); } -size_t ClientImpl::is_socket_open() const { - std::lock_guard guard(socket_mutex_); - return socket_.is_open(); -} - -socket_t ClientImpl::socket() const { return socket_.sock; } - void ClientImpl::stop() { std::lock_guard guard(socket_mutex_); @@ -5324,12 +5611,23 @@ void ClientImpl::stop() { return; } - // Otherwise, sitll holding the mutex, we can shut everything down ourselves + // Otherwise, still holding the mutex, we can shut everything down ourselves shutdown_ssl(socket_, true); shutdown_socket(socket_); close_socket(socket_); } +std::string ClientImpl::host() const { return host_; } + +int ClientImpl::port() const { return port_; } + +size_t ClientImpl::is_socket_open() const { + std::lock_guard guard(socket_mutex_); + return socket_.is_open(); +} + +socket_t ClientImpl::socket() const { return socket_.sock; } + void ClientImpl::set_connection_timeout(time_t sec, time_t usec) { connection_timeout_sec_ = sec; connection_timeout_usec_ = usec; @@ -5417,9 +5715,7 @@ void ClientImpl::set_proxy_digest_auth(const std::string &username, proxy_digest_auth_username_ = username; proxy_digest_auth_password_ = password; } -#endif -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT void ClientImpl::set_ca_cert_path(const std::string &ca_cert_file_path, const std::string &ca_cert_dir_path) { ca_cert_file_path_ = ca_cert_file_path; @@ -5431,9 +5727,34 @@ void ClientImpl::set_ca_cert_store(X509_STORE *ca_cert_store) { ca_cert_store_ = ca_cert_store; } } -#endif -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +X509_STORE *ClientImpl::create_ca_cert_store(const char *ca_cert, + std::size_t size) { + auto mem = BIO_new_mem_buf(ca_cert, static_cast(size)); + if (!mem) return nullptr; + + auto inf = PEM_X509_INFO_read_bio(mem, nullptr, nullptr, nullptr); + if (!inf) { + BIO_free_all(mem); + return nullptr; + } + + auto cts = X509_STORE_new(); + if (cts) { + for (auto i = 0; i < static_cast(sk_X509_INFO_num(inf)); i++) { + auto itmp = sk_X509_INFO_value(inf, i); + if (!itmp) { continue; } + + if (itmp->x509) { X509_STORE_add_cert(cts, itmp->x509); } + if (itmp->crl) { X509_STORE_add_crl(cts, itmp->crl); } + } + } + + sk_X509_INFO_pop_free(inf, X509_INFO_free); + BIO_free_all(mem); + return cts; +} + void ClientImpl::enable_server_certificate_verification(bool enabled) { server_certificate_verification_ = enabled; } @@ -5497,7 +5818,7 @@ bool ssl_connect_or_accept_nonblocking(socket_t sock, SSL *ssl, U ssl_connect_or_accept, time_t timeout_sec, time_t timeout_usec) { - int res = 0; + auto res = 0; while ((res = ssl_connect_or_accept(ssl)) != 1) { auto err = SSL_get_error(ssl, res); switch (err) { @@ -5578,7 +5899,7 @@ ssize_t SSLSocketStream::read(char *ptr, size_t size) { auto ret = SSL_read(ssl_, ptr, static_cast(size)); if (ret < 0) { auto err = SSL_get_error(ssl_, ret); - int n = 1000; + auto n = 1000; #ifdef _WIN32 while (--n >= 0 && (err == SSL_ERROR_WANT_READ || (err == SSL_ERROR_SYSCALL && @@ -5611,7 +5932,7 @@ ssize_t SSLSocketStream::write(const char *ptr, size_t size) { auto ret = SSL_write(ssl_, ptr, static_cast(handle_size)); if (ret < 0) { auto err = SSL_get_error(ssl_, ret); - int n = 1000; + auto n = 1000; #ifdef _WIN32 while (--n >= 0 && (err == SSL_ERROR_WANT_WRITE || (err == SSL_ERROR_SYSCALL && @@ -5666,8 +5987,9 @@ SSLServer::SSLServer(const char *cert_path, const char *private_key_path, // add default password callback before opening encrypted private key if (private_key_password != nullptr && (private_key_password[0] != '\0')) { - SSL_CTX_set_default_passwd_cb_userdata(ctx_, - (char *)private_key_password); + SSL_CTX_set_default_passwd_cb_userdata( + ctx_, + reinterpret_cast(const_cast(private_key_password))); } if (SSL_CTX_use_certificate_chain_file(ctx_, cert_path) != 1 || @@ -5737,7 +6059,7 @@ bool SSLServer::process_and_close_socket(socket_t sock) { }, [](SSL * /*ssl2*/) { return true; }); - bool ret = false; + auto ret = false; if (ssl) { ret = detail::process_server_socket_ssl( svr_sock_, ssl, sock, keep_alive_max_count_, keep_alive_timeout_sec_, @@ -5831,6 +6153,11 @@ void SSLClient::set_ca_cert_store(X509_STORE *ca_cert_store) { } } +void SSLClient::load_ca_cert_store(const char *ca_cert, + std::size_t size) { + set_ca_cert_store(ClientImpl::create_ca_cert_store(ca_cert, size)); +} + long SSLClient::get_openssl_verify_result() const { return verify_result_; } @@ -5915,11 +6242,16 @@ bool SSLClient::load_certs() { ret = false; } } else { + auto loaded = false; #ifdef _WIN32 - detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_)); -#else - SSL_CTX_set_default_verify_paths(ctx_); -#endif + loaded = + detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_)); +#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && defined(__APPLE__) +#if TARGET_OS_OSX + loaded = detail::load_system_certs_on_macos(SSL_CTX_get_cert_store(ctx_)); +#endif // TARGET_OS_OSX +#endif // _WIN32 + if (!loaded) { SSL_CTX_set_default_verify_paths(ctx_); } } }); @@ -5971,6 +6303,10 @@ bool SSLClient::initialize_ssl(Socket &socket, Error &error) { return true; }, [&](SSL *ssl2) { + // NOTE: With -Wold-style-cast, this can produce a warning, since + // SSL_set_tlsext_host_name is a macro (in OpenSSL), which contains + // an old style cast. Short of doing compiler specific pragma's + // here, we can't get rid of this warning. :'( SSL_set_tlsext_host_name(ssl2, host_.c_str()); return true; }); @@ -6064,15 +6400,16 @@ SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const { if (alt_names) { auto dsn_matched = false; - auto ip_mached = false; + auto ip_matched = false; auto count = sk_GENERAL_NAME_num(alt_names); for (decltype(count) i = 0; i < count && !dsn_matched; i++) { auto val = sk_GENERAL_NAME_value(alt_names, i); if (val->type == type) { - auto name = (const char *)ASN1_STRING_get0_data(val->d.ia5); - auto name_len = (size_t)ASN1_STRING_length(val->d.ia5); + auto name = + reinterpret_cast(ASN1_STRING_get0_data(val->d.ia5)); + auto name_len = static_cast(ASN1_STRING_length(val->d.ia5)); switch (type) { case GEN_DNS: dsn_matched = check_host_name(name, name_len); break; @@ -6080,17 +6417,18 @@ SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const { case GEN_IPADD: if (!memcmp(&addr6, name, addr_len) || !memcmp(&addr, name, addr_len)) { - ip_mached = true; + ip_matched = true; } break; } } } - if (dsn_matched || ip_mached) { ret = true; } + if (dsn_matched || ip_matched) { ret = true; } } - GENERAL_NAMES_free((STACK_OF(GENERAL_NAME) *)alt_names); + GENERAL_NAMES_free(const_cast( + reinterpret_cast(alt_names))); return ret; } @@ -6502,12 +6840,16 @@ bool Client::send(Request &req, Response &res, Error &error) { Result Client::send(const Request &req) { return cli_->send(req); } +void Client::stop() { cli_->stop(); } + +std::string Client::host() const { return cli_->host(); } + +int Client::port() const { return cli_->port(); } + size_t Client::is_socket_open() const { return cli_->is_socket_open(); } socket_t Client::socket() const { return cli_->socket(); } -void Client::stop() { cli_->stop(); } - void Client::set_hostname_addr_map(std::map addr_map) { cli_->set_hostname_addr_map(std::move(addr_map)); @@ -6591,7 +6933,9 @@ void Client::enable_server_certificate_verification(bool enabled) { } #endif -void Client::set_logger(Logger logger) { cli_->set_logger(logger); } +void Client::set_logger(Logger logger) { + cli_->set_logger(std::move(logger)); +} #ifdef CPPHTTPLIB_OPENSSL_SUPPORT void Client::set_ca_cert_path(const std::string &ca_cert_file_path, @@ -6607,6 +6951,10 @@ void Client::set_ca_cert_store(X509_STORE *ca_cert_store) { } } +void Client::load_ca_cert_store(const char *ca_cert, std::size_t size) { + set_ca_cert_store(cli_->create_ca_cert_store(ca_cert, size)); +} + long Client::get_openssl_verify_result() const { if (is_ssl_) { return static_cast(*cli_).get_openssl_verify_result(); diff --git a/source/http/httplib.h b/source/http/httplib.h index f75d315..ba1dddf 100644 --- a/source/http/httplib.h +++ b/source/http/httplib.h @@ -1,14 +1,14 @@ // // httplib.h // -// Copyright (c) 2022 Yuji Hirose. All rights reserved. +// Copyright (c) 2023 Yuji Hirose. All rights reserved. // MIT License // #ifndef CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_HTTPLIB_H -#define CPPHTTPLIB_VERSION "0.12.0" +#define CPPHTTPLIB_VERSION "0.13.1" /* * Configuration @@ -87,7 +87,7 @@ #endif #ifndef CPPHTTPLIB_RECV_BUFSIZ -#define CPPHTTPLIB_RECV_BUFSIZ size_t(4096u) +#define CPPHTTPLIB_RECV_BUFSIZ size_t(16384u) #endif #ifndef CPPHTTPLIB_COMPRESSION_BUFSIZ @@ -172,9 +172,15 @@ using socket_t = SOCKET; #else // not _WIN32 #include -#ifndef _AIX +#if !defined(_AIX) && !defined(__MVS__) #include #endif +#ifdef __MVS__ +#include +#ifndef NI_MAXHOST +#define NI_MAXHOST 1025 +#endif +#endif #include #include #include @@ -223,6 +229,9 @@ using socket_t = int; #include #include #include +#include +#include +#include #ifdef CPPHTTPLIB_OPENSSL_SUPPORT #ifdef _WIN32 @@ -239,7 +248,13 @@ using socket_t = int; #pragma comment(lib, "crypt32.lib") #pragma comment(lib, "cryptui.lib") #endif -#endif //_WIN32 +#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && defined(__APPLE__) +#include +#if TARGET_OS_OSX +#include +#include +#endif // TARGET_OS_OSX +#endif // _WIN32 #include #include @@ -308,6 +323,34 @@ struct ci { } }; +// This is based on +// "http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189". + +struct scope_exit { + explicit scope_exit(std::function &&f) + : exit_function(std::move(f)), execute_on_destruction{true} {} + + scope_exit(scope_exit &&rhs) + : exit_function(std::move(rhs.exit_function)), + execute_on_destruction{rhs.execute_on_destruction} { + rhs.release(); + } + + ~scope_exit() { + if (execute_on_destruction) { this->exit_function(); } + } + + void release() { this->execute_on_destruction = false; } + +private: + scope_exit(const scope_exit &) = delete; + void operator=(const scope_exit &) = delete; + scope_exit &operator=(scope_exit &&) = delete; + + std::function exit_function; + bool execute_on_destruction; +}; + } // namespace detail using Headers = std::multimap; @@ -340,6 +383,7 @@ public: std::function write; std::function done; + std::function done_with_trailer; std::ostream os; private: @@ -430,6 +474,7 @@ struct Request { MultipartFormDataMap files; Ranges ranges; Match matches; + std::unordered_map path_params; // for client ResponseHandler response_handler; @@ -623,6 +668,76 @@ using SocketOptions = std::function; void default_socket_options(socket_t sock); +namespace detail { + +class MatcherBase { +public: + virtual ~MatcherBase() = default; + + // Match request path and populate its matches and + virtual bool match(Request &request) const = 0; +}; + +/** + * Captures parameters in request path and stores them in Request::path_params + * + * Capture name is a substring of a pattern from : to /. + * The rest of the pattern is matched agains the request path directly + * Parameters are captured starting from the next character after + * the end of the last matched static pattern fragment until the next /. + * + * Example pattern: + * "/path/fragments/:capture/more/fragments/:second_capture" + * Static fragments: + * "/path/fragments/", "more/fragments/" + * + * Given the following request path: + * "/path/fragments/:1/more/fragments/:2" + * the resulting capture will be + * {{"capture", "1"}, {"second_capture", "2"}} + */ +class PathParamsMatcher : public MatcherBase { +public: + PathParamsMatcher(const std::string &pattern); + + bool match(Request &request) const override; + +private: + static constexpr char marker = ':'; + // Treat segment separators as the end of path parameter capture + // Does not need to handle query parameters as they are parsed before path + // matching + static constexpr char separator = '/'; + + // Contains static path fragments to match against, excluding the '/' after + // path params + // Fragments are separated by path params + std::vector static_fragments_; + // Stores the names of the path parameters to be used as keys in the + // Request::path_params map + std::vector param_names_; +}; + +/** + * Performs std::regex_match on request path + * and stores the result in Request::matches + * + * Note that regex match is performed directly on the whole request. + * This means that wildcard patterns may match multiple path segments with /: + * "/begin/(.*)/end" will match both "/begin/middle/end" and "/begin/1/2/end". + */ +class RegexMatcher : public MatcherBase { +public: + RegexMatcher(const std::string &pattern) : regex_(pattern) {} + + bool match(Request &request) const override; + +private: + std::regex regex_; +}; + +} // namespace detail + class Server { public: using Handler = std::function; @@ -708,6 +823,7 @@ public: bool listen(const std::string &host, int port, int socket_flags = 0); bool is_running() const; + void wait_until_ready() const; void stop(); std::function new_task_queue; @@ -717,7 +833,7 @@ protected: bool &connection_closed, const std::function &setup_request); - std::atomic svr_sock_; + std::atomic svr_sock_{INVALID_SOCKET}; size_t keep_alive_max_count_ = CPPHTTPLIB_KEEPALIVE_MAX_COUNT; time_t keep_alive_timeout_sec_ = CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND; time_t read_timeout_sec_ = CPPHTTPLIB_READ_TIMEOUT_SECOND; @@ -729,9 +845,14 @@ protected: size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH; private: - using Handlers = std::vector>; + using Handlers = + std::vector, Handler>>; using HandlersForContentReader = - std::vector>; + std::vector, + HandlerWithContentReader>>; + + static std::unique_ptr + make_matcher(const std::string &pattern); socket_t create_server_socket(const std::string &host, int port, int socket_flags, @@ -769,21 +890,23 @@ private: ContentReceiver multipart_receiver); bool read_content_core(Stream &strm, Request &req, Response &res, ContentReceiver receiver, - MultipartContentHeader mulitpart_header, + MultipartContentHeader multipart_header, ContentReceiver multipart_receiver); virtual bool process_and_close_socket(socket_t sock); + std::atomic is_running_{false}; + std::atomic done_{false}; + struct MountPointEntry { std::string mount_point; std::string base_dir; Headers headers; }; std::vector base_dirs_; - - std::atomic is_running_; std::map file_extension_and_mimetype_map_; Handler file_request_handler_; + Handlers get_handlers_; Handlers post_handlers_; HandlersForContentReader post_handlers_for_content_reader_; @@ -794,13 +917,15 @@ private: Handlers delete_handlers_; HandlersForContentReader delete_handlers_for_content_reader_; Handlers options_handlers_; + HandlerWithResponse error_handler_; ExceptionHandler exception_handler_; HandlerWithResponse pre_routing_handler_; Handler post_routing_handler_; - Logger logger_; Expect100ContinueHandler expect_100_continue_handler_; + Logger logger_; + int address_family_ = AF_UNSPEC; bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; SocketOptions socket_options_ = default_socket_options; @@ -823,6 +948,9 @@ enum class Error { UnsupportedMultipartBoundaryChars, Compression, ConnectionTimeout, + + // For internal use only + SSLPeerCouldBeClosed_, }; std::string to_string(const Error error); @@ -831,6 +959,7 @@ std::ostream &operator<<(std::ostream &os, const Error &obj); class Result { public: + Result() = default; Result(std::unique_ptr &&res, Error err, Headers &&request_headers = Headers{}) : res_(std::move(res)), err_(err), @@ -859,7 +988,7 @@ public: private: std::unique_ptr res_; - Error err_; + Error err_ = Error::Unknown; Headers request_headers_; }; @@ -1019,12 +1148,14 @@ public: bool send(Request &req, Response &res, Error &error); Result send(const Request &req); - size_t is_socket_open() const; - - socket_t socket() const; - void stop(); + std::string host() const; + int port() const; + + size_t is_socket_open() const; + socket_t socket() const; + void set_hostname_addr_map(std::map addr_map); void set_default_headers(Headers headers); @@ -1077,6 +1208,7 @@ public: void set_ca_cert_path(const std::string &ca_cert_file_path, const std::string &ca_cert_dir_path = std::string()); void set_ca_cert_store(X509_STORE *ca_cert_store); + X509_STORE *create_ca_cert_store(const char *ca_cert, std::size_t size); #endif #ifdef CPPHTTPLIB_OPENSSL_SUPPORT @@ -1095,8 +1227,6 @@ protected: bool is_open() const { return sock != INVALID_SOCKET; } }; - Result send_(Request &&req); - virtual bool create_and_connect_socket(Socket &socket, Error &error); // All of: @@ -1118,7 +1248,7 @@ protected: void copy_settings(const ClientImpl &rhs); - // Socket endoint information + // Socket endpoint information const std::string host_; const int port_; const std::string host_and_port_; @@ -1197,6 +1327,9 @@ protected: Logger logger_; private: + bool send_(Request &req, Response &res, Error &error); + Result send_(Request &&req); + socket_t create_client_socket(Error &error) const; bool read_response_line(Stream &strm, const Request &req, Response &res); bool write_request(Stream &strm, Request &req, bool close_connection, @@ -1390,12 +1523,14 @@ public: bool send(Request &req, Response &res, Error &error); Result send(const Request &req); - size_t is_socket_open() const; - - socket_t socket() const; - void stop(); + std::string host() const; + int port() const; + + size_t is_socket_open() const; + socket_t socket() const; + void set_hostname_addr_map(std::map addr_map); void set_default_headers(Headers headers); @@ -1456,6 +1591,7 @@ public: const std::string &ca_cert_dir_path = std::string()); void set_ca_cert_store(X509_STORE *ca_cert_store); + void load_ca_cert_store(const char *ca_cert, std::size_t size); long get_openssl_verify_result() const; @@ -1515,6 +1651,7 @@ public: bool is_valid() const override; void set_ca_cert_store(X509_STORE *ca_cert_store); + void load_ca_cert_store(const char *ca_cert, std::size_t size); long get_openssl_verify_result() const; @@ -1624,22 +1761,22 @@ inline ssize_t Stream::write_format(const char *fmt, const Args &...args) { inline void default_socket_options(socket_t sock) { int yes = 1; #ifdef _WIN32 - setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&yes), - sizeof(yes)); + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, + reinterpret_cast(&yes), sizeof(yes)); setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, - reinterpret_cast(&yes), sizeof(yes)); + reinterpret_cast(&yes), sizeof(yes)); #else #ifdef SO_REUSEPORT - setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast(&yes), - sizeof(yes)); + setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, + reinterpret_cast(&yes), sizeof(yes)); #else - setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&yes), - sizeof(yes)); + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, + reinterpret_cast(&yes), sizeof(yes)); #endif #endif - int const size = 1048576; - setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)); - setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size)); + //int const size = 1048576; + //setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)); + //setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size)); } template @@ -1791,6 +1928,9 @@ std::string params_to_query_str(const Params ¶ms); void parse_query_text(const std::string &s, Params ¶ms); +bool parse_multipart_boundary(const std::string &content_type, + std::string &boundary); + bool parse_range_header(const std::string &s, Ranges &ranges); int close_socket(socket_t sock); diff --git a/source/server/http_server.cpp b/source/server/http_server.cpp index 0e68872..9ebeb94 100644 --- a/source/server/http_server.cpp +++ b/source/server/http_server.cpp @@ -8,10 +8,14 @@ #include "windows.h" #include "lang.h" #include "system.h" +#include "dbglogger.h" #define SERVER_CERT_FILE "/app0/assets/certs/domain.crt" #define SERVER_PRIVATE_KEY_FILE "/app0/assets/certs/domain.key" #define SERVER_PRIVATE_KEY_PASSWORD "12345678" +#define SUCCESS_MSG "{ \"result\": { \"success\": true, \"error\": null } }" +#define FAILURE_MSG "{ \"result\": { \"success\": false, \"error\": \"%s\" } }" +#define SUCCESS_MSG_LEN 48 using namespace httplib; SSLServer *svr; @@ -76,6 +80,99 @@ namespace HttpServer return s; } + void failed(Response & res, int status, const std::string &msg) + { + res.status = status; + char response_msg[msg.length()+strlen(FAILURE_MSG)+2]; + snprintf(response_msg, sizeof(response_msg), "{ \"result\": { \"success\": false, \"error\": \"%s\" } }", msg.c_str()); + res.set_content(response_msg, strlen(response_msg), "application/json"); + return; + } + + void bad_request(Response & res, const std::string &msg) + { + failed(res, 200, msg); + return; + } + + void success(Response & res) + { + res.status = 200; + res.set_content(SUCCESS_MSG, SUCCESS_MSG_LEN, "application/json"); + return; + } + + int CopyOrMove(const DirEntry &src, const char *dest, bool isCopy) + { + int ret; + if (src.isDir) + { + int err; + std::vector entries = FS::ListDir(src.path, &err); + FS::MkDirs(dest); + for (int i = 0; i < entries.size(); i++) + { + int path_length = strlen(dest) + strlen(entries[i].name) + 2; + char *new_path = (char *)malloc(path_length); + snprintf(new_path, path_length, "%s%s%s", dest, FS::hasEndSlash(dest) ? "" : "/", entries[i].name); + + if (entries[i].isDir) + { + if (strcmp(entries[i].name, "..") == 0) + continue; + + FS::MkDirs(new_path); + ret = CopyOrMove(entries[i], new_path, isCopy); + if (ret <= 0) + { + free(new_path); + return ret; + } + } + else + { + if (isCopy) + { + ret = FS::Copy(entries[i].path, new_path); + } + else + { + ret = FS::Move(entries[i].path, new_path); + } + if (ret <= 0) + { + free(new_path); + return ret; + } + } + free(new_path); + } + if (!isCopy) + FS::RmRecursive(src.path); + } + else + { + int path_length = strlen(dest) + strlen(src.name) + 2; + char *new_path = (char *)malloc(path_length); + snprintf(new_path, path_length, "%s%s%s", dest, FS::hasEndSlash(dest) ? "" : "/", src.name); + if (isCopy) + { + ret = FS::Copy(src.path, new_path); + } + else + { + ret = FS::Move(src.path, new_path); + } + if (ret <= 0) + { + free(new_path); + return 0; + } + free(new_path); + } + return 1; + } + void *ServerThread(void *argp) { svr->Get("/", [&](const Request & req, Response & res) @@ -96,11 +193,15 @@ namespace HttpServer onlyFolders = true; if (path == nullptr) { - res.status = 400; - res.set_content("path parameter is missing", 25, "text/plain"); + bad_request(res, "Required path parameter missing"); return; } } + else + { + bad_request(res, "Invalid payload"); + return; + } int err; std::vector files = FS::ListDir(path, &err); @@ -113,7 +214,7 @@ namespace HttpServer json_object_object_add(new_file, "name", json_object_new_string(it->name)); json_object_object_add(new_file, "rights", json_object_new_string(it->isDir ? "drwxrwxrwx" : "rw-rw-rw-")); json_object_object_add(new_file, "date", json_object_new_string(it->display_date)); - json_object_object_add(new_file, "size", json_object_new_string(it->isDir ? "" : it->display_size)); + json_object_object_add(new_file, "size", json_object_new_string(it->isDir ? "" : std::to_string(it->file_size).c_str())); json_object_object_add(new_file, "type", json_object_new_string(it->isDir ? "dir" : "file")); json_object_array_add(json_files, new_file); } @@ -126,6 +227,360 @@ namespace HttpServer res.set_content(results_str, strlen(results_str), "application/json"); }); + svr->Post("/__local__/rename", [&](const Request & req, Response & res) + { + const char *item; + const char *newItemPath; + json_object *jobj = json_tokener_parse(req.body.c_str()); + if (jobj != nullptr) + { + item = json_object_get_string(json_object_object_get(jobj, "item")); + newItemPath = json_object_get_string(json_object_object_get(jobj, "newItemPath")); + if (item == nullptr || newItemPath == nullptr) + { + bad_request(res, "Required item or newItemPath parameter missing"); + return; + } + } + else + { + bad_request(res, "Invalid payload"); + return; + } + + FS::Rename(item, newItemPath); + success(res); + return; + }); + + svr->Post("/__local__/move", [&](const Request & req, Response & res) + { + const json_object *items; + const char *newPath; + json_object *jobj = json_tokener_parse(req.body.c_str()); + if (jobj != nullptr) + { + items = json_object_object_get(jobj, "items"); + newPath = json_object_get_string(json_object_object_get(jobj, "newPath")); + if (items == nullptr || newPath == nullptr) + { + bad_request(res, "Required items or newPath parameter missing"); + return; + } + } + else + { + bad_request(res, "Invalid payload"); + return; + } + + std::string failed_items; + size_t len = json_object_array_length(items); + for (size_t i=0; i < len; i++) + { + const char *item = json_object_get_string(json_object_array_get_idx(items, i)); + DirEntry entry; + std::string temp = std::string(item); + size_t slash_pos = temp.find_last_of("/"); + sprintf(entry.name, "%s", temp.substr(slash_pos+1).c_str()); + sprintf(entry.path, "%s", item); + entry.isDir = FS::IsFolder(item); + bool ret = CopyOrMove(entry, newPath, false); + if (!ret) + { + failed_items += std::string(item) + ","; + } + } + + if (failed_items.length() > 0) + { + std::string error_msg = std::string("One or more file(s) failed to move. ") + failed_items; + failed(res, 200, error_msg); + } + else + success(res); + }); + + svr->Post("/__local__/copy", [&](const Request & req, Response & res) + { + const json_object *items; + const char *newPath; + const char *singleFilename; + + json_object *jobj = json_tokener_parse(req.body.c_str()); + if (jobj != nullptr) + { + items = json_object_object_get(jobj, "items"); + newPath = json_object_get_string(json_object_object_get(jobj, "newPath")); + singleFilename = json_object_get_string(json_object_object_get(jobj, "singleFilename")); + + if (items == nullptr || newPath == nullptr) + { + bad_request(res, "Required items or newPath or singleFilename parameter missing"); + return; + } + } + else + { + bad_request(res, "Invalid payload"); + return; + } + + std::string failed_items; + if (singleFilename != nullptr) + { + const char *src = json_object_get_string(json_object_array_get_idx(items, 0)); + std::string dest = std::string(newPath) + "/" + singleFilename; + if (dest.compare(src) != 0 && !FS::Copy(src, dest)) + { + failed_items += src; + } + } + else + { + size_t len = json_object_array_length(items); + for (size_t i=0; i < len; i++) + { + const char *item = json_object_get_string(json_object_array_get_idx(items, i)); + DirEntry entry; + std::string temp = std::string(item); + size_t slash_pos = temp.find_last_of("/"); + sprintf(entry.name, "%s", temp.substr(slash_pos+1).c_str()); + sprintf(entry.path, "%s", item); + entry.isDir = FS::IsFolder(item); + bool ret = CopyOrMove(entry, newPath, true); + if (!ret) + { + failed_items += std::string(item) + ","; + } + } + } + + if (failed_items.length() > 0) + { + std::string error_msg = std::string("One or more file(s) failed to copy. ") + failed_items; + failed(res, 200, error_msg); + } + else + success(res); + }); + + svr->Post("/__local__/remove", [&](const Request & req, Response & res) + { + json_object *items; + json_object *jobj = json_tokener_parse(req.body.c_str()); + if (jobj != nullptr) + { + items = json_object_object_get(jobj, "items"); + if (items == nullptr) + { + bad_request(res, "Required items parameter missing"); + return; + } + } + else + { + bad_request(res, "Invalid payload"); + return; + } + + std::string failed_items; + size_t len = json_object_array_length(items); + for (size_t i=0; i < len; i++) + { + const char *item = json_object_get_string(json_object_array_get_idx(items, i)); + bool ret = FS::RmRecursive(item); + if (!ret) + { + failed_items += std::string(item) + ","; + } + } + + if (failed_items.length() > 0) + { + std::string error_msg = std::string("One or more file(s) failed to delete. ") + failed_items; + failed(res, 200, error_msg); + } + else + success(res); + }); + + svr->Post("/__local__/edit", [&](const Request & req, Response & res) + { + const char *item; + const char *content; + size_t content_len; + json_object *jobj = json_tokener_parse(req.body.c_str()); + if (jobj != nullptr) + { + item = json_object_get_string(json_object_object_get(jobj, "item")); + json_object *content_obj = json_object_object_get(jobj, "content"); + content = json_object_get_string(content_obj); + content_len = json_object_get_string_len(content_obj); + if (item == nullptr || content == nullptr) + { + bad_request(res, "Required item or content parameter missing"); + return; + } + } + else + { + bad_request(res, "Invalid payload"); + return; + } + + bool ret = FS::Save(item, content, content_len); + if (!ret) + { + failed(res, 200, "Failed to content to file."); + return; + } + + success(res); + }); + + svr->Post("/__local__/getContent", [&](const Request & req, Response & res) + { + const char *item; + json_object *jobj = json_tokener_parse(req.body.c_str()); + if (jobj != nullptr) + { + item = json_object_get_string(json_object_object_get(jobj, "item")); + if (item == nullptr) + { + bad_request(res, "Required item parameter missing"); + return; + } + } + else + { + bad_request(res, "Invalid payload"); + return; + } + + std::vector content = FS::Load(item); + json_object *result = json_object_new_object(); + json_object_object_add(result, "result", json_object_new_string(content.data())); + const char *result_str = json_object_to_json_string(result); + res.status = 200; + res.set_content(result_str, strlen(result_str), "application/json"); + }); + + svr->Post("/__local__/createFolder", [&](const Request & req, Response & res) + { + const char *newPath; + json_object *jobj = json_tokener_parse(req.body.c_str()); + if (jobj != nullptr) + { + newPath = json_object_get_string(json_object_object_get(jobj, "newPath")); + if (newPath == nullptr) + { + bad_request(res, "Required newPath parameter missing"); + return; + } + } + else + { + bad_request(res, "Invalid payload"); + return; + } + + FS::MkDirs(newPath); + success(res); + }); + + svr->Post("/__local__/permission", [&](const Request & req, Response & res) + { + failed(res, 200, "Operation not supported"); + }); + + svr->Post("/__local__/compress", [&](const Request & req, Response & res) + { + failed(res, 200, "Operation not supported"); + }); + + svr->Post("/__local__/extract", [&](const Request & req, Response & res) + { + failed(res, 200, "Operation not supported"); + }); + + svr->Post("/__local__/upload", [&](const Request &req, Response &res, const ContentReader &content_reader) + { + MultipartFormDataItems items; + std::string destination; + FILE *out = nullptr; + std::string new_file; + content_reader( + [&](const MultipartFormData &item) + { + items.push_back(item); + if (item.name != "destination") + { + new_file = destination + "/" + item.filename; + if (out != nullptr) + { + FS::Close(out); + } + out = FS::Create(new_file); + } + return true; + }, + [&](const char *data, size_t data_length) + { + items.back().content.append(data, data_length); + if (items.back().name == "destination") + { + destination = items.back().content; + } + else + { + dbglogger_log("data_length=%lld", data_length); + FS::Write(out, data, data_length); + } + return true; + }); + if (out != nullptr) + { + FS::Close(out); + } + success(res); + }); + + // Download multiple files as ZIP + svr->Get("/__local__/downloadMultiple", [&](const Request & req, Response & res) + { + const char *path; + json_object *jobj = json_tokener_parse(req.body.c_str()); + if (jobj != nullptr) + { + path = json_object_get_string(json_object_object_get(jobj, "path")); + if (path == nullptr) + { + failed(res, 200, "One or more file(s) failed to download"); + return; + } + } + else + { + bad_request(res, "Invalid payload"); + return; + } + }); + + // Download single file + svr->Get("/__local__/downloadFile", [&](const Request & req, Response & res) + { + std::string path = req.get_param_value("path", 0); + if (path.empty()) + { + bad_request(res, "Failed to download"); + return; + } + + dbglogger_log("path=%s", path.c_str()); + res.status = 200; + }); + svr->Get("/google_auth", [](const Request &req, Response &res) { std::string auth_code = req.get_param_value("code");