From 3bce736834f7d49f01bb799360fb587e9f3ceb1c Mon Sep 17 00:00:00 2001 From: Mike Nolan Date: Mon, 31 Aug 2026 20:57:18 -0500 Subject: [PATCH] Fix license compliance and fix things up, add reverse proxy, fix some security flaws with HttpUtils --- .gitea/workflows/tag.yaml | 65 +-- Doxyfile | 2 - apps/tsimplereverseproxy.cpp | 33 ++ apps/ttimezone.cpp | 9 +- changelog.md | 2 +- cmake/apps.cmake | 6 +- cmake/sources.cmake | 2 + .../Crypto/ClientTLSStream.hpp | 31 ++ include/TessesFramework/Crypto/Crypto.hpp | 25 +- include/TessesFramework/Date/Date.hpp | 372 ++++++++++++- .../TessesFramework/Filesystem/FSHelpers.hpp | 34 ++ include/TessesFramework/HiddenField.hpp | 4 +- .../TessesFramework/Http/ChangeableServer.hpp | 6 +- include/TessesFramework/Http/DomainServer.hpp | 41 ++ include/TessesFramework/Http/HttpClient.hpp | 16 +- .../TessesFramework/Http/HttpReverseProxy.hpp | 84 +++ include/TessesFramework/Http/HttpUtils.hpp | 65 ++- .../TessesFramework/Http/MountableServer.hpp | 2 +- include/TessesFramework/Http/RouteServer.hpp | 2 + .../TessesFramework/Streams/NetworkStream.hpp | 5 +- include/TessesFramework/Streams/Stream.hpp | 9 + include/TessesFramework/TessesFramework.hpp | 4 +- include/TessesFramework/Threading/Mutex.hpp | 31 ++ src/Crypto/MbedTLS/ClientTLSStream.cpp | 159 +++++- src/Date/Date.cpp | 44 +- src/Filesystem/VFS.cpp | 1 + src/Http/ChangeableServer.cpp | 17 +- src/Http/DomainServer.cpp | 40 ++ src/Http/FileServer.cpp | 2 +- src/Http/HttpClient.cpp | 56 +- src/Http/HttpReverseProxy.cpp | 525 ++++++++++++++++++ src/Http/HttpServer.cpp | 55 +- src/Http/HttpStream.cpp | 78 ++- src/Http/HttpUtils.cpp | 496 ++++++++++------- src/Http/MountableServer.cpp | 12 +- src/Http/RouteServer.cpp | 25 +- src/Platform/Process.cpp | 56 +- src/Streams/NetworkStream.cpp | 52 +- src/Streams/Stream.cpp | 18 + src/TF_Init.cpp | 16 +- src/Threading/Mutex.cpp | 109 +++- 41 files changed, 2227 insertions(+), 384 deletions(-) create mode 100644 apps/tsimplereverseproxy.cpp create mode 100644 include/TessesFramework/Http/DomainServer.hpp create mode 100644 include/TessesFramework/Http/HttpReverseProxy.hpp create mode 100644 src/Http/DomainServer.cpp create mode 100644 src/Http/HttpReverseProxy.cpp diff --git a/.gitea/workflows/tag.yaml b/.gitea/workflows/tag.yaml index b7f42b2..9d46c95 100644 --- a/.gitea/workflows/tag.yaml +++ b/.gitea/workflows/tag.yaml @@ -11,7 +11,7 @@ env: jobs: build-ppc: name: "🔨 Build for PowerPC" - runs-on: ubuntu-latest + runs-on: fw13-ubuntu-latest container: image: git.tesses.org/tesses50/linux-ppc:latest steps: @@ -24,7 +24,38 @@ jobs: prerelease: true files: |- artifacts/** + update-tap-and-build-win32: + name: "🔨 Build win32 and update the tap 🍺" + runs-on: fw13-ubuntu-latest + container: git.tesses.org/tesses50/global-containers-mingw:latest + steps: + - uses: actions/checkout@v4 + - run: | + mkdir artifacts + env -C Packaging/WinToolsIntel bash build.sh + - uses: akkuman/gitea-release-action@v1 + env: + NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18 + with: + prerelease: true + files: |- + artifacts/** + + - uses: actions/checkout@v4 + with: + ref: "master" + path: "tapdir" + repository: "tesses50/tesses-tap.git" + token: ${{ env.PACKAGE_AND_BREW }} + - run: | + cd tapdir + bash ../Packaging/edit-formula.sh + git config user.name "Tesses Gitea Bot" + git config user.email "noreply@tesses.net" + git add . + git commit -m "Push tessesframework=${{ env.VERSION }}" + git push build-packages: name: "🔨 Build for everything else" runs-on: fw13-ubuntu-latest @@ -61,34 +92,4 @@ jobs: files: |- artifacts/** - update-tap-and-build-win32: - name: "🔨 Build win32 and update the tap 🍺" - runs-on: global-container-mingw - steps: - - uses: actions/checkout@v4 - - run: | - mkdir artifacts - env -C Packaging/WinToolsIntel bash build.sh - - - uses: akkuman/gitea-release-action@v1 - env: - NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18 - with: - prerelease: true - files: |- - artifacts/** - - - uses: actions/checkout@v4 - with: - ref: "master" - path: "tapdir" - repository: "tesses50/tesses-tap.git" - token: ${{ env.PACKAGE_AND_BREW }} - - run: | - cd tapdir - bash ../Packaging/edit-formula.sh - git config user.name "Tesses Gitea Bot" - git config user.email "noreply@tesses.net" - git add . - git commit -m "Push tessesframework=${{ env.VERSION }}" - git push + \ No newline at end of file diff --git a/Doxyfile b/Doxyfile index 0899475..6405e45 100644 --- a/Doxyfile +++ b/Doxyfile @@ -1,8 +1,6 @@ GENERATE_HTML = YES -GENERATE_LATEX = YES HTML_OUTPUT = doxy_html -LATEX_OUTPUT = doxy_latex # optional but common RECURSIVE = YES diff --git a/apps/tsimplereverseproxy.cpp b/apps/tsimplereverseproxy.cpp new file mode 100644 index 0000000..9965c74 --- /dev/null +++ b/apps/tsimplereverseproxy.cpp @@ -0,0 +1,33 @@ +#include "TessesFramework/TessesFramework.hpp" +#include +using namespace Tesses::Framework; +using namespace Tesses::Framework::Http; +using namespace Tesses::Framework::Streams; +using namespace Tesses::Framework::TextStreams; +using namespace Tesses::Framework::Threading; + +int main(int argc, char **argv) { + TF_InitWithConsole(); + + uint16_t port = 4269; + + Args args(argc, argv); + for (auto &item : args.options) { + if (item.first == "port") + port = (uint16_t)std::stoul(item.second); + } + + if (args.positional.empty()) { + Console::WriteLine(args.filename + " [--port=] "); + return 1; + } + + Tesses::Framework::Http::HttpServer server( + port, std::make_shared(args.positional[0])); + server.StartAccepting(); + TF_RunEventLoop(); + std::cout << "Closing server" << std::endl; + TF_Quit(); + + return 0; +} \ No newline at end of file diff --git a/apps/ttimezone.cpp b/apps/ttimezone.cpp index 4c61382..28ffaea 100644 --- a/apps/ttimezone.cpp +++ b/apps/ttimezone.cpp @@ -1,7 +1,14 @@ #include int main(int argc, char **argv) { + using namespace Tesses::Framework; + using namespace Tesses::Framework::TextStreams; Tesses::Framework::TF_InitWithConsole(); - std::cout << Tesses::Framework::Date::GetTimeZone() << std::endl; + StdOut() << "TimeZone Standard Offset: " + << (int64_t)(Date::GetTimeZone() / 3600) << NewLine(); + StdOut() << (Date::TimeZoneSupportDST() ? "TimeZone Supports DST" + : "TimeZone Doesn't Support DST") + << NewLine(); + return 0; } \ No newline at end of file diff --git a/changelog.md b/changelog.md index b891e9f..86deb40 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,7 @@ # Changelog ## 0.0.7 -Fix license compliance and fix things up +Fix license compliance and fix things up, add reverse proxy, fix some security flaws with HttpUtils ## 0.0.6 Use my new toolchain, fix a bug on mac/freebsd diff --git a/cmake/apps.cmake b/cmake/apps.cmake index a5da3d6..8db6abd 100644 --- a/cmake/apps.cmake +++ b/cmake/apps.cmake @@ -66,7 +66,11 @@ target_link_libraries(tuuid PUBLIC tessesframework) install(TARGETS tuuid DESTINATION "${CMAKE_INSTALL_BINDIR}") add_executable(ttimezone apps/ttimezone.cpp) - target_link_libraries(ttimezone PUBLIC tessesframework) install(TARGETS ttimezone DESTINATION "${CMAKE_INSTALL_BINDIR}") + +add_executable(tsimplereverseproxy apps/tsimplereverseproxy.cpp) +target_link_libraries(tsimplereverseproxy PUBLIC tessesframework) +install(TARGETS tsimplereverseproxy DESTINATION "${CMAKE_INSTALL_BINDIR}") + endif() \ No newline at end of file diff --git a/cmake/sources.cmake b/cmake/sources.cmake index 2587293..2c4b761 100644 --- a/cmake/sources.cmake +++ b/cmake/sources.cmake @@ -5,12 +5,14 @@ src/Date/Date.cpp src/Http/FileServer.cpp src/Http/MountableServer.cpp src/Http/RouteServer.cpp +src/Http/DomainServer.cpp src/Http/CallbackServer.cpp src/Http/HttpServer.cpp src/Http/HttpUtils.cpp src/Http/HttpClient.cpp src/Http/HttpStream.cpp src/Http/ContentDisposition.cpp +src/Http/HttpReverseProxy.cpp src/Http/WebSocket.cpp src/Http/ChangeableServer.cpp src/Http/BasicAuthServer.cpp diff --git a/include/TessesFramework/Crypto/ClientTLSStream.hpp b/include/TessesFramework/Crypto/ClientTLSStream.hpp index 6535f86..c668189 100644 --- a/include/TessesFramework/Crypto/ClientTLSStream.hpp +++ b/include/TessesFramework/Crypto/ClientTLSStream.hpp @@ -21,6 +21,7 @@ #pragma once #include "../Streams/Stream.hpp" +#include "Crypto.hpp" namespace Tesses::Framework::Crypto { /** @@ -59,6 +60,32 @@ class ClientTLSStream : public Tesses::Framework::Streams::Stream { ClientTLSStream( std::shared_ptr innerStream, bool verify, std::string domain, std::string cert); + /** + * @brief Construct a new Client TLS Stream object, with mTLS + * + * @param innerStream the underlying encrypted in transit stream + * @param verify do we verify the certificate + * @param domain the domain name + * @param keyStore the keystore for mTLS + */ + ClientTLSStream( + std::shared_ptr innerStream, + bool verify, std::string domain, CertificateKeyStore keyStore); + /** + * @brief Construct a new Client TLS Stream object with an alternative + * certificate chain (for server with self signed certificates) for mTLS + * + * @param innerStream the underlying encrypted in transit stream + * @param verify do we verify the certificate + * @param domain the domain name + * @param cert the actual certificate + * @param keyStore the keystore for mTLS + */ + ClientTLSStream( + std::shared_ptr innerStream, + bool verify, std::string domain, std::string cert, + CertificateKeyStore keyStore); + /** * @brief Read from the stream * @@ -96,6 +123,10 @@ class ClientTLSStream : public Tesses::Framework::Streams::Stream { * @return false no */ bool EndOfStream(); + + void Shutdown(Tesses::Framework::Streams::StreamShutdownMode sdm); + void SetSendTimeout(uint64_t seconds); + void SetRecvTimeout(uint64_t seconds); ~ClientTLSStream(); }; diff --git a/include/TessesFramework/Crypto/Crypto.hpp b/include/TessesFramework/Crypto/Crypto.hpp index 20dd6fa..d40e13f 100644 --- a/include/TessesFramework/Crypto/Crypto.hpp +++ b/include/TessesFramework/Crypto/Crypto.hpp @@ -316,6 +316,29 @@ typedef enum { */ bool PBKDF2(std::vector &output, std::string pass, std::vector &salt, long itterations, ShaVersion version); - +/** + * @brief Get secure random bytes + * + * @param output The buffer to write random bytes to + * @param personal_str Some string to ensure the rng is unique (for mbedtls at + * least) + * @return true successfully generated the bytes + * @return false we failed to generate the bytes + */ bool RandomBytes(std::vector &output, std::string personal_str); + +struct CertificateKeyStore { + CertificateKeyStore() = default; + CertificateKeyStore(std::string certificate, std::string key, + std::optional chain = std::nullopt, + std::string password = "") + : certificate(certificate), key(key), chain(chain), password(password) { + } + + std::string certificate; + std::string key; + std::optional chain; + std::string password; +}; + } // namespace Tesses::Framework::Crypto \ No newline at end of file diff --git a/include/TessesFramework/Date/Date.hpp b/include/TessesFramework/Date/Date.hpp index ac55270..2f054b8 100644 --- a/include/TessesFramework/Date/Date.hpp +++ b/include/TessesFramework/Date/Date.hpp @@ -26,9 +26,23 @@ namespace Tesses::Framework::Date { +/** + * @brief Get the time zone in seconds from UTC + * + * @return int the utc distance in seconds, west + */ int GetTimeZone(); +/** + * @brief Does the timezone support daylight savings + * + * @return true yes + * @return false no + */ bool TimeZoneSupportDST(); - +/** + * @brief Stores a DateTime + * + */ class DateTime { int year = 1970; int month = 1; @@ -41,83 +55,413 @@ class DateTime { void FromEpochNoConvert(int64_t gmt); public: + /** + * @brief Construct a new Date Time object with Jan 1, 1970 at 12:00AM UTC + * + */ DateTime(); + /** + * @brief Construct a new Date Time object + * + * @param year the year + * @param month the month (1-12) + * @param day the day (1-31) + * @param hour the hour (0-23) + * @param minute the minute (0-59) + * @param seconds the second (0-59) + * @param isLocal true: your timezone, false: utc + */ DateTime(int year, int month, int day, int hour, int minute, int seconds, bool isLocal = true); + /** + * @brief Construct a new DateTime object + * + * @param epoch seconds since Jan 1, 1970 at 12:00AM UTC + */ DateTime(int64_t epoch); + /** + * @brief Get the year + * + * @return int ex 1992 means the year is 1992 + */ int Year() const; + /** + * @brief Get the month (1-12) + * + * @return int ex 8 means august + */ int Month() const; + /** + * @brief Get the day (1-31) + * + * @return int ex 20 means the day is the 20th + */ int Day() const; + /** + * @brief Get the hour (0-23) + * + * @return int ex 12 means noon, 0 means midnight, 18 means 6 PM + */ int Hour() const; + /** + * @brief Get the minute (0-59) + * + * @return int ex 15 means quarter after the hour + */ int Minute() const; + /** + * @brief Get the second (0-59) + * + * @return int the seconds value + */ int Second() const; + /** + * @brief Get the day of week (0-6) + * + * @retval 0 Sunday + * @retval 1 Monday + * @retval 2 Tuesday + * @retval 3 Wednesday + * @retval 4 Thursday + * @retval 5 Friday + * @retval 6 Saturday + */ int DayOfWeek() const; + /** + * @brief Is local timezone + * + * @return true yes + * @return false no + */ bool IsLocal() const; + /** + * @brief Get the time as epoch + * + * @return int64_t seconds since Jan 1, 1970 at 12:00AM UTC + */ int64_t ToEpoch() const; + /** + * @brief Convert this time to local time + * + * @return DateTime this time in local time + */ DateTime ToLocal() const; + /** + * @brief Convert this time to utc time + * + * @return DateTime this time in UTC time + */ DateTime ToUTC() const; + /** + * @brief Set this time to local + * + */ void SetToLocal(); + /** + * @brief Set this time to UTC + * + */ void SetToUTC(); + /** + * @brief Set the current year + * + * @param y year + */ void SetYear(int y); + /** + * @brief Set the current month (1-12) + * + * @param m month + */ void SetMonth(int m); + /** + * @brief Set the current day (1-31) + * + * @param d day + */ void SetDay(int d); + /** + * @brief Set the current hour (0-23) + * + * @param h hour + */ void SetHour(int h); + /** + * @brief Set the current minute (0-59) + * + * @param m minute + */ void SetMinute(int m); + /** + * @brief Set the current second (0-59) + * + * @param s second + */ void SetSecond(int s); + /** + * @brief Set whether this time is local or utc + * + * @param local true set this time to localtime or false set this time to + * utc + */ void SetLocal(bool local); + /** + * @brief Set the seconds since Jan 1, 1970 at 12:00AM UTC + * + * @param epoch seconds since Jan 1, 1970 at 12:00AM UTC + */ void Set(int64_t epoch); + /** + * @brief Set the time of this DateTime object + * + * @param year the year + * @param month the month (1-12) + * @param day the day (1-31) + * @param hour the hour (0-23) + * @param minute the minute (0-59) + * @param seconds the second (0-59) + * @param isLocal true: your timezone, false: utc + */ void Set(int year, int month, int day, int hour, int minute, int seconds, bool isLocal = true); - + /** + * @brief Set this DateTime to right now in your timezone + * + */ void SetToNow(); + /** + * @brief Set this DateTime to right now in utc + * + */ void SetToNowUTC(); + /** + * @brief Construct a datetime from right now in your timezone + * + * @return DateTime the current time object + */ static DateTime Now(); + /** + * @brief Construct a datetime from right now in your timezone + * + * @return DateTime the current time object + */ static DateTime NowUTC(); - + /** + * @brief Convert date to string same as ToString("%Y/%m/%d %H:%M:%S") + * + * @return std::string the date as string + */ std::string ToString() const; + /** + * @brief Convert date to string with your own fmt + * + * @param fmt the format, see: + * https://git.tesses.org/tesses50/tessesframework/wiki/DateTime_ToString_Formating + * for more details + * @return std::string the date as a string based on fmt + */ std::string ToString(std::string fmt) const; - + /** + * @brief Format as IMF-fixdate (RFC 9110 §5.6.7) + * + * Example: "Tue, 01 Sep 2026 18:07:05 GMT" + */ std::string ToHttpDate() const; - static bool TryParseHttpDate(std::string txt, DateTime &dt); + /** + * @brief Tries to parse an IMF-fixdate (RFC 9110 §5.6.7) date into a + * DateTime + * + * @param txt example "Tue, 01 Sep 2026 18:07:05 GMT" + * @param dt a reference to a datetime + * @return true we did parse the date correctly + * @return false we didn't parse the date correctly + */ + static bool TryParseHttpDate(std::string_view txt, DateTime &dt); + /** + * @brief Tries to parse an IMF-fixdate (RFC 9110 §5.6.7) date into the + * DateTime + * + * @param txt example "Tue, 01 Sep 2026 18:07:05 GMT" + * @return true we did parse the date correctly + * @return false we didn't parse the date correctly + */ + bool TryParseHttpDate(std::string_view txt); }; +/** + * @brief Stores a time offset in seconds + * + */ class TimeSpan { int64_t totalSeconds; public: + /** + * @brief Construct a new TimeSpan with 0 seconds + * + */ TimeSpan(); + /** + * @brief Construct a new TimeSpan object with totalSeconds + * + */ TimeSpan(int64_t totalSeconds); + /** + * @brief Construct a new TimeSpan object with hours, minutes and seconds + * + */ TimeSpan(int hours, int minutes, int seconds); + /** + * @brief Construct a new TimeSpan object with days, hours, minutes and + * seconds + * + */ TimeSpan(int days, int hours, int minutes, int seconds); - + /** + * @brief Set the timespan with new days, hours, minutes and seconds + * + */ void Set(int days, int hours, int minutes, int seconds); + /** + * @brief Set the timespan with new hours, minutes and seconds + * + */ void Set(int hours, int minutes, int seconds); - + /** + * @brief Set the days component (hours, minutes, seconds unchanged) + */ void SetDays(int d); + /** + * @brief Set the hours component (days, minutes, seconds unchanged) + */ void SetHours(int h); + /** + * @brief Set the minutes component (days, hours, seconds unchanged) + */ void SetMinutes(int m); + /** + * @brief Set the seconds component (days, hours, minutes unchanged) + */ void SetSeconds(int s); - + /** + * @brief Get the days of the timespan + * + */ int Days() const; + /** + * @brief Get the hours of the timespan + * + */ int Hours() const; + /** + * @brief Get the minutes of the timespan + * + */ int Minutes() const; + /** + * @brief Get the seconds of the timespan + * + */ int Seconds() const; - + /** + * @brief Get the total seconds of the timespan + * + */ int64_t TotalSeconds() const; + /** + * @brief Get the total minutes of the timespan + * + */ int64_t TotalMinutes() const; + /** + * @brief Get the total hours of the timespan + * + */ int64_t TotalHours() const; - + /** + * @brief Set the total seconds of the timespan, removing any old value + * + */ void SetTotalSeconds(int64_t totalSeconds); + /** + * @brief Set the total minutes of the timespan, removing any old value + * + */ void SetTotalMinutes(int64_t totalMinutes); + /** + * @brief Set the total hours of the timespan, removing any old value + * + */ void SetTotalHours(int64_t totalHours); - + /** + * @brief Set the total days of the timespan, removing any old value + * + */ + void SetTotalDays(int64_t totalHours); + /** + * @brief Add seconds to the timespan + * + */ void AddSeconds(int64_t seconds); + /** + * @brief Add minutes to the timespan + * + */ void AddMinutes(int64_t minutes); + /** + * @brief Add hours to the timespan + * + */ void AddHours(int64_t hours); + /** + * @brief Add days to the timespan + * + */ void AddDays(int64_t days); - + /** + * @brief Converts the timespan to string + * + * @param slim If true, omits leading zeros (e.g. "1:00", "10:00", + * "1:00:00"). If false, always zero-pads (e.g. "00:00:00") unless there are + * days. + */ std::string ToString(bool slim = true) const; - - static bool TryParse(std::string text, TimeSpan &span); + /** + * @brief Try to parse a TimeSpan from a string + * + * @param text the string to parse + * @param span receives the parsed TimeSpan on success + * @return true we parsed successfully + * @return false we failed to parse + */ + static bool TryParse(std::string_view text, TimeSpan &span); + /** + * @brief Try to parse this timespan from string + * + * @param text the string to parse + * @return true we parsed successfully + * @return false we failed to parse + */ + bool TryParse(std::string_view text); + /** + * @brief Create a timespan with seconds, same as ctor + * + */ + static TimeSpan FromSeconds(int64_t seconds); + /** + * @brief Create a timespan with minutes + * + */ + static TimeSpan FromMinutes(int64_t minutes); + /** + * @brief Create a timespan with hours + * + */ + static TimeSpan FromHours(int64_t hours); + /** + * @brief Create a timespan with days + * + */ + static TimeSpan FromDays(int64_t days); }; inline DateTime operator+(const DateTime &dt, const TimeSpan &ts) { diff --git a/include/TessesFramework/Filesystem/FSHelpers.hpp b/include/TessesFramework/Filesystem/FSHelpers.hpp index df44934..b4ddf7d 100644 --- a/include/TessesFramework/Filesystem/FSHelpers.hpp +++ b/include/TessesFramework/Filesystem/FSHelpers.hpp @@ -24,13 +24,47 @@ #include "VFSFix.hpp" namespace Tesses::Framework::Filesystem::Helpers { +/** @brief Read all of the text from a file + * @param vfs the VFS you want to use + * @param path the path to the file in the VFS + * @param text the file's contents + */ void ReadAllText(std::shared_ptr vfs, VFSPath path, std::string &text); +/** + * @brief Read all of the lines from a file + * + * @param vfs the VFS you want to use + * @param path the path to the file in the VFS + * @param lines the file's lines + */ void ReadAllLines(std::shared_ptr vfs, VFSPath path, std::vector &lines); +/** + * @brief Read all of the bytes from a file + * + * @param vfs the VFS you want to use + * @param path the path to the file in the VFS + * @param array the file's contents + */ void ReadAllBytes(std::shared_ptr vfs, VFSPath path, std::vector &array); +/** + * @brief Read all of the text from a file + * + * @param vfs the VFS you want to use + * @param path the path to the file in the VFS + * @return std::string the file's contents + */ std::string ReadAllText(std::shared_ptr vfs, VFSPath path); +/** + * @brief Read all of the lines from a file + * + * @param vfs the VFS you want to use + * @param path the path to the file in the VFS + * @return std::vector the file's lines + */ std::vector ReadAllLines(std::shared_ptr vfs, VFSPath path); + std::vector ReadAllBytes(std::shared_ptr vfs, VFSPath path); void WriteAllText(std::shared_ptr vfs, VFSPath path, const std::string &text); diff --git a/include/TessesFramework/HiddenField.hpp b/include/TessesFramework/HiddenField.hpp index 53cb9c6..d257bbe 100644 --- a/include/TessesFramework/HiddenField.hpp +++ b/include/TessesFramework/HiddenField.hpp @@ -38,8 +38,8 @@ class HiddenField { HiddenField(HiddenFieldData *data); void SetField(HiddenFieldData *data); template T GetField() { return dynamic_cast(ptr); } - template T *AllocField() { - auto v = new T(); + template T *AllocField(TArgs &&...args) { + auto v = new T(std::forward(args)...); SetField(v); return v; } diff --git a/include/TessesFramework/Http/ChangeableServer.hpp b/include/TessesFramework/Http/ChangeableServer.hpp index 4d413be..7c4e02f 100644 --- a/include/TessesFramework/Http/ChangeableServer.hpp +++ b/include/TessesFramework/Http/ChangeableServer.hpp @@ -24,11 +24,15 @@ namespace Tesses::Framework::Http { class ChangeableServer { + std::shared_ptr server; + Tesses::Framework::Threading::Mutex mtx; + public: ChangeableServer(); ChangeableServer(std::shared_ptr original); - std::shared_ptr server; bool Handle(ServerContext &ctx); + void SetServer(std::shared_ptr server); + std::shared_ptr GetServer(); ~ChangeableServer(); }; } // namespace Tesses::Framework::Http \ No newline at end of file diff --git a/include/TessesFramework/Http/DomainServer.hpp b/include/TessesFramework/Http/DomainServer.hpp new file mode 100644 index 0000000..d40291d --- /dev/null +++ b/include/TessesFramework/Http/DomainServer.hpp @@ -0,0 +1,41 @@ +/* + TessesFramework a library to make C++ easier for me, used in CrossLang: + https://git.tesses.org/tesses50/crosslang + + Copyright (C) 2026 Mike Nolan + SPDX-License-Identifier: GPL-3.0-or-later WITH TessesFramework-Exception-1.0 + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#pragma once +#include "../Filesystem/VFS.hpp" +#include "../Filesystem/VFSFix.hpp" +#include "HttpServer.hpp" + +namespace Tesses::Framework::Http { +class DomainServer : public IHttpServer { + std::shared_ptr root; + std::map> servers; + Tesses::Framework::Threading::Mutex mtx; + + public: + DomainServer(); + DomainServer(std::shared_ptr root); + void Set(std::string domain, std::shared_ptr server); + void Unset(std::string domain); + void Clear(); + bool Handle(ServerContext &ctx); +}; +} // namespace Tesses::Framework::Http \ No newline at end of file diff --git a/include/TessesFramework/Http/HttpClient.hpp b/include/TessesFramework/Http/HttpClient.hpp index e417742..1db95ec 100644 --- a/include/TessesFramework/Http/HttpClient.hpp +++ b/include/TessesFramework/Http/HttpClient.hpp @@ -20,6 +20,7 @@ */ #pragma once +#include "../Crypto/Crypto.hpp" #include "../Streams/Stream.hpp" #include "HttpUtils.hpp" // clang-format off @@ -66,6 +67,7 @@ class HttpRequest { public: HttpRequest(); std::string trusted_root_cert_bundle; + std::optional mTLS_keyStore; bool ignoreSSLErrors; bool followRedirects; @@ -73,15 +75,17 @@ class HttpRequest { std::string url; std::string unixSocket; HttpDictionary requestHeaders; - HttpRequestBody *body; + std::shared_ptr body; static std::shared_ptr - EstablishConnection(Uri uri, bool ignoreSSLErrors, - std::string trusted_root_cert_bundle); + EstablishConnection( + Uri uri, bool ignoreSSLErrors, std::string trusted_root_cert_bundle, + std::optional mTLS_keyStore); static std::shared_ptr - EstablishUnixPathConnection(std::string unixPath, Uri uri, - bool ignoreSSLErrors, - std::string trusted_root_cert_bundle); + EstablishUnixPathConnection( + std::string unixPath, Uri uri, bool ignoreSSLErrors, + std::string trusted_root_cert_bundle, + std::optional mTLS_keyStore); void SendRequest(std::shared_ptr strm); }; diff --git a/include/TessesFramework/Http/HttpReverseProxy.hpp b/include/TessesFramework/Http/HttpReverseProxy.hpp new file mode 100644 index 0000000..4a59c87 --- /dev/null +++ b/include/TessesFramework/Http/HttpReverseProxy.hpp @@ -0,0 +1,84 @@ +/* + TessesFramework a library to make C++ easier for me, used in CrossLang: + https://git.tesses.org/tesses50/crosslang + + Copyright (C) 2026 Mike Nolan + SPDX-License-Identifier: GPL-3.0-or-later WITH TessesFramework-Exception-1.0 + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#pragma once +#include "HttpClient.hpp" +#include "HttpServer.hpp" +#include + +namespace Tesses::Framework::Http { +enum class ReverseProxyAction { Continue, Handled, Unhandled }; +class ReverseProxyConnectionBuilder { + public: + ReverseProxyConnectionBuilder(const ReverseProxyConnectionBuilder &b) = + delete; + ReverseProxyConnectionBuilder & + operator=(const ReverseProxyConnectionBuilder &b) = delete; + ReverseProxyConnectionBuilder(ReverseProxyConnectionBuilder &&b) = delete; + ReverseProxyConnectionBuilder & + operator=(ReverseProxyConnectionBuilder &&b) = delete; + + ReverseProxyConnectionBuilder(ServerContext &ctx, + bool essentalheaders = true); + ReverseProxyConnectionBuilder &WithHeadersFromRequest(); + ReverseProxyConnectionBuilder &WithHeader(std::string key, + std::string value); + ReverseProxyConnectionBuilder &SetHeader(std::string key, + std::string value); + ReverseProxyConnectionBuilder &WithoutHeader(std::string key); + ReverseProxyConnectionBuilder &WithUrl(std::string url); + ReverseProxyConnectionBuilder &WithResponseCallback( + std::function rc); + ReverseProxyConnectionBuilder & + WithWhitelistedUpgrade(std::string protocol = "websocket"); + bool Handle(); + + private: + ServerContext &m_ctx; + std::optional m_url; + HttpDictionary m_reqheaders; + std::unordered_set m_whitelistedupgrades; + std::function m_rc; + bool m_fail = false; +}; + +class ReverseProxyServer : public IHttpServer { + private: + std::string url; + std::function + request_callback; + std::function + response_callback; + bool essentialheaders; + + public: + ReverseProxyServer( + std::string url, + std::function + request_callback = nullptr, + std::function + response_callback = nullptr, + bool essentialheaders = true); + bool Handle(ServerContext &ctx); +}; +} // namespace Tesses::Framework::Http \ No newline at end of file diff --git a/include/TessesFramework/Http/HttpUtils.hpp b/include/TessesFramework/Http/HttpUtils.hpp index e0bd151..304b907 100644 --- a/include/TessesFramework/Http/HttpUtils.hpp +++ b/include/TessesFramework/Http/HttpUtils.hpp @@ -22,6 +22,9 @@ #pragma once #include "../Common.hpp" #include "../Date/Date.hpp" + +#include "../Filesystem/VFS.hpp" +#include "../Filesystem/VFSFix.hpp" #include namespace Tesses::Framework::Http { @@ -91,9 +94,7 @@ typedef enum StatusCode { } StatusCode; struct CaseInsensitiveLess { - CaseInsensitiveLess(const CaseInsensitiveLess &str); - CaseInsensitiveLess(); - CaseInsensitiveLess *offset; + explicit CaseInsensitiveLess(bool caseSensitive); bool caseSensitive; bool operator()(const std::string &s1, const std::string &s2) const; }; @@ -136,7 +137,17 @@ class HttpDictionary { bool GetFirstBoolean(std::string key); + bool TryGetOnlyOne(std::string key, std::string &value); + + bool TryGetOnlyOneInt(std::string key, int64_t &value); + + bool TryGetOnlyOneDouble(std::string key, double &value); + bool TryGetOnlyOneDate(std::string key, Date::DateTime &value); + + bool TryGetOnlyOneBoolean(std::string key, bool &value); + bool AnyEquals(std::string key, std::string value); + bool AnyEqualsCSV(std::string key, std::string value); }; class Uri { @@ -169,30 +180,40 @@ class HttpUtils { bool isUppercase); static void BytesToHex(std::string &text, const std::vector &data, bool isUppercase); - static std::vector HexToBytes(const std::string &text); - static void HexToBytes(std::vector &data, const std::string &text); - static std::string MimeType(std::filesystem::path p); + static std::vector HexToBytes(std::string_view text); + static void HexToBytes(std::vector &data, std::string_view text); + static std::string GetMimeType(const std::string &ext); + static std::string GetMimeTypePath(const Filesystem::VFSPath &pathWithExt); + static void AddMimeType(const std::string &ext, const std::string &mime); + static void AddMimeTypePath(const Filesystem::VFSPath &pathWithExt, + const std::string &mime); static bool Invalid(char c); - static std::string Sanitise(std::string text); - static void QueryParamsDecode(HttpDictionary &dict, std::string query); - static std::string Join(std::string joinStr, std::vector ents); + static std::string Sanitise(std::string_view text); + static void QueryParamsDecode(HttpDictionary &dict, std::string_view query); + static std::string Join(std::string_view joinStr, + std::vector ents); static std::string QueryParamsEncode(HttpDictionary &dict); - static std::string UrlDecode(std::string v); - static std::string UrlEncode(std::string v); - static std::string UrlPathDecode(std::string v); - static std::string UrlPathEncode(std::string v, bool ignoreSpace = false); - static std::string HtmlEncode(std::string v); - static std::string HtmlP(std::string text); - static std::string HtmlDecodeOnlyEntityNumber(std::string v); + static std::string UrlDecode(std::string_view v); + static std::string UrlEncode(std::string_view v); + static std::string UrlPathDecode(std::string_view v); + static std::string UrlPathEncode(std::string_view v, + bool ignoreSpace = false); + static std::string HtmlEncode(std::string_view v); + static std::string HtmlP(std::string_view text); + static void SplitString(std::vector &out, + std::string_view text, std::string_view delimiter, + std::size_t maxCnt = std::string::npos); static std::vector - SplitString(std::string text, std::string delimiter, + SplitString(std::string_view text, std::string_view delimiter, std::size_t maxCnt = std::string::npos); - static std::string Replace(std::string str, std::string find, - std::string replace); + static std::string Replace(std::string_view str, std::string_view find, + std::string_view replace); static std::string StatusCodeString(StatusCode code); - static std::string ToLower(std::string str); - static std::string ToUpper(std::string str); - static std::string LeftPad(std::string text, int count, char c); + static std::string ToLower(std::string_view str); + static std::string ToUpper(std::string_view str); + static std::string LeftPad(std::string_view text, int count, char c); + static bool CaseInsensitiveCompare(std::string_view left, + std::string_view right); }; } // namespace Tesses::Framework::Http \ No newline at end of file diff --git a/include/TessesFramework/Http/MountableServer.hpp b/include/TessesFramework/Http/MountableServer.hpp index 714b4f5..ef48ee2 100644 --- a/include/TessesFramework/Http/MountableServer.hpp +++ b/include/TessesFramework/Http/MountableServer.hpp @@ -32,6 +32,7 @@ class MountableServer : public IHttpServer { Filesystem::VFSPath offsetPath); bool StartsWith(Filesystem::VFSPath fullPath, Filesystem::VFSPath offsetPath); + Tesses::Framework::Threading::Mutex mtx; public: MountableServer(); @@ -39,6 +40,5 @@ class MountableServer : public IHttpServer { void Mount(std::string path, std::shared_ptr server); void Unmount(std::string path); bool Handle(ServerContext &ctx); - ~MountableServer(); }; } // namespace Tesses::Framework::Http \ No newline at end of file diff --git a/include/TessesFramework/Http/RouteServer.hpp b/include/TessesFramework/Http/RouteServer.hpp index b50813e..65e47e9 100644 --- a/include/TessesFramework/Http/RouteServer.hpp +++ b/include/TessesFramework/Http/RouteServer.hpp @@ -42,6 +42,7 @@ class RouteServer : public IHttpServer { }; std::vector routes; std::shared_ptr root; + Tesses::Framework::Threading::Mutex mtx; public: RouteServer() = default; @@ -58,5 +59,6 @@ class RouteServer : public IHttpServer { void Add(std::string method, std::string pattern, ServerRequestHandler handler); bool Handle(ServerContext &ctx); + void Clear(); }; } // namespace Tesses::Framework::Http \ No newline at end of file diff --git a/include/TessesFramework/Streams/NetworkStream.hpp b/include/TessesFramework/Streams/NetworkStream.hpp index 48c2775..e222f30 100644 --- a/include/TessesFramework/Streams/NetworkStream.hpp +++ b/include/TessesFramework/Streams/NetworkStream.hpp @@ -53,7 +53,7 @@ class NetworkStream : public Stream { int32_t sock; bool owns; bool success; - bool endOfStream; + std::atomic endOfStream; public: bool DataAvailable(int timeout = 0); @@ -85,6 +85,9 @@ class NetworkStream : public Stream { ~NetworkStream(); void SetNoDelay(bool noDelay); + void Shutdown(StreamShutdownMode mode); + void SetSendTimeout(uint64_t seconds); + void SetRecvTimeout(uint64_t seconds); void Close(); }; } // namespace Tesses::Framework::Streams diff --git a/include/TessesFramework/Streams/Stream.hpp b/include/TessesFramework/Streams/Stream.hpp index 5fba160..0da4b6e 100644 --- a/include/TessesFramework/Streams/Stream.hpp +++ b/include/TessesFramework/Streams/Stream.hpp @@ -21,8 +21,11 @@ #pragma once #include "../Common.hpp" +#include "../Date/Date.hpp" namespace Tesses::Framework::Streams { enum class SeekOrigin : uint8_t { Begin = 0, Current = 1, End = 2 }; +enum class StreamShutdownMode { Read = 0, Write = 1, ReadWrite = 2 }; + class Stream { public: int32_t ReadByte(); @@ -42,6 +45,12 @@ class Stream { void CopyTo(std::shared_ptr strm, size_t buffSize = 1024); void CopyToLimit(std::shared_ptr strm, uint64_t len, size_t buffSize = 1024); + virtual void Shutdown(StreamShutdownMode mode); + void SetSendTimeout(Tesses::Framework::Date::TimeSpan ts); + void SetRecvTimeout(Tesses::Framework::Date::TimeSpan ts); + virtual void SetSendTimeout(uint64_t seconds); + virtual void SetRecvTimeout(uint64_t seconds); + virtual void Close(); virtual ~Stream(); }; diff --git a/include/TessesFramework/TessesFramework.hpp b/include/TessesFramework/TessesFramework.hpp index bf9dfa5..ae0280e 100644 --- a/include/TessesFramework/TessesFramework.hpp +++ b/include/TessesFramework/TessesFramework.hpp @@ -38,9 +38,9 @@ #include "Http/CallbackServer.hpp" #include "Http/ChangeableServer.hpp" #include "Http/ContentDisposition.hpp" +#include "Http/DomainServer.hpp" #include "Http/FileServer.hpp" -#include "Http/HttpClient.hpp" -#include "Http/HttpServer.hpp" +#include "Http/HttpReverseProxy.hpp" #include "Http/MountableServer.hpp" #include "Http/RouteServer.hpp" #include "Lazy.hpp" diff --git a/include/TessesFramework/Threading/Mutex.hpp b/include/TessesFramework/Threading/Mutex.hpp index bbdbb75..cad5a59 100644 --- a/include/TessesFramework/Threading/Mutex.hpp +++ b/include/TessesFramework/Threading/Mutex.hpp @@ -21,6 +21,7 @@ #pragma once +#include "../Date/Date.hpp" #include "../HiddenField.hpp" namespace Tesses::Framework::Threading { class Mutex { @@ -28,9 +29,39 @@ class Mutex { public: Mutex(); + void Lock(); + void Unlock(); bool TryLock(); + + void lock() { Lock(); } + void unlock() { Unlock(); } ~Mutex(); + + friend class Cond; }; +class LockGuard { + Mutex &mtx; + + public: + explicit LockGuard(Mutex &m) : mtx(m) { mtx.Lock(); } + ~LockGuard() { mtx.Unlock(); } + LockGuard(const LockGuard &) = delete; + LockGuard &operator=(const LockGuard &) = delete; +}; + +class Cond { + HiddenField data; + + public: + Cond(); + void Wait(Mutex *mtx); + bool Wait(Mutex *mtx, uint32_t milliseconds); + bool Wait(Mutex *mtx, Date::TimeSpan ts); + void Signal(); + void Broadcast(); + ~Cond(); +}; + } // namespace Tesses::Framework::Threading \ No newline at end of file diff --git a/src/Crypto/MbedTLS/ClientTLSStream.cpp b/src/Crypto/MbedTLS/ClientTLSStream.cpp index 571210c..8b30292 100644 --- a/src/Crypto/MbedTLS/ClientTLSStream.cpp +++ b/src/Crypto/MbedTLS/ClientTLSStream.cpp @@ -45,13 +45,24 @@ class ClientTLSPrivateData { public: bool eos; bool success; + bool mTLS; std::shared_ptr strm; mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context ctr_drbg; mbedtls_ssl_context ssl; mbedtls_ssl_config conf; mbedtls_x509_crt cachain; + mbedtls_x509_crt client_cert; + mbedtls_pk_context client_key; + ~ClientTLSPrivateData() { + strm->SetSendTimeout((uint64_t)7); + mbedtls_ssl_close_notify(&ssl); + if (mTLS) { + mbedtls_x509_crt_free(&client_cert); + mbedtls_pk_free(&client_key); + } + mbedtls_x509_crt_free(&cachain); mbedtls_ctr_drbg_free(&ctr_drbg); mbedtls_entropy_free(&entropy); @@ -101,6 +112,7 @@ ClientTLSStream::ClientTLSStream( data->eos = false; data->success = false; data->strm = innerStream; + data->mTLS = false; mbedtls_ssl_init(&data->ssl); mbedtls_ssl_config_init(&data->conf); @@ -119,15 +131,10 @@ ClientTLSStream::ClientTLSStream( return; } - if (ret != 0) { - printf("FAILED mbedtls_x509_crt_parse cert %i\n", ret); - return; - } ret = mbedtls_x509_crt_parse( &data->cachain, (const unsigned char *)cert.c_str(), cert.size() + 1); - if (ret != 0) { - printf("FAILED mbedtls_x509_crt_parse chain %i\n", ret); + printf("FAILED mbedtls_x509_crt_parse cert %i\n", ret); return; } @@ -185,6 +192,132 @@ ClientTLSStream::ClientTLSStream( #endif } + +ClientTLSStream::ClientTLSStream( + std::shared_ptr innerStream, + bool verify, std::string domain, std::string cert, + CertificateKeyStore keyStore) { +#if defined(TESSESFRAMEWORK_ENABLE_MBED) + if (cert.empty()) { + cert = GetCertChain(); + } + + ClientTLSPrivateData *data = new ClientTLSPrivateData(); + this->privateData = static_cast(data); + data->eos = false; + data->success = false; + data->strm = innerStream; + data->mTLS = true; + + mbedtls_ssl_init(&data->ssl); + mbedtls_ssl_config_init(&data->conf); + mbedtls_x509_crt_init(&data->cachain); + mbedtls_ctr_drbg_init(&data->ctr_drbg); + mbedtls_entropy_init(&data->entropy); + mbedtls_x509_crt_init(&data->client_cert); + mbedtls_pk_init(&data->client_key); + + const char *pers = "TessesFramework"; + + int ret = 0; + + if ((ret = mbedtls_ctr_drbg_seed( + &data->ctr_drbg, mbedtls_entropy_func, &data->entropy, + (const unsigned char *)pers, strlen(pers))) != 0) { + printf("FAILED mbedtls_ctr_drbg_seed\n"); + return; + } + + ret = mbedtls_x509_crt_parse( + &data->cachain, (const unsigned char *)cert.c_str(), cert.size() + 1); + + if (ret != 0) { + printf("FAILED mbedtls_x509_crt_parse chain %i\n", ret); + return; + } + + ret = mbedtls_x509_crt_parse( + &data->client_cert, (const unsigned char *)keyStore.certificate.c_str(), + keyStore.certificate.size() + 1); + + if (ret != 0) { + printf("FAILED mbedtls_x509_crt_parse client_certificate %i\n", ret); + return; + } + + ret = mbedtls_pk_parse_key( + &data->client_key, (const unsigned char *)keyStore.certificate.c_str(), + keyStore.certificate.size() + 1, + keyStore.password.empty() + ? NULL + : (const unsigned char *)keyStore.password.c_str(), + keyStore.password.size(), mbedtls_entropy_func, &data->entropy); + + if (ret != 0) { + printf("FAILED mbedtls_x509_crt_parse client_certificate %i\n", ret); + return; + } + + if ((ret = mbedtls_ssl_config_defaults(&data->conf, MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT)) != 0) { + char buffer[100]; + mbedtls_strerror(ret, buffer, sizeof(buffer)); + printf("FAILED mbedtls_ssl_conf_defaults %s\n", buffer); + return; + } + + mbedtls_ssl_conf_rng(&data->conf, mbedtls_ctr_drbg_random, &data->ctr_drbg); + + /* #if defined(MBEDTLS_SSL_CACHE_C) + mbedtls_ssl_conf_session_cache(&conf, &cache, + mbedtls_ssl_cache_get, + mbedtls_ssl_cache_set); +#endif*/ + mbedtls_ssl_conf_authmode(&data->conf, verify ? MBEDTLS_SSL_VERIFY_REQUIRED + : MBEDTLS_SSL_VERIFY_NONE); + mbedtls_ssl_conf_ca_chain(&data->conf, &data->cachain, NULL); + + mbedtls_ssl_conf_own_cert(&data->conf, &data->client_cert, + &data->client_key); + + mbedtls_ssl_set_bio(&data->ssl, static_cast(data), strm_send, + strm_recv, NULL); + if ((ret = mbedtls_ssl_setup(&data->ssl, &data->conf) != 0)) { + printf("FAILED mbedtls_ssl_setup %i\n", ret); + return; + } + if ((ret = mbedtls_ssl_set_hostname(&data->ssl, domain.c_str()) != 0)) { + printf("FAILED mbedtls_ssl_set_hostname %i\n", ret); + return; + } + if ((ret = mbedtls_ssl_handshake(&data->ssl)) != 0) { + char buffer[100]; + mbedtls_strerror(ret, buffer, sizeof(buffer)); + printf("FAILED mbedtls_ssl_handshake %s\n", buffer); + return; + } + uint32_t flags; + if ((flags = mbedtls_ssl_get_verify_result(&data->ssl)) != 0) { +#if !defined(MBEDTLS_X509_REMOVE_INFO) + char vrfy_buf[512]; +#endif + +#if !defined(MBEDTLS_X509_REMOVE_INFO) + mbedtls_x509_crt_verify_info(vrfy_buf, sizeof(vrfy_buf), " ! ", flags); + +#endif + if (verify) + return; + } + + data->success = true; + +#endif +} + +void ClientTLSStream::Shutdown(StreamShutdownMode mode) {} + size_t ClientTLSStream::Read(uint8_t *buffer, size_t len) { #if defined(TESSESFRAMEWORK_ENABLE_MBED) auto priv = static_cast(this->privateData); @@ -245,4 +378,18 @@ ClientTLSStream::~ClientTLSStream() { delete static_cast(this->privateData); #endif } + +void ClientTLSStream::SetSendTimeout(uint64_t seconds) { +#if defined(TESSESFRAMEWORK_ENABLE_MBED) + static_cast(this->privateData) + ->strm->SetSendTimeout(seconds); +#endif +} +void ClientTLSStream::SetRecvTimeout(uint64_t seconds) { +#if defined(TESSESFRAMEWORK_ENABLE_MBED) + static_cast(this->privateData) + ->strm->SetRecvTimeout(seconds); +#endif +} + } // namespace Tesses::Framework::Crypto \ No newline at end of file diff --git a/src/Date/Date.cpp b/src/Date/Date.cpp index 519bc48..9e34a11 100644 --- a/src/Date/Date.cpp +++ b/src/Date/Date.cpp @@ -392,7 +392,10 @@ const char *weekday_long[] = {"Sunday", "Monday", "Tuesday", "Wednesday", const char *months_long[] = {"January", "Febuary", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; -bool DateTime::TryParseHttpDate(std::string txt, DateTime &dt) { +bool DateTime::TryParseHttpDate(std::string_view txt) { + return TryParseHttpDate(txt, *this); +} +bool DateTime::TryParseHttpDate(std::string_view txt, DateTime &dt) { // Mon, 24 Jul 2018 11:00:00 GMT auto split = Http::HttpUtils::SplitString(txt, ", ", 2); if (split.size() != 2) @@ -579,9 +582,8 @@ std::string DateTime::ToString(std::string fmt) const { break; case 'u': { - int dow = weekday + 6; - dow %= 7; - text.append(std::to_string(dow + 1)); + + text.append(std::to_string(weekday + 1)); } break; case 'w': { @@ -590,10 +592,10 @@ std::string DateTime::ToString(std::string fmt) const { case 'c': { text.append(weekday_short[weekday]); text.push_back(' '); - text.append(months_short[month]); + text.append(months_short[month - 1]); text.push_back(' '); text.append( - Http::HttpUtils::LeftPad(std::to_string(day), 2, '0')); + Http::HttpUtils::LeftPad(std::to_string(day), 2, ' ')); text.push_back(' '); text.append( Http::HttpUtils::LeftPad(std::to_string(hour), 2, '0')); @@ -709,7 +711,29 @@ void TimeSpan::SetTotalMinutes(int64_t totalMinutes) { void TimeSpan::SetTotalHours(int64_t totalHours) { this->totalSeconds = totalHours * 3600; } - +void TimeSpan::SetTotalDays(int64_t totalDays) { + this->totalSeconds = totalDays * 86400; +} +TimeSpan TimeSpan::FromSeconds(int64_t sec) { + TimeSpan span; + span.SetTotalSeconds(sec); + return span; +} +TimeSpan TimeSpan::FromMinutes(int64_t minutes) { + TimeSpan span; + span.SetTotalMinutes(minutes); + return span; +} +TimeSpan TimeSpan::FromHours(int64_t hours) { + TimeSpan span; + span.SetTotalMinutes(hours); + return span; +} +TimeSpan TimeSpan::FromDays(int64_t days) { + TimeSpan span; + span.SetTotalMinutes(days); + return span; +} std::string TimeSpan::ToString(bool slim) const { std::string str = {}; if (this->totalSeconds < 0) @@ -750,8 +774,8 @@ std::string TimeSpan::ToString(bool slim) const { } return str; } - -bool TimeSpan::TryParse(std::string text, TimeSpan &span) { +bool TimeSpan::TryParse(std::string_view text) { return TryParse(text, *this); } +bool TimeSpan::TryParse(std::string_view text, TimeSpan &span) { if (text.empty()) return false; bool negative = text[0] == '-'; @@ -759,7 +783,7 @@ bool TimeSpan::TryParse(std::string text, TimeSpan &span) { try { - std::string colonPart = text.substr(negative ? 1 : 0); + std::string_view colonPart = text.substr(negative ? 1 : 0); auto res = Http::HttpUtils::SplitString(colonPart, ":"); if (res.size() < 2 || res.size() > 3) diff --git a/src/Filesystem/VFS.cpp b/src/Filesystem/VFS.cpp index 6902b3a..feec74f 100644 --- a/src/Filesystem/VFS.cpp +++ b/src/Filesystem/VFS.cpp @@ -296,6 +296,7 @@ VFSPath VFSPath::CollapseRelativeParents() const { newpath.path = parts; return newpath; } + VFSPath VFSPath::CurrentDirectoryAsRelative() { VFSPath path; path.relative = true; diff --git a/src/Http/ChangeableServer.cpp b/src/Http/ChangeableServer.cpp index 5923a3f..dad727e 100644 --- a/src/Http/ChangeableServer.cpp +++ b/src/Http/ChangeableServer.cpp @@ -27,9 +27,22 @@ ChangeableServer::ChangeableServer(std::shared_ptr original) { this->server = original; } +std::shared_ptr ChangeableServer::GetServer() { + mtx.Lock(); + auto server = this->server; + mtx.Unlock(); + return server; +} +void ChangeableServer::SetServer(std::shared_ptr server) { + mtx.Lock(); + this->server = server; + mtx.Unlock(); +} bool ChangeableServer::Handle(ServerContext &ctx) { - if (this->server) - this->server->Handle(ctx); + auto server = GetServer(); + + if (server) + return server->Handle(ctx); return false; } ChangeableServer::~ChangeableServer() {} diff --git a/src/Http/DomainServer.cpp b/src/Http/DomainServer.cpp new file mode 100644 index 0000000..85f56d9 --- /dev/null +++ b/src/Http/DomainServer.cpp @@ -0,0 +1,40 @@ +#include "TessesFramework/Http/DomainServer.hpp" + +namespace Tesses::Framework::Http { +DomainServer::DomainServer() {} +DomainServer::DomainServer(std::shared_ptr root) : root(root) {} +void DomainServer::Set(std::string domain, + std::shared_ptr server) { + mtx.Lock(); + this->servers[domain] = server; + mtx.Unlock(); +} +void DomainServer::Unset(std::string domain) { + mtx.Lock(); + this->servers.erase(domain); + mtx.Unlock(); +} +void DomainServer::Clear() { + mtx.Lock(); + this->servers.clear(); + mtx.Unlock(); +} +bool DomainServer::Handle(ServerContext &ctx) { + std::string host; + + std::shared_ptr server = nullptr; + if (ctx.requestHeaders.TryGetFirst("Host", host)) { + + mtx.Lock(); + if (this->servers.count(host) > 0) + server = this->servers[host]; + mtx.Unlock(); + } + + if (server) + return server->Handle(ctx); + if (this->root) + return this->root->Handle(ctx); + return false; +} +} // namespace Tesses::Framework::Http \ No newline at end of file diff --git a/src/Http/FileServer.cpp b/src/Http/FileServer.cpp index dd218af..2a73baf 100644 --- a/src/Http/FileServer.cpp +++ b/src/Http/FileServer.cpp @@ -63,7 +63,7 @@ bool FileServer::SendFile(ServerContext &ctx, VFSPath path) { Date::DateTime lw, la; this->vfs->GetDate(path, lw, la); ctx.WithLastModified(lw) - .WithMimeType(HttpUtils::MimeType(path.GetFileName())) + .WithMimeType(HttpUtils::GetMimeTypePath(path)) .SendStream(strm); retVal = true; } diff --git a/src/Http/HttpClient.cpp b/src/Http/HttpClient.cpp index 6f216b5..9e91804 100644 --- a/src/Http/HttpClient.cpp +++ b/src/Http/HttpClient.cpp @@ -65,10 +65,12 @@ StreamHttpRequestBody::StreamHttpRequestBody(std::shared_ptr strm, this->mimeType = mimeType; } void StreamHttpRequestBody::HandleHeaders(HttpDictionary &dict) { - dict.AddValue("Content-Type", this->mimeType); + dict.SetValue("Content-Type", this->mimeType); auto len = this->strm->GetLength(); if (len > -1) - dict.AddValue("Content-Length", std::to_string(len)); + dict.SetValue("Content-Length", std::to_string(len)); + else + dict.SetValue("Transfer-Encoding", "chunked"); } void StreamHttpRequestBody::Write( std::shared_ptr strm) { @@ -96,6 +98,7 @@ void HttpRequest::SendRequest( std::string request = method + " " + uri.GetPathAndQuery() + " HTTP/1.1\r\nHost: " + uri.HostPort() + "\r\n"; + for (auto headers : requestHeaders.kvp) { for (auto item : headers.second) { request.append(headers.first); @@ -106,7 +109,6 @@ void HttpRequest::SendRequest( } request.append("\r\n"); - StreamWriter writer(strm); writer.Write(request); @@ -114,9 +116,9 @@ void HttpRequest::SendRequest( body->Write(strm); } } -std::shared_ptr -HttpRequest::EstablishConnection(Uri uri, bool ignoreSSLErrors, - std::string trusted_root_cert_bundle) { +std::shared_ptr HttpRequest::EstablishConnection( + Uri uri, bool ignoreSSLErrors, std::string trusted_root_cert_bundle, + std::optional mTLS_keyStore) { if (uri.scheme == "http:" || uri.scheme == "ws:") { return std::make_shared(uri.host, uri.GetPort(), false, false, false); @@ -125,24 +127,34 @@ HttpRequest::EstablishConnection(Uri uri, bool ignoreSSLErrors, false, false, false); if (netStrm == nullptr) return nullptr; - return std::make_shared( - netStrm, !ignoreSSLErrors, uri.host, trusted_root_cert_bundle); + return mTLS_keyStore + ? std::make_shared( + netStrm, !ignoreSSLErrors, uri.host, + trusted_root_cert_bundle, mTLS_keyStore.value()) + : std::make_shared( + netStrm, !ignoreSSLErrors, uri.host, + trusted_root_cert_bundle); } return nullptr; } -std::shared_ptr -HttpRequest::EstablishUnixPathConnection(std::string unixPath, Uri uri, - bool ignoreSSLErrors, - std::string trusted_root_cert_bundle) { +std::shared_ptr HttpRequest::EstablishUnixPathConnection( + std::string unixPath, Uri uri, bool ignoreSSLErrors, + std::string trusted_root_cert_bundle, + std::optional mTLS_keyStore) { if (uri.scheme == "http:" || uri.scheme == "ws:") { return std::make_shared(unixPath, false); } else if (uri.scheme == "https:" || uri.scheme == "wss:") { auto netStrm = std::make_shared(unixPath, false); if (netStrm == nullptr) return nullptr; - return std::make_shared( - netStrm, !ignoreSSLErrors, uri.host, trusted_root_cert_bundle); + return mTLS_keyStore + ? std::make_shared( + netStrm, !ignoreSSLErrors, uri.host, + trusted_root_cert_bundle, mTLS_keyStore.value()) + : std::make_shared( + netStrm, !ignoreSSLErrors, uri.host, + trusted_root_cert_bundle); } return nullptr; @@ -186,13 +198,13 @@ HttpResponse::HttpResponse(HttpRequest &req) : responseHeaders(false) { std::string url = req.url; Uri uri; while (Uri::TryParse(url, uri)) { - auto strm = - req.unixSocket.empty() - ? HttpRequest::EstablishConnection(uri, req.ignoreSSLErrors, - req.trusted_root_cert_bundle) - : HttpRequest::EstablishUnixPathConnection( - req.unixSocket, uri, req.ignoreSSLErrors, - req.trusted_root_cert_bundle); + auto strm = req.unixSocket.empty() + ? HttpRequest::EstablishConnection( + uri, req.ignoreSSLErrors, + req.trusted_root_cert_bundle, req.mTLS_keyStore) + : HttpRequest::EstablishUnixPathConnection( + req.unixSocket, uri, req.ignoreSSLErrors, + req.trusted_root_cert_bundle, req.mTLS_keyStore); if (strm == nullptr) return; auto reqHeaders = req.requestHeaders; @@ -628,7 +640,7 @@ void WebSocketUnixSocketClient(std::string unixSocket, std::string url, if (resp.statusCode != 101 || !resp.responseHeaders.TryGetFirst("Sec-WebSocket-Accept", accept) || !resp.responseHeaders.AnyEquals("Connection", "Upgrade") || - !resp.responseHeaders.AnyEquals("Upgrade", "websocket")) { + !resp.responseHeaders.AnyEqualsCSV("Upgrade", "websocket")) { cb(resp.responseHeaders, false); return; } diff --git a/src/Http/HttpReverseProxy.cpp b/src/Http/HttpReverseProxy.cpp new file mode 100644 index 0000000..3064c8f --- /dev/null +++ b/src/Http/HttpReverseProxy.cpp @@ -0,0 +1,525 @@ +/* + TessesFramework a library to make C++ easier for me, used in CrossLang: + https://git.tesses.org/tesses50/crosslang + + Copyright (C) 2026 Mike Nolan + SPDX-License-Identifier: GPL-3.0-or-later WITH TessesFramework-Exception-1.0 + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +#include "TessesFramework/Http/HttpReverseProxy.hpp" +namespace Tesses::Framework::Http { + +static std::string CONNECTION = "CONNECTION"; +static std::string UPGRADE = "UPGRADE"; + +static std::initializer_list STRIP_EQ = { + "XREALIP", "FORWARDED", "KEEPALIVE", "TE", + "TRAILER", "TRANSFERENCODING", "EXPECT", "CONTENTLENGTH", + "CONTENTTYPE", "ACCEPTENCODING", "HOST"}; // Content-Type is OK but we get + // it elsewhere, Content-Length + // is the same way + +static std::initializer_list STRIP_PREFIX = { + "XFORWARDED", "PROXY", "XORIGINAL", "CF", "SSL", "XSSL"}; + +static void sanitise_uppercase_only(const std::string &src, std::string &dest) { + dest.reserve(src.size()); + for (size_t i = 0; i < src.size(); i++) { + char c = (src[i] & 0b11011111); + if (c >= 'A' && c <= 'Z') + dest.push_back(c); + } +} + +static void get_upgrades(const std::string &src, + std::unordered_set &upgrades) { + auto parts = HttpUtils::SplitString(src, ","); + for (auto &item : parts) { + size_t start = item.find_first_not_of(" \t"); + if (start == std::string::npos) + continue; // Empty + size_t end = item.find_last_not_of(" \t"); + upgrades.insert( + HttpUtils::ToLower(item.substr(start, end - start + 1))); + } +} + +static bool uppercase_eq(const std::string &src, const std::string &match, + bool startsWith) { + std::string cmp; + sanitise_uppercase_only(src, cmp); + + if (startsWith) { + if (cmp.size() < match.size()) + return false; + for (size_t i = 0; i < match.size(); i++) { + if (cmp[i] != match[i]) + return false; + } + return true; + } else { + return cmp == match; + } +} + +ReverseProxyConnectionBuilder::ReverseProxyConnectionBuilder( + ServerContext &ctx, bool essentalheaders) + : m_reqheaders(false), m_ctx(ctx) { + + if (essentalheaders) { + std::string str; + + if (ctx.requestHeaders.TryGetFirst("Host", str)) + m_reqheaders.SetValue("X-Forwarded-Host", str); + + m_reqheaders.SetValue("X-Forwarded-For", ctx.ip); + + m_reqheaders.SetValue("X-Forwarded-Proto", + ctx.encrypted ? "https" : "http"); + } +} +ReverseProxyConnectionBuilder & +ReverseProxyConnectionBuilder::WithHeadersFromRequest() { + std::unordered_set wanted_upgrade_protocols; + bool doUpgradeIfApprovedUpgrade = false; + + for (auto item : this->m_ctx.requestHeaders.kvp) { + + if (!item.first.empty()) { + + if (uppercase_eq(item.first, UPGRADE, false)) { + for (auto &item2 : item.second) + get_upgrades(item2, wanted_upgrade_protocols); + + continue; + } + + if (uppercase_eq(item.first, CONNECTION, false)) { + bool anyNotUpgrade = item.second.size() != 1; + + if (!anyNotUpgrade && + !uppercase_eq(item.second.front(), UPGRADE, false)) { + anyNotUpgrade = true; + continue; + } + + if (anyNotUpgrade) + continue; + + doUpgradeIfApprovedUpgrade = true; + + continue; + } + bool doCont = false; + for (auto &strip : STRIP_EQ) + if (uppercase_eq(item.first, strip, false)) { + doCont = true; + break; + } + if (doCont) + continue; + for (auto &strip : STRIP_PREFIX) + if (uppercase_eq(item.first, strip, true)) { + doCont = true; + break; + } + + if (doCont) + continue; + + this->m_reqheaders.SetValue(item.first, item.second); + } + } + + if (doUpgradeIfApprovedUpgrade && !wanted_upgrade_protocols.empty()) { + std::vector valid_upgrades; + for (auto &item : this->m_whitelistedupgrades) { + if (wanted_upgrade_protocols.find(item) != + wanted_upgrade_protocols.end()) { + valid_upgrades.push_back(item); + } + } + + if (!valid_upgrades.empty()) { + this->m_reqheaders.SetValue("Connection", "upgrade"); + + std::string upgrade_text = ""; + for (auto &item : valid_upgrades) { + if (upgrade_text.empty()) + upgrade_text = item; + else + upgrade_text += ", " + item; + } + + this->m_reqheaders.SetValue("Upgrade", upgrade_text); + } + } + + return *this; +} +ReverseProxyConnectionBuilder & +ReverseProxyConnectionBuilder::WithHeader(std::string key, std::string value) { + this->m_reqheaders.AddValue(key, value); + return *this; +} +ReverseProxyConnectionBuilder & +ReverseProxyConnectionBuilder::SetHeader(std::string key, std::string value) { + this->m_reqheaders.SetValue(key, value); + return *this; +} +ReverseProxyConnectionBuilder & +ReverseProxyConnectionBuilder::WithoutHeader(std::string key) { + this->m_reqheaders.Clear(key, false); + return *this; +} +ReverseProxyConnectionBuilder & +ReverseProxyConnectionBuilder::WithUrl(std::string url) { + this->m_url = url; + return *this; +} +ReverseProxyConnectionBuilder & +ReverseProxyConnectionBuilder::WithResponseCallback( + std::function rc) { + this->m_rc = rc; + return *this; +} + +static void +StreamExchange(std::shared_ptr server, + std::shared_ptr client) { + Tesses::Framework::Threading::Thread server2client([server, + client]() -> void { + try { + std::vector buffer; + buffer.resize(1024); + + size_t read = 0; + + do { + read = server->Read(buffer.data(), buffer.size()); + if (read == 0) + return; + read = client->Write(buffer.data(), + std::min(read, buffer.size())); + } while (read != 0); + } catch (...) { + } + + server->Shutdown(Tesses::Framework::Streams::StreamShutdownMode::Read); + + client->Shutdown(Tesses::Framework::Streams::StreamShutdownMode::Write); + }); + + Tesses::Framework::Threading::Thread client2server([client, + server]() -> void { + try { + std::vector buffer; + buffer.resize(1024); + + size_t read = 0; + + do { + read = client->Read(buffer.data(), buffer.size()); + if (read == 0) + return; + read = server->Write(buffer.data(), + std::min(read, buffer.size())); + } while (read != 0); + } catch (...) { + } + client->Shutdown(Tesses::Framework::Streams::StreamShutdownMode::Read); + + server->Shutdown(Tesses::Framework::Streams::StreamShutdownMode::Write); + }); + + server2client.Join(); + client2server.Join(); +} + +bool ReverseProxyConnectionBuilder::Handle() { + if (m_fail) { + m_ctx.statusCode = StatusCode::BadRequest; + return false; + } + + if (!m_url) { + m_ctx.statusCode = StatusCode::InternalServerError; + m_ctx.SendText("" + "" + "500 Internal Server " + "Error" + "" + "

500 Internal Server Error

" + + HttpUtils::HtmlEncode( + "Please chain the reverse proxy url with " + ".WithUrl(\"http://example.com/path/on/backend\")") + + "

" + "" + "" + + ); + + return true; + } + + HttpRequest req; + + req.url = m_url.value(); + req.method = m_ctx.method; + req.requestHeaders = m_reqheaders; + req.followRedirects = false; + std::string type; + + bool isNotUpgradeOrGet = + !(m_ctx.method == "GET" || m_ctx.method == "HEAD" || + m_reqheaders.AnyEquals("Connection", "upgrade")); + + if (isNotUpgradeOrGet && + m_ctx.requestHeaders.TryGetFirst("Content-Type", type)) { + if (type == "application/x-www-form-urlencoded") { + std::string body = HttpUtils::QueryParamsEncode(m_ctx.bodyParams); + req.body = std::make_shared( + body, "application/x-www-form-urlencoded"); + } else { + req.body = std::make_shared( + m_ctx.OpenRequestStream(), type); + } + } else if (isNotUpgradeOrGet) { + req.body = std::make_shared( + m_ctx.OpenRequestStream(), "application/octet-stream"); + } + std::unique_ptr resp; + + try { + resp = std::make_unique(req); + } catch (...) { + m_ctx.statusCode = StatusCode::BadGateway; + m_ctx.SendErrorPage(true); + return true; + } + + std::string val; + + if (resp->responseHeaders.TryGetOnlyOne("Content-Type", val)) + this->m_ctx.responseHeaders.SetValue("Content-Type", val); + if (resp->responseHeaders.TryGetOnlyOne("Set-Cookie", val)) + this->m_ctx.responseHeaders.SetValue("Set-Cookie", val); + if (resp->responseHeaders.TryGetOnlyOne("Cache-Control", val)) + this->m_ctx.responseHeaders.SetValue("Cache-Control", val); + if (resp->responseHeaders.TryGetOnlyOne("ETag", val)) + this->m_ctx.responseHeaders.SetValue("ETag", val); + if (resp->responseHeaders.TryGetOnlyOne("WWW-Authenticate", val)) + this->m_ctx.responseHeaders.SetValue("WWW-Authenticate", val); + if (resp->responseHeaders.TryGetOnlyOne("Last-Modified", val)) + this->m_ctx.responseHeaders.SetValue("Last-Modified", val); + if (resp->responseHeaders.TryGetOnlyOne("Date", val)) + this->m_ctx.responseHeaders.SetValue("Date", val); + + m_ctx.statusCode = resp->statusCode; + + std::unordered_set wanted_upgrade_protocols; + bool doUpgradeIfApprovedUpgrade = false; + + for (auto &item : resp->responseHeaders.kvp) { + if (!item.first.empty()) { + if (uppercase_eq(item.first, UPGRADE, false)) { + for (auto &item2 : item.second) + get_upgrades(item2, wanted_upgrade_protocols); + + continue; + } + + if (uppercase_eq(item.first, CONNECTION, false)) { + bool anyNotUpgrade = item.second.size() != 1; + + if (!anyNotUpgrade && + !uppercase_eq(item.second.front(), UPGRADE, false)) { + anyNotUpgrade = true; + continue; + } + + if (anyNotUpgrade) + continue; + + doUpgradeIfApprovedUpgrade = true; + + continue; + } + } + } + + if (resp->responseHeaders.TryGetOnlyOne("Location", val)) { + Uri uri_src; + if (Uri::TryParse(m_url.value(), uri_src)) { + Uri uri_dest; + if (uri_src.Relative(val, uri_dest)) { + + if (uri_dest.host == uri_src.host && + uri_dest.port == uri_src.port) { + Tesses::Framework::Filesystem::VFSPath dest = uri_dest.path; + Tesses::Framework::Filesystem::VFSPath src = uri_src.path; + + auto path = dest.MakeRelative(src); + + auto path2 = path.MakeAbsolute(m_ctx.path); + + std::vector parts; + + for (auto item : path2.path) { + if (item == "..") { + if (!parts.empty()) { + parts.erase(parts.end() - 1); + } else { + m_ctx.statusCode = + StatusCode::InternalServerError; + m_ctx.SendText( + "" + "" + "500 " + "Internal Server " + "Error" + "" + "

500 Internal Server Error

" + "A redirect went beyond the reverse " + "proxy " + "root" + "

" + "" + "" + + ); + return true; + } + } else if (item == ".") { + // do nothing but don't emit this + } else { + parts.push_back(item); + } + } + Tesses::Framework::Filesystem::VFSPath newpath = + m_ctx.GetServerRoot(); + + newpath.path.insert(newpath.path.cend(), parts.cbegin(), + parts.cend()); + + std::string location = + newpath.ToString() + uri_dest.GetQuery(); + if (!uri_dest.hash.empty()) + location += "#" + uri_dest.hash; + this->m_ctx.responseHeaders.SetValue("Location", location); + } else { + this->m_ctx.responseHeaders.SetValue("Location", + uri_dest.ToString()); + } + } + } + } + + if (this->m_rc) + switch (this->m_rc(m_ctx, *resp)) { + case ReverseProxyAction::Continue: + break; + case ReverseProxyAction::Handled: + return true; + case ReverseProxyAction::Unhandled: + return false; + } + + if (resp->statusCode == StatusCode::SwitchingProtocols) { + if (doUpgradeIfApprovedUpgrade && !wanted_upgrade_protocols.empty()) { + std::vector valid_upgrades; + for (auto &item : this->m_whitelistedupgrades) { + if (wanted_upgrade_protocols.find(item) != + wanted_upgrade_protocols.end()) { + valid_upgrades.push_back(item); + } + } + + if (!valid_upgrades.empty()) { + m_ctx.responseHeaders.SetValue("Connection", "upgrade"); + + std::string upgrade_text = ""; + for (auto &item : valid_upgrades) { + if (upgrade_text.empty()) + upgrade_text = item; + else + upgrade_text += ", " + item; + } + + m_ctx.responseHeaders.SetValue("Upgrade", upgrade_text); + + m_ctx.WriteHeaders(); + + StreamExchange(m_ctx.GetStream(), resp->GetInternalStream()); + return true; + } + } + m_ctx.statusCode = StatusCode::BadRequest; + m_ctx.SendErrorPage(true); + return true; + } else { + m_ctx.SendStream(resp->ReadAsStream()); + return true; + } +} + +ReverseProxyServer::ReverseProxyServer( + std::string url, + std::function + request_callback, + std::function + response_callback, + bool essentialheaders) + : url(url), request_callback(request_callback), + response_callback(response_callback), essentialheaders(essentialheaders) { +} +bool ReverseProxyServer::Handle(ServerContext &ctx) { + Uri uri; + + if (Uri::TryParse(url, uri)) { + uri.query = ctx.queryParams; + Tesses::Framework::Filesystem::VFSPath srcUrl = uri.path; + Tesses::Framework::Filesystem::VFSPath destUrl = ctx.path; + + srcUrl.path.insert(srcUrl.path.cend(), destUrl.path.cbegin(), + destUrl.path.cend()); + uri.path = srcUrl.ToString(); + + ReverseProxyConnectionBuilder builder(ctx, essentialheaders); + + if (request_callback) { + switch (request_callback(ctx, builder, uri)) { + case ReverseProxyAction::Continue: + return builder.WithResponseCallback(response_callback).Handle(); + break; + case ReverseProxyAction::Unhandled: + return false; + case ReverseProxyAction::Handled: + return true; + } + } else { + return builder.WithUrl(uri.ToString()) + .WithHeadersFromRequest() + .WithResponseCallback(response_callback) + .Handle(); + } + } + return false; +} + +} // namespace Tesses::Framework::Http \ No newline at end of file diff --git a/src/Http/HttpServer.cpp b/src/Http/HttpServer.cpp index bc9aaec..af14d88 100644 --- a/src/Http/HttpServer.cpp +++ b/src/Http/HttpServer.cpp @@ -51,6 +51,11 @@ using namespace Tesses::Framework::TextStreams; namespace Tesses::Framework::Http { +static const std::initializer_list ONLY_ONE_OF_THESE = { + "Host", "Content-Length", "Transfer-Encoding", + "Connection", "Date", "Expect", + "Max-Forwards", "Range", "Referer"}; + void ServerSentEvents::SendEventRaw(const std::string &evt) { this->mtx.Lock(); for (auto &item : this->strms) { @@ -283,7 +288,7 @@ class WSServer { return; } - if (!ctx->requestHeaders.AnyEquals("Upgrade", "websocket")) { + if (!ctx->requestHeaders.AnyEqualsCSV("Upgrade", "websocket")) { return; } @@ -647,7 +652,7 @@ void HttpServer::StartAccepting() { if (sock == nullptr) { std::cout << "STREAM ERROR" << std::endl; - return; + continue; } TF_LOG("Before entering socket thread"); @@ -904,10 +909,6 @@ ServerContext &ServerContext::WithContentDisposition(std::string filename, ContentDisposition cd; cd.type = isInline ? "inline" : "attachment"; cd.filename = filename; - - // std::string cd; - // cd = (isInline ? "inline; filename*=UTF-8''" : "attachment; - // filename*=UTF-8''") + HttpUtils::UrlPathEncode(filename); this->responseHeaders.SetValue("Content-Disposition", cd.ToString()); return *this; } @@ -985,6 +986,9 @@ ServerContext &ServerContext::WriteHeaders() { return *this; this->sent = true; + if (this->responseHeaders.kvp.count("Date") == 0) + this->responseHeaders.SetValue("Date", Date::DateTime::NowUTC()); + StreamWriter writer(this->strm); writer.newline = "\r\n"; writer.WriteLine("HTTP/1.1 " + std::to_string((int)statusCode) + " " + @@ -999,6 +1003,21 @@ ServerContext &ServerContext::WriteHeaders() { return *this; } + +static void sanitise_path(std::string &path) { + bool endsWithSlash = !path.empty() ? (path.back() == '/') : false; + Tesses::Framework::Filesystem::VFSPath path2 = path; + for (auto ittr = path2.path.begin(); ittr != path2.path.end(); ittr++) { + if (*ittr == ".." || *ittr == ".") { + path2.path.erase(ittr); + ittr--; + } + } + path = path2.ToString(); + if (endsWithSlash) + path += '/'; +} + void HttpServer::Process(std::shared_ptr strm, std::shared_ptr server, std::string ip, uint16_t port, uint16_t serverPort, bool encrypted, @@ -1042,6 +1061,7 @@ void HttpServer::Process(std::shared_ptr strm, pp.resize(2); ctx.originalPath = pp[0]; + sanitise_path(ctx.originalPath); ctx.path = ctx.originalPath; TF_LOG(ctx.method + " with path " + ctx.path); @@ -1074,6 +1094,29 @@ void HttpServer::Process(std::shared_ptr strm, std::string type; int64_t length; + for (auto &item : ONLY_ONE_OF_THESE) { + if (ctx.requestHeaders.kvp.count(item) > 1) { + ctx.statusCode = StatusCode::BadRequest; + ctx.SendText( + "" + "" + "400 Bad " + "Request" + "" + "

400 Bad Request

" + + HttpUtils::HtmlEncode( + "There are multiple instances of the header \"" + item + + "\".") + + "

" + "" + "" + + ); + return; + } + } + if (!(ctx.method == "GET" || ctx.method == "HEAD") && ctx.requestHeaders.TryGetFirst("Content-Type", type) && type == "application/x-www-form-urlencoded" && diff --git a/src/Http/HttpStream.cpp b/src/Http/HttpStream.cpp index edc2d58..c9830c3 100644 --- a/src/Http/HttpStream.cpp +++ b/src/Http/HttpStream.cpp @@ -57,6 +57,8 @@ bool HttpStream::CanWrite() { bool HttpStream::EndOfStream() { if (this->done) return true; + if (this->recv && this->length > 0) + return this->position >= this->length; if (this->offset < this->read) return false; return this->strm->EndOfStream(); @@ -64,6 +66,8 @@ bool HttpStream::EndOfStream() { int64_t HttpStream::GetLength() { return this->length; } int64_t HttpStream::GetPosition() { return this->position; } size_t HttpStream::Read(uint8_t *buff, size_t len) { + if (len == 0) + return 0; if (this->done) return 0; if (!this->recv) @@ -71,11 +75,21 @@ size_t HttpStream::Read(uint8_t *buff, size_t len) { if (this->length == 0) return 0; if (this->length > 0) { - + if (this->position >= this->length) + return 0; len = std::min((size_t)(this->length - this->position), len); if (len > 0) len = this->strm->Read(buff, len); + + if (len == 0 && this->position < this->length) { + throw std::runtime_error( + "Incomplete Read: " + std::to_string(this->position) + + " bytes read, " + + std::to_string(this->length - this->position) + + " more expected"); + } + this->position += len; return len; } else { @@ -85,6 +99,14 @@ size_t HttpStream::Read(uint8_t *buff, size_t len) { len = std::min((size_t)(this->read - this->offset), len); if (len > 0) len = this->strm->Read(buff, len); + + if (len == 0 && this->offset < this->read) { + throw std::runtime_error( + "Incomplete Read (chunked): " + + std::to_string(this->offset) + " of " + + std::to_string(this->read) + " bytes in current chunk"); + } + this->offset += len; this->position += len; if (this->offset >= this->read) { @@ -95,30 +117,48 @@ size_t HttpStream::Read(uint8_t *buff, size_t len) { } else { StreamReader reader(this->strm); std::string line = reader.ReadLine(); - if (!line.empty()) { - this->read = std::stoull(line, NULL, 16); + if (line.empty()) { - if (this->read == 0) { - reader.ReadLine(); - this->done = true; - return 0; - } else { - this->offset = 0; - - len = - std::min((size_t)(this->read - this->offset), len); - if (len > 0) - len = this->strm->Read(buff, len); - this->offset += len; - this->position += len; - return len; + if (this->position > 0) { + throw std::runtime_error( + "Incomplete Read (chunked): connection closed " + "before final 0 chunk"); } + this->done = true; + return 0; } + this->read = std::stoull(line, NULL, 16); + + if (this->read == 0) { + reader.ReadLine(); + this->done = true; + return 0; + } else { + this->offset = 0; + + len = std::min((size_t)(this->read - this->offset), len); + if (len > 0) + len = this->strm->Read(buff, len); + if (len == 0 && this->read > 0) { + throw std::runtime_error( + "Incomplete Read (chunked): connection closed " + "after chunk size, 0 of " + + std::to_string(this->read) + " bytes"); + } + this->offset += len; + this->position += len; + return len; + } + return 0; } } else { - return this->strm->Read(buff, len); + len = this->strm->Read(buff, len); + if (len == 0) + this->done = true; + this->position += len; + return len; } } } @@ -151,8 +191,10 @@ size_t HttpStream::Write(const uint8_t *buff, size_t len) { this->strm->WriteBlock(buff, len); writer.WriteLine(); + this->position += len; return len; } else { + this->position += len; return this->strm->Write(buff, len); } } diff --git a/src/Http/HttpUtils.cpp b/src/Http/HttpUtils.cpp index 8581e0c..ca16b6d 100644 --- a/src/Http/HttpUtils.cpp +++ b/src/Http/HttpUtils.cpp @@ -162,17 +162,19 @@ std::string Uri::ToString() { uri.append(this->GetPathAndQuery()); return uri; } -std::string HttpUtils::Replace(std::string text, std::string find, - std::string replace) { +std::string HttpUtils::Replace(std::string_view text, std::string_view find, + std::string_view replace) { + if (find.empty()) + return std::string(text); std::string dest; while (text.length() > 0) { std::size_t index = text.find(find); - if (index == std::string::npos) { + if (index == std::string_view::npos) { dest.append(text); break; } else { - std::string left = text.substr(0, index); + std::string_view left = text.substr(0, index); text = text.substr(index + find.size()); dest.append(left); @@ -182,12 +184,12 @@ std::string HttpUtils::Replace(std::string text, std::string find, return dest; } -std::string HttpUtils::LeftPad(std::string text, int count, char c) { - if (text.size() >= (size_t)count) - return text; - - text.insert(text.begin(), (size_t)count - text.size(), c); - return text; +std::string HttpUtils::LeftPad(std::string_view text, int count, char c) { + if (text.size() >= count) + return std::string(text); + std::string newStr(count - text.size(), c); + newStr.append(text); + return newStr; } char HttpUtils::NibbleToHex(uint8_t b, bool isUppercase) { if (isUppercase) { @@ -221,74 +223,105 @@ uint8_t HttpUtils::HexToNibble(char c) { return 0; } -std::string HttpUtils::MimeType(std::filesystem::path p) { - std::string ext = p.extension().string(); - if (ext == ".html" || ext == ".htm") { - return "text/html"; - } - if (ext == ".txt" || ext == ".log" || ext == ".twss") { - return "text/plain"; - } - if (ext == ".woff") { - return "application/x-font-woff"; - } - if (ext == ".vtt") { - return "text/vtt"; - } - if (ext == ".svg") { - return "image/svg+xml"; - } - if (ext == ".webp") { - return "image/webp"; - } - if (ext == ".vcf") { - return "text/v-card"; - } - if (ext == ".rss" || ext == ".xml" || ext == ".atom" || ext == ".rdf") { - return "application/xml"; - } - if (ext == ".js") { - return "text/javascript"; - } - if (ext == ".json") { - return "application/json"; - } - if (ext == ".wasm") { - return "application/wasm"; - } - if (ext == ".png") { - return "image/png"; - } - if (ext == ".jpg" || ext == ".jpeg") { - return "image/jpeg"; - } - if (ext == ".css") { - return "text/css"; - } - if (ext == ".gif") { - return "image/gif"; - } - if (ext == ".mp4") { - return "video/mp4"; - } - if (ext == ".mov") { - return "video/quicktime"; - } - if (ext == ".m4a") { - return "audio/mp4"; - } - if (ext == ".webm") { - return "video/webm"; - } - if (ext == ".webmanifest") { - return "application/manifest+json"; - } - if (ext == ".ico") { - return "image/x-icon"; - } +struct mimetype_reg { + Threading::Mutex mtx; + std::unordered_map map = { + {".html", "text/html"}, + {".htm", "text/html"}, + {".txt", "text/plain"}, + {".log", "text/plain"}, + {".twss", "text/twss"}, + {".css", "text/css"}, + {".js", "text/javascript"}, + {".mjs", "text/javascript"}, + {".json", "application/json"}, + {".map", "application/json"}, + {".pdf", "application/pdf"}, + {".png", "image/png"}, + {".jpg", "image/jpeg"}, + {".jpeg", "image/jpeg"}, + {".gif", "image/gif"}, + {".webp", "image/webp"}, + {".svg", "image/svg+xml"}, + {".ico", "image/x-icon"}, + {".bmp", "image/bmp"}, + {".avif", "image/avif"}, + {".heic", "image/heic"}, + {".tif", "image/tiff"}, + {".tiff", "image/tiff"}, + {".mp4", "video/mp4"}, + {".webm", "video/webm"}, + {".mov", "video/quicktime"}, + {".mkv", "video/x-matroska"}, + {".avi", "video/x-msvideo"}, + {".ts", "video/mp2t"}, + {".mp3", "audio/mpeg"}, + {".m4a", "audio/mp4"}, + {".ogg", "audio/ogg"}, + {".oga", "audio/ogg"}, + {".opus", "audio/ogg"}, + {".wav", "audio/wav"}, + {".flac", "audio/flac"}, + {".aac", "audio/aac"}, + {".weba", "audio/webm"}, + {".woff", "font/woff"}, + {".woff2", "font/woff2"}, + {".ttf", "font/ttf"}, + {".zip", "application/zip"}, + {".gz", "application/gzip"}, + {".7z", "application/x-7z-compressed"}, + {".rar", "application/vnd.rar"}, + {".csv", "text/csv"}, + {".vtt", "text/vtt"}, + {".vcf", "text/v-card"}, + {".rss", "application/xml"}, + {".xml", "application/xml"}, + {".atom", "application/xml"}, + {".rdf", "application/xml"}, + {".wasm", "application/wasm"}, + {".webmanifest", "application/manifest+json"}, + {".m3u8", "application/vnd.apple.mpegurl"}, + {".mpd", "application/dash+xml"}, + {".md", "text/markdown"}, + {".epub", "application/epub+zip"}, + {".ics", "text/calendar"}, + {".apk", "application/vnd.android.package-archive"}, + {".crvm", "application/crvm"}, + {".tcross", "text/tcross"}, + }; + std::string Get(const std::string &ext) { + Threading::LockGuard lg(mtx); + std::string mime; - return "application/octet-stream"; + if (map.count(ext) != 0) + mime = map[ext]; + + if (mime.empty()) + return "application/octet-stream"; + return mime; + } + void Add(const std::string &ext, const std::string &mime) { + Threading::LockGuard lg(mtx); + map[ext] = mime; + } +}; + +static mimetype_reg mime_reg; + +std::string HttpUtils::GetMimeType(const std::string &ext) { + return mime_reg.Get(ext); } +std::string HttpUtils::GetMimeTypePath(const Filesystem::VFSPath &p) { + return GetMimeType(p.GetExtension()); +} +void HttpUtils::AddMimeType(const std::string &ext, const std::string &mime) { + mime_reg.Add(ext, mime); +} +void HttpUtils::AddMimeTypePath(const Filesystem::VFSPath &p, + const std::string &mime) { + AddMimeType(p.GetExtension(), mime); +} + bool HttpUtils::Invalid(char c) { // just do windows because it is the strictist when it comes to windows, mac // and linux @@ -316,7 +349,7 @@ bool HttpUtils::Invalid(char c) { return true; return false; } -std::string HttpUtils::Sanitise(std::string text) { +std::string HttpUtils::Sanitise(std::string_view text) { std::string myStr = {}; for (auto item : text) { if (Invalid(item)) @@ -326,7 +359,8 @@ std::string HttpUtils::Sanitise(std::string text) { return myStr; } -void HttpUtils::QueryParamsDecode(HttpDictionary &dict, std::string query) { +void HttpUtils::QueryParamsDecode(HttpDictionary &dict, + std::string_view query) { for (auto item : SplitString(query, "&")) { std::vector ss = SplitString(item, "=", 2); if (ss.size() >= 1) { @@ -338,7 +372,7 @@ void HttpUtils::QueryParamsDecode(HttpDictionary &dict, std::string query) { } } } -std::string HttpUtils::Join(std::string joinStr, +std::string HttpUtils::Join(std::string_view joinStr, std::vector ents) { std::string str = {}; bool first = true; @@ -368,41 +402,43 @@ std::string HttpUtils::QueryParamsEncode(HttpDictionary &dict) { return s; } -std::string HttpUtils::UrlDecode(std::string v) { +std::string HttpUtils::UrlDecode(std::string_view v) { std::string s = {}; for (size_t i = 0; i < v.size(); i++) { if (v[i] == '+') s.push_back(' '); else if (v[i] == '%') { - i++; - uint8_t n = HexToNibble(v[i]) << 4; - i++; - n |= HexToNibble(v[i]); - s.push_back((char)n); + if (i + 2 >= v.size()) + break; + + uint8_t n = (HexToNibble(v[i + 1]) << 4) | (HexToNibble(v[i + 2])); + i += 2; + s.push_back(static_cast(n)); } else s.push_back(v[i]); } return s; } -std::string HttpUtils::UrlPathEncode(std::string v, bool ignoreSpace) { +std::string HttpUtils::UrlPathEncode(std::string_view v, bool ignoreSpace) { std::string s = {}; for (auto item : v) { - if (item >= 'A' && item <= 'Z') + uint8_t byte = static_cast(item); + if (byte >= 'A' && byte <= 'Z') s.push_back(item); - else if (item >= 'a' && item <= 'z') + else if (byte >= 'a' && byte <= 'z') s.push_back(item); - else if (item >= '0' && item <= '9') + else if (byte >= '0' && byte <= '9') s.push_back(item); - else if (item == '-' || item == '_' || item == '.' || item == '~' || - item == '/') + else if (byte == '-' || byte == '_' || byte == '.' || byte == '~' || + byte == '/') s.push_back(item); else { - if (item != ' ' || !ignoreSpace) { + if (byte != ' ' || !ignoreSpace) { s.push_back('%'); - s.push_back(NibbleToHex((item >> 4) & 0xF)); - s.push_back(NibbleToHex((item) & 0xF)); + s.push_back(NibbleToHex((byte >> 4) & 0xF)); + s.push_back(NibbleToHex((byte) & 0xF)); } else { s.push_back(' '); } @@ -410,134 +446,87 @@ std::string HttpUtils::UrlPathEncode(std::string v, bool ignoreSpace) { } return s; } -std::string HttpUtils::UrlPathDecode(std::string v) { +std::string HttpUtils::UrlPathDecode(std::string_view v) { std::string s = {}; for (size_t i = 0; i < v.size(); i++) { if (v[i] == '%') { - i++; - uint8_t n = HexToNibble(v[i]) << 4; - i++; - n |= HexToNibble(v[i]); - s.push_back((char)n); + if (i + 2 >= v.size()) + break; + + uint8_t n = (HexToNibble(v[i + 1]) << 4) | (HexToNibble(v[i + 2])); + i += 2; + s.push_back(static_cast(n)); } else s.push_back(v[i]); } return s; } -std::string HttpUtils::UrlEncode(std::string v) { +std::string HttpUtils::UrlEncode(std::string_view v) { std::string s = {}; for (auto item : v) { - if (item == ' ') + uint8_t byte = static_cast(item); + if (byte == ' ') s.push_back('+'); - else if (item >= 'A' && item <= 'Z') + else if (byte >= 'A' && byte <= 'Z') s.push_back(item); - else if (item >= 'a' && item <= 'z') + else if (byte >= 'a' && byte <= 'z') s.push_back(item); - else if (item >= '0' && item <= '9') + else if (byte >= '0' && byte <= '9') s.push_back(item); - else if (item == '-' || item == '_' || item == '.' || item == '~') + else if (byte == '-' || byte == '_' || byte == '.' || byte == '~') s.push_back(item); else { s.push_back('%'); - s.push_back(NibbleToHex((item >> 4) & 0xF)); - s.push_back(NibbleToHex((item) & 0xF)); + s.push_back(NibbleToHex((byte >> 4) & 0xF)); + s.push_back(NibbleToHex((byte) & 0xF)); } } return s; } - -std::vector HttpUtils::SplitString(std::string text, - std::string delimiter, - std::size_t maxCnt) { - std::vector strs; +void HttpUtils::SplitString(std::vector &strs, + std::string_view text, std::string_view delimiter, + std::size_t maxCnt) { + if (maxCnt == 0) + return; + if (delimiter.empty()) { + strs.push_back(std::string(text)); + return; + } std::size_t i = 1; - while (text.length() > 0) { + while (true) { if (i == maxCnt) { - strs.push_back(text); + strs.push_back(std::string(text)); break; } std::size_t index = text.find(delimiter); - if (index == std::string::npos) { - strs.push_back(text); + if (index == std::string_view::npos) { + strs.push_back(std::string(text)); break; - } else { - std::string left = text.substr(0, index); - - text = text.substr(index + delimiter.size()); - - strs.push_back(left); } + std::string_view left = text.substr(0, index); + + text = text.substr(index + delimiter.size()); + + strs.push_back(std::string(left)); + i++; } +} + +std::vector HttpUtils::SplitString(std::string_view text, + std::string_view delimiter, + std::size_t maxCnt) { + std::vector strs; + SplitString(strs, text, delimiter, maxCnt); + return strs; } -std::string HttpUtils::HtmlDecodeOnlyEntityNumber(std::string v) { - std::string buff = {}; - int state = 0; - uint64_t n = 0; - for (auto item : v) { - switch (state) { - case 0: - if (item == '&') - state = 1; - else - buff.push_back(item); - break; - case 1: - if (item == '#') { - state = 2; - n = 0; - } else { - state = 0; - buff.push_back('&'); - buff.push_back(item); - } - break; - case 2: - if (item == ';') { - state = 0; - if (n <= 0x7F) { - buff.push_back((char)n); - } else if (n >= 0x80 && n <= 0x7FF) { - uint8_t high = - 0b11000000 | ((uint8_t)(n >> 6) & 0b00011111); - uint8_t low = 0b10000000 | ((uint8_t)(n) & 0b00111111); - buff.push_back((char)high); - buff.push_back((char)low); - } else if (n >= 0x800 && n <= 0xFFFF) { - uint8_t high = - 0b11100000 | ((uint8_t)(n >> 12) & 0b00001111); - uint8_t low = 0b10000000 | ((uint8_t)(n >> 6) & 0b00111111); - uint8_t lowest = 0b10000000 | ((uint8_t)(n) & 0b00111111); - buff.push_back((char)high); - buff.push_back((char)low); - buff.push_back((char)lowest); - } else if (n >= 0x010000 && n <= 0x10FFFF) { - uint8_t highest = - 0b11110000 | ((uint8_t)(n >> 18) & 0b00000111); - uint8_t high = - 0b10000000 | ((uint8_t)(n >> 12) & 0b00111111); - uint8_t low = 0b10000000 | ((uint8_t)(n >> 6) & 0b00111111); - uint8_t lowest = 0b10000000 | ((uint8_t)(n) & 0b00111111); - buff.push_back((char)highest); - buff.push_back((char)high); - buff.push_back((char)low); - buff.push_back((char)lowest); - } - } else if (item >= '0' && item <= '9') { - n *= 10; - n += item - '0'; - } - } - } - return buff; -} -std::string HttpUtils::HtmlP(std::string text) { +std::string HttpUtils::HtmlP(std::string_view text) { std::string newText = ""; std::string builder = ""; @@ -589,7 +578,7 @@ std::string HttpUtils::HtmlP(std::string text) { return newText; } -std::string HttpUtils::HtmlEncode(std::string html) { +std::string HttpUtils::HtmlEncode(std::string_view html) { std::string myHtml = {}; for (auto item : html) { if (item == '\"') { @@ -736,18 +725,12 @@ std::string HttpUtils::StatusCodeString(StatusCode code) { return ""; } } -CaseInsensitiveLess::CaseInsensitiveLess(const CaseInsensitiveLess &str) { - this->caseSensitive = str.caseSensitive; - this->offset = this; -} -CaseInsensitiveLess::CaseInsensitiveLess() { - this->caseSensitive = false; - this->offset = this; -} -HttpDictionary::HttpDictionary(bool isCaseSensitive) { - this->kvp.key_comp().offset->caseSensitive = isCaseSensitive; +CaseInsensitiveLess::CaseInsensitiveLess(bool caseSensitive) { + this->caseSensitive = caseSensitive; } +HttpDictionary::HttpDictionary(bool isCaseSensitive) + : kvp(CaseInsensitiveLess(isCaseSensitive)) {} bool HttpDictionary::AnyEquals(std::string key, std::string value) { if (this->kvp.count(key) > 0) for (auto v : this->kvp[key]) @@ -756,6 +739,45 @@ bool HttpDictionary::AnyEquals(std::string key, std::string value) { // for(auto v : item.second) if(v == value) return true; return false; } +bool HttpUtils::CaseInsensitiveCompare(std::string_view left, + std::string_view right) { + if (left.size() != right.size()) + return false; + + for (size_t i = 0; i < left.size(); i++) { + unsigned char senleft = static_cast(left[i]); + unsigned char senright = static_cast(right[i]); + unsigned char insenleft = (senleft & 0b11011111); + unsigned char insenright = (senright & 0b11011111); + + if (insenleft >= 'A' && insenleft <= 'Z' && insenright >= 'A' && + insenright <= 'Z') { + if (insenleft != insenright) + return false; + } else if (senleft != senright) + return false; + } + return true; +} +bool HttpDictionary::AnyEqualsCSV(std::string key, std::string value) { + + if (this->kvp.count(key)) { + for (std::string v : this->kvp[key]) { + auto items = HttpUtils::SplitString(v, ","); + for (auto item : items) { + std::string_view itm = item; + size_t start = itm.find_first_not_of(" \t"); + if (start == std::string::npos) + continue; // Empty + size_t end = itm.find_last_not_of(" \t"); + std::string_view val = itm.substr(start, end - start + 1); + if (HttpUtils::CaseInsensitiveCompare(value, val)) + return true; + } + } + } + return false; +} void HttpDictionary::Clear() { kvp.clear(); } void HttpDictionary::Clear(std::string key, bool kvpExistsAfter) { @@ -848,20 +870,89 @@ bool HttpDictionary::TryGetFirstDouble(std::string key, double &value) { } return true; } + +bool HttpDictionary::TryGetOnlyOne(std::string key, std::string &value) { + if (kvp.count(key) == 0) + return false; + auto &ls = kvp[key]; + if (ls.size() != 1) + return false; + value = ls.front(); + + return true; +} + +bool HttpDictionary::TryGetOnlyOneInt(std::string key, int64_t &value) { + std::string val; + if (!TryGetOnlyOne(key, val)) + return false; + try { + size_t off = 0; + auto v = std::stoll(val, &off); + if (off != val.size()) + return false; + value = v; + } catch (std::exception &ex) { + return false; + } + return true; +} + +bool HttpDictionary::TryGetOnlyOneDouble(std::string key, double &value) { + std::string val; + if (!TryGetOnlyOne(key, val)) + return false; + try { + size_t off = 0; + auto v = std::stod(val, &off); + if (off != val.size()) + return false; + value = v; + } catch (std::exception &ex) { + return false; + } + return true; +} +bool HttpDictionary::TryGetOnlyOneDate(std::string key, Date::DateTime &value) { + std::string val; + if (!TryGetOnlyOne(key, val)) + return false; + return Date::DateTime::TryParseHttpDate(val, value); +} + +bool HttpDictionary::TryGetOnlyOneBoolean(std::string key, bool &val) { + val = false; + if (kvp.count(key) == 0) + return true; + + auto &ls = kvp[key]; + if (ls.size() != 1) + return false; + + auto front = ls.front(); + val = front == "on" || front == "true"; + + return true; +} + bool CaseInsensitiveLess::operator()(const std::string &s1, const std::string &s2) const { if (this->caseSensitive) - return s1 == s2; - return HttpUtils::ToLower(s1) < HttpUtils::ToLower(s2); + return s1 < s2; + return std::lexicographical_compare( + s1.begin(), s1.end(), s2.begin(), s2.end(), [](char a, char b) { + return std::tolower(static_cast(a)) < + std::tolower(static_cast(b)); + }); } -std::string HttpUtils::ToLower(std::string str) { +std::string HttpUtils::ToLower(std::string_view str) { std::string str1(str.length(), ' '); std::transform(str.begin(), str.end(), str1.begin(), tolower); return str1; } -std::string HttpUtils::ToUpper(std::string str) { +std::string HttpUtils::ToUpper(std::string_view str) { std::string str1(str.length(), ' '); std::transform(str.begin(), str.end(), str1.begin(), toupper); return str1; @@ -909,13 +1000,12 @@ void HttpUtils::BytesToHex(std::string &text, const std::vector &data, text[i * 2 + 1] += NibbleToHex(data[i], isUpper); } } -std::vector HttpUtils::HexToBytes(const std::string &text) { +std::vector HttpUtils::HexToBytes(std::string_view text) { std::vector data; HexToBytes(data, text); return data; } -void HttpUtils::HexToBytes(std::vector &data, - const std::string &text) { +void HttpUtils::HexToBytes(std::vector &data, std::string_view text) { if (text.empty()) { data.clear(); return; diff --git a/src/Http/MountableServer.cpp b/src/Http/MountableServer.cpp index 624a47a..81bc973 100644 --- a/src/Http/MountableServer.cpp +++ b/src/Http/MountableServer.cpp @@ -51,36 +51,46 @@ MountableServer::MountableServer(std::shared_ptr root) { void MountableServer::Mount(std::string path, std::shared_ptr server) { + mtx.Lock(); this->servers.insert( this->servers.begin(), std::pair>(path, server)); + mtx.Unlock(); } void MountableServer::Unmount(std::string path) { + mtx.Lock(); for (auto i = this->servers.begin(); i != this->servers.end(); i++) { auto &item = *i; if (item.first == path) { this->servers.erase(i); + mtx.Unlock(); return; } } + mtx.Unlock(); } bool MountableServer::Handle(ServerContext &ctx) { std::string oldPath = ctx.path; + mtx.Lock(); + bool needUnlock = true; for (auto item : this->servers) { if (StartsWith(oldPath, item.first)) { ctx.path = Subpath(oldPath, item.first); + mtx.Unlock(); if (item.second->Handle(ctx)) { ctx.path = oldPath; return true; } ctx.path = oldPath; + needUnlock = false; break; } } + if (needUnlock) + mtx.Unlock(); ctx.path = oldPath; if (this->root && this->root->Handle(ctx)) return true; return false; } -MountableServer::~MountableServer() {} } // namespace Tesses::Framework::Http diff --git a/src/Http/RouteServer.cpp b/src/Http/RouteServer.cpp index 8c81449..49b3124 100644 --- a/src/Http/RouteServer.cpp +++ b/src/Http/RouteServer.cpp @@ -58,22 +58,36 @@ RouteServer::RouteServer(std::shared_ptr root) : root(root) {} void RouteServer::Add(std::string method, std::string pattern, ServerRequestHandler handler) { + mtx.Lock(); this->routes.emplace_back(pattern, method, handler); + mtx.Unlock(); } bool RouteServer::Handle(ServerContext &ctx) { auto pathArgs = ctx.pathArguments; auto path = Tesses::Framework::Filesystem::VFSPath::ParseUriPath(ctx.path); + bool mustUnlock = true; + mtx.Lock(); for (auto &svr : this->routes) { if (svr.method != ctx.method && !((svr.method == "GET" && ctx.method == "HEAD") || (svr.method == "HEAD" && ctx.method == "GET"))) continue; ctx.pathArguments = pathArgs; - if (svr.Equals(path, ctx.pathArguments) && svr.handler && - svr.handler(ctx)) - return true; + if (svr.Equals(path, ctx.pathArguments) && svr.handler) { + auto hndl = svr.handler; + mtx.Unlock(); + + if (hndl(ctx)) { + return true; + } + mustUnlock = false; + + break; + } } + if (mustUnlock) + mtx.Unlock(); ctx.pathArguments = pathArgs; if (this->root) @@ -104,4 +118,9 @@ void RouteServer::Query(std::string pattern, ServerRequestHandler handler) { void RouteServer::Options(std::string pattern, ServerRequestHandler handler) { Add("OPTIONS", pattern, handler); } +void RouteServer::Clear() { + mtx.Lock(); + this->routes.clear(); + mtx.Unlock(); +} } // namespace Tesses::Framework::Http \ No newline at end of file diff --git a/src/Platform/Process.cpp b/src/Platform/Process.cpp index d3cb722..1c17894 100644 --- a/src/Platform/Process.cpp +++ b/src/Platform/Process.cpp @@ -699,12 +699,56 @@ void ShellFileOrUrl(std::string fileOrUrl) { #elif !defined(TESSESFRAMEWORK_ENABLE_PROCESS) throw std::runtime_error("Process not enabled"); #elif defined(_WIN32) - auto exec = Tesses::Framework::Platform::Environment::GetRealExecutablePath( - (std::string) "cmd"); - Process p(exec.ToString(), {"cmd", "/c", "start", fileOrUrl}); - if (p.Start()) - if (p.WaitForExit() != 0) - throw std::runtime_error("Exit code did not indicate success"); + std::u16string wname; + UTF16::FromUTF8(wname, fileOrUrl); + + auto result = ShellExecuteW(NULL, L"open", (LPCWSTR)wname.c_str(), NULL, + NULL, SW_SHOWNORMAL); + if ((INT_PTR)result <= 32) { + switch ((int)result) { + case 0: + throw std::runtime_error( + "The operating system is out of memory or resources."); + case 2: + throw std::runtime_error( + "The specified file was not found (SE_ERR_FNF)."); + case 3: + throw std::runtime_error( + "The specified path was not found (SE_ERR_PNF)."); + case 5: + throw std::runtime_error("Access denied (SE_ERR_ACCESSDENIED)."); + case 8: + throw std::runtime_error("Out of memory (SE_ERR_OOM)."); + case 11: + throw std::runtime_error( + "Bad executable format (ERROR_BAD_FORMAT)."); + case 26: + throw std::runtime_error( + "Sharing violation occurred (SE_ERR_SHARE)."); + case 27: + throw std::runtime_error("File association incomplete or invalid " + "(SE_ERR_ASSOCINCOMPLETE)."); + case 28: + throw std::runtime_error( + "DDE transaction timed out (SE_ERR_DDETIMEOUT)."); + case 29: + throw std::runtime_error( + "DDE transaction failed (SE_ERR_DDEFAIL)."); + case 30: + throw std::runtime_error("DDE transaction busy (SE_ERR_DDEBUSY)."); + case 31: + throw std::runtime_error( + "No application associated with file extension " + "(SE_ERR_NOASSOC)."); + case 32: + throw std::runtime_error("Dynamic-link library (DLL) not found " + "(SE_ERR_DLLNOTFOUND)."); + default: + throw std::runtime_error("Unknown error: " + + std::to_string((int)result)); + } + } + #elif defined(__APPLE__) auto exec = Tesses::Framework::Platform::Environment::GetRealExecutablePath( (std::string) "open"); diff --git a/src/Streams/NetworkStream.cpp b/src/Streams/NetworkStream.cpp index e32e5c9..62191d5 100644 --- a/src/Streams/NetworkStream.cpp +++ b/src/Streams/NetworkStream.cpp @@ -43,12 +43,15 @@ using HttpUtils = Tesses::Framework::Http::HttpUtils; #else #if defined(_WIN32) -#include +// clang-format off + #include +#include #include #include +// clang-format on #if defined __has_include #if __has_include() #include @@ -72,8 +75,8 @@ extern "C" { !defined(__PS2__) #include #endif - #include +#include } #endif #if defined(GEKKO) @@ -98,6 +101,7 @@ extern "C" uint32_t if_config(char *local_ip, char *netmask, char *gateway, #define NETWORK_GETADDRINFO getaddrinfo #define NETWORK_FREEADDRINFO freeaddrinfo #define NETWORK_GETSOCKNAME getsockname +#define NETWORK_SHUTDOWN shutdown #if defined(_WIN32) #define NETWORK_CLOSE closesocket @@ -841,6 +845,33 @@ size_t NetworkStream::WriteTo(const uint8_t *buff, size_t sz, std::string ip, return 0; return (size_t)sz2; } +void NetworkStream::Shutdown(StreamShutdownMode mode) { + + switch (mode) { + case StreamShutdownMode::Read: +#if defined(_WIN32) + + NETWORK_SHUTDOWN(this->sock, SD_RECEIVE); +#else + NETWORK_SHUTDOWN(this->sock, SHUT_RD); +#endif + break; + case StreamShutdownMode::Write: +#if defined(_WIN32) + NETWORK_SHUTDOWN(this->sock, SD_SEND); +#else + NETWORK_SHUTDOWN(this->sock, SHUT_WR); +#endif + break; + case StreamShutdownMode::ReadWrite: +#if defined(_WIN32) + NETWORK_SHUTDOWN(this->sock, SD_BOTH); +#else + NETWORK_SHUTDOWN(this->sock, SHUT_RDWR); +#endif + break; + } +} void NetworkStream::Close() { if (this->owns && this->success) NETWORK_CLOSE(this->sock); @@ -858,6 +889,22 @@ void NetworkStream::SetNoDelay(bool noDelay) { (const char *)&noDelay2, (socklen_t)sizeof(noDelay2)); } +void NetworkStream::SetSendTimeout(uint64_t seconds) { + timeval timeout; + timeout.tv_sec = (time_t)seconds; + timeout.tv_usec = 0; + + NETWORK_SETSOCKOPT(this->sock, SOL_SOCKET, SO_SNDTIMEO, + (const char *)&timeout, (socklen_t)sizeof(timeout)); +} +void NetworkStream::SetRecvTimeout(uint64_t seconds) { + timeval timeout; + timeout.tv_sec = (time_t)seconds; + timeout.tv_usec = 0; + + NETWORK_SETSOCKOPT(this->sock, SOL_SOCKET, SO_RCVTIMEO, + (const char *)&timeout, (socklen_t)sizeof(timeout)); +} } // namespace Tesses::Framework::Streams #else namespace Tesses::Framework::Streams { @@ -911,6 +958,7 @@ NetworkStream::GetIPs(bool ipV6) { } NetworkStream::~NetworkStream() {} void NetworkStream::SetNoDelay(bool noDelay) {} +void NetworkStream::Shutdown(StreamShutdownMode mode) {} void NetworkStream::Close() {} uint16_t NetworkStream::GetPort() { return 0; } uint16_t TcpServer::GetPort() { return 0; } diff --git a/src/Streams/Stream.cpp b/src/Streams/Stream.cpp index 76d4b39..0d02178 100644 --- a/src/Streams/Stream.cpp +++ b/src/Streams/Stream.cpp @@ -117,4 +117,22 @@ void Stream::CopyTo(std::shared_ptr strm, size_t buffSize) { strm->Flush(); } Stream::~Stream() {} + +void Stream::Shutdown(StreamShutdownMode mode) {} +void Stream::SetSendTimeout(Tesses::Framework::Date::TimeSpan ts) { + int64_t sec = ts.TotalSeconds(); + if (sec < 0) + SetSendTimeout(0); + else + SetSendTimeout(sec); +} +void Stream::SetRecvTimeout(Tesses::Framework::Date::TimeSpan ts) { + int64_t sec = ts.TotalSeconds(); + if (sec < 0) + SetRecvTimeout(0); + else + SetRecvTimeout(sec); +} +void Stream::SetSendTimeout(uint64_t seconds) {} +void Stream::SetRecvTimeout(uint64_t seconds) {} } // namespace Tesses::Framework::Streams diff --git a/src/TF_Init.cpp b/src/TF_Init.cpp index 64d989a..38d225e 100644 --- a/src/TF_Init.cpp +++ b/src/TF_Init.cpp @@ -521,7 +521,7 @@ std::string TF_GetExecutableName() { if (sysctl(mib, 4, path.data(), &len, NULL, 0) < 0) { return ""; } - path.resize(strlen(path.c_str())); + path.resize(len - 1); return path; #elif defined(__NetBSD__) auto path = Filesystem::LocalFS->ReadLink( @@ -534,18 +534,14 @@ std::string TF_GetExecutableName() { return path.ToString(); #elif TARGET_OS_MAC && !TARGET_OS_IPHONE std::string path; - path.resize(1025); - uint32_t bufsize = (uint32_t)path.size(); + uint32_t bufsize = 0; + _NSGetExecutablePath(NULL, &bufsize); + path.resize(bufsize); if (_NSGetExecutablePath(path.data(), &bufsize) == 0) { - path.resize(strlen(path.c_str())); + path.resize(bufsize - 1); return path; - } else { - path.resize(bufsize); - if (_NSGetExecutablePath(path.data(), &bufsize) == 0) { - path.resize(strlen(path.c_str())); - return path; - } } + #endif return ""; diff --git a/src/Threading/Mutex.cpp b/src/Threading/Mutex.cpp index 09d7298..10b9b0e 100644 --- a/src/Threading/Mutex.cpp +++ b/src/Threading/Mutex.cpp @@ -34,26 +34,45 @@ namespace Tesses::Framework::Threading { class MutexHiddenFieldData : public HiddenFieldData { public: #if defined(_WIN32) - HANDLE mtx; + CRITICAL_SECTION mtx; #else pthread_mutex_t mtx; pthread_mutexattr_t attr; #endif ~MutexHiddenFieldData() { #if defined(_WIN32) - CloseHandle(mtx); + DeleteCriticalSection(&mtx); #else pthread_mutex_destroy(&mtx); pthread_mutexattr_destroy(&attr); #endif } }; + +class MutexCondHiddenFieldData : public HiddenFieldData { + public: +#if defined(_WIN32) + CONDITION_VARIABLE cond; +#else + + pthread_cond_t cond; #endif + + ~MutexCondHiddenFieldData() { +#if defined(_WIN32) + +#else + pthread_cond_destroy(&cond); +#endif + } +}; +#endif + Mutex::Mutex() { #if defined(TESSESFRAMEWORK_ENABLE_THREADING) auto md = this->data.AllocField(); #if defined(_WIN32) - md->mtx = CreateMutex(NULL, false, NULL); + InitializeCriticalSection(&(md->mtx)); #else pthread_mutexattr_init(&md->attr); pthread_mutexattr_settype(&md->attr, PTHREAD_MUTEX_RECURSIVE); @@ -66,7 +85,7 @@ void Mutex::Lock() { #if defined(TESSESFRAMEWORK_ENABLE_THREADING) auto md = this->data.GetField(); #if defined(_WIN32) - WaitForSingleObject(md->mtx, INFINITE); + EnterCriticalSection(&(md->mtx)); #else pthread_mutex_lock(&md->mtx); @@ -77,7 +96,7 @@ void Mutex::Unlock() { #if defined(TESSESFRAMEWORK_ENABLE_THREADING) auto md = this->data.GetField(); #if defined(_WIN32) - ReleaseMutex(md->mtx); + LeaveCriticalSection(&(md->mtx)); #else pthread_mutex_unlock(&md->mtx); #endif @@ -87,12 +106,88 @@ bool Mutex::TryLock() { #if defined(TESSESFRAMEWORK_ENABLE_THREADING) auto md = this->data.GetField(); #if defined(_WIN32) - return WaitForSingleObject(md->mtx, 100) == WAIT_OBJECT_0; - + return TryEnterCriticalSection(&md->mtx) != FALSE; #else return pthread_mutex_trylock(&md->mtx) == 0; #endif #endif } Mutex::~Mutex() {} + +Cond::Cond() { +#if defined(TESSESFRAMEWORK_ENABLE_THREADING) + auto md = this->data.AllocField(); +#if defined(_WIN32) + InitializeConditionVariable(&md->cond); +#else + pthread_cond_init(&md->cond, NULL); + +#endif +#endif +} + +void Cond::Wait(Mutex *mtx) { + if (mtx == nullptr) + return; +#if defined(TESSESFRAMEWORK_ENABLE_THREADING) + auto mcd = this->data.GetField(); + auto md = mtx->data.GetField(); +#if defined(_WIN32) + SleepConditionVariableCS(&mcd->cond, &md->mtx, INFINITE); +#else + pthread_cond_wait(&mcd->cond, &md->mtx); +#endif +#endif +} +bool Cond::Wait(Mutex *mtx, uint32_t milliseconds) { + if (mtx == nullptr) + return false; +#if defined(TESSESFRAMEWORK_ENABLE_THREADING) + auto mcd = this->data.GetField(); + auto md = mtx->data.GetField(); +#if defined(_WIN32) + return SleepConditionVariableCS(&mcd->cond, &md->mtx, milliseconds) != + FALSE; +#else + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += milliseconds / 1000; + ts.tv_nsec += (milliseconds % 1000) * 1000000L; + if (ts.tv_nsec >= 1000000000L) { + ts.tv_sec++; + ts.tv_nsec -= 1000000000L; + } + return pthread_cond_timedwait(&mcd->cond, &md->mtx, &ts) == 0; +#endif +#endif + return false; +} +bool Cond::Wait(Mutex *mtx, Date::TimeSpan ts) { + return Wait(mtx, (uint32_t)ts.TotalSeconds() * 1000); +} +void Cond::Signal() { +#if defined(TESSESFRAMEWORK_ENABLE_THREADING) + auto mcd = this->data.GetField(); + +#if defined(_WIN32) + WakeConditionVariable(&mcd->cond); +#else + pthread_cond_signal(&mcd->cond); +#endif +#endif +} +void Cond::Broadcast() { +#if defined(TESSESFRAMEWORK_ENABLE_THREADING) + auto mcd = this->data.GetField(); + +#if defined(_WIN32) + WakeConditionVariable(&mcd->cond); +#else + pthread_cond_broadcast(&mcd->cond); +#endif +#endif +} + +Cond::~Cond() {} + }; // namespace Tesses::Framework::Threading