mirror of
https://git.tesses.org/tesses50/tessesframework.git
synced 2026-10-01 17:14:13 +00:00
Compare commits
5
Commits
313e75b14c
..
v0.0.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95442f8acc | ||
|
|
dbeae862c5 | ||
|
|
5b54af1728 | ||
|
|
1223bf35c2 | ||
|
|
3bce736834 |
+47
-39
@@ -11,11 +11,12 @@ env:
|
||||
jobs:
|
||||
build-ppc:
|
||||
name: "🔨 Build for PowerPC"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: asrock
|
||||
container:
|
||||
image: git.tesses.org/tesses50/linux-ppc:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
url: https://git.tesses.org/tesses50/tessesframework.git
|
||||
- run: bash build-for-ppc.sh
|
||||
- uses: akkuman/gitea-release-action@v1
|
||||
env:
|
||||
@@ -24,48 +25,13 @@ jobs:
|
||||
prerelease: true
|
||||
files: |-
|
||||
artifacts/**
|
||||
|
||||
build-packages:
|
||||
name: "🔨 Build for everything else"
|
||||
runs-on: fw13-ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- container: "git.tesses.org/tesses50/linux-x64:latest"
|
||||
nfpmName: "amd64"
|
||||
debianName: "x86_64-linux-gnu"
|
||||
- container: "git.tesses.org/tesses50/linux-x86:latest"
|
||||
nfpmName: "386"
|
||||
debianName: "i386-linux-gnu"
|
||||
- container: "git.tesses.org/tesses50/linux-arm:latest"
|
||||
nfpmName: "arm7"
|
||||
debianName: "arm-linux-gnueabihf"
|
||||
- container: "git.tesses.org/tesses50/linux-arm64:latest"
|
||||
nfpmName: "arm64"
|
||||
debianName: "aarch64-linux-gnu"
|
||||
- container: "git.tesses.org/tesses50/linux-riscv64:latest"
|
||||
nfpmName: "riscv64"
|
||||
debianName: "riscv64-linux-gnu"
|
||||
|
||||
container:
|
||||
image: ${{ matrix.container }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: bash build-for-nfpm.sh ${{ matrix.nfpmName }} ${{ matrix.debianName }}
|
||||
- uses: akkuman/gitea-release-action@v1
|
||||
env:
|
||||
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
|
||||
with:
|
||||
prerelease: true
|
||||
files: |-
|
||||
artifacts/**
|
||||
|
||||
update-tap-and-build-win32:
|
||||
name: "🔨 Build win32 and update the tap 🍺"
|
||||
runs-on: global-container-mingw
|
||||
runs-on: msi-walmart
|
||||
container: git.tesses.org/tesses50/global-containers-mingw:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
url: https://git.tesses.org/tesses50/tessesframework.git
|
||||
- run: |
|
||||
mkdir artifacts
|
||||
env -C Packaging/WinToolsIntel bash build.sh
|
||||
@@ -92,3 +58,45 @@ jobs:
|
||||
git add .
|
||||
git commit -m "Push tessesframework=${{ env.VERSION }}"
|
||||
git push
|
||||
build-packages:
|
||||
name: "🔨 Build for everything else"
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- container: "git.tesses.org/tesses50/linux-x64:latest"
|
||||
puter: "msi-walmart"
|
||||
nfpmName: "amd64"
|
||||
debianName: "x86_64-linux-gnu"
|
||||
- container: "git.tesses.org/tesses50/linux-x86:latest"
|
||||
puter: "asrock"
|
||||
nfpmName: "386"
|
||||
debianName: "i386-linux-gnu"
|
||||
- container: "git.tesses.org/tesses50/linux-arm:latest"
|
||||
puter: "fw13-ubuntu-latest"
|
||||
nfpmName: "arm7"
|
||||
debianName: "arm-linux-gnueabihf"
|
||||
- container: "git.tesses.org/tesses50/linux-arm64:latest"
|
||||
puter: "msi-walmart"
|
||||
nfpmName: "arm64"
|
||||
debianName: "aarch64-linux-gnu"
|
||||
- container: "git.tesses.org/tesses50/linux-riscv64:latest"
|
||||
nfpmName: "riscv64"
|
||||
debianName: "riscv64-linux-gnu"
|
||||
puter: "asrock"
|
||||
runs-on: ${{ matrix.puter }}
|
||||
container:
|
||||
image: ${{ matrix.container }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
url: https://git.tesses.org/tesses50/tessesframework.git
|
||||
- run: bash build-for-nfpm.sh ${{ matrix.nfpmName }} ${{ matrix.debianName }}
|
||||
- uses: akkuman/gitea-release-action@v1
|
||||
env:
|
||||
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
|
||||
with:
|
||||
prerelease: true
|
||||
files: |-
|
||||
artifacts/**
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
include(GNUInstallDirs)
|
||||
include(CheckLibraryExists)
|
||||
include(FetchContent)
|
||||
include(cmake/sources.cmake)
|
||||
include(cmake/options.cmake)
|
||||
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
GENERATE_HTML = YES
|
||||
GENERATE_LATEX = YES
|
||||
|
||||
HTML_OUTPUT = doxy_html
|
||||
LATEX_OUTPUT = doxy_latex
|
||||
|
||||
# optional but common
|
||||
RECURSIVE = YES
|
||||
|
||||
@@ -22,6 +22,7 @@ Tesses Framework
|
||||
- EventLoop
|
||||
- Basic crytography using mbedtls
|
||||
- a LCG Random number generator (do not use for crypto, use it for non security things like number guessing games and stuff, where rand would be ok)
|
||||
- Simple HTML parser
|
||||
|
||||
## Supported Platforms
|
||||
- Linux
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "TessesFramework/TessesFramework.hpp"
|
||||
using namespace Tesses::Framework::Streams;
|
||||
using namespace Tesses::Framework::Serialization;
|
||||
using namespace Tesses::Framework::TextStreams;
|
||||
|
||||
Json::JToken Html2JToken(std::shared_ptr<Html::HtmlNode> node) {
|
||||
if (node->isText) {
|
||||
return node->text_or_tag;
|
||||
} else {
|
||||
Json::JObject obj;
|
||||
obj.SetValue("Tag", node->text_or_tag);
|
||||
Json::JArray attrs;
|
||||
for (auto &item : node->attributes) {
|
||||
if (item.second)
|
||||
attrs.Add(Json::JObject(
|
||||
{{"Key", item.first}, {"Value", item.second.value()}}));
|
||||
else
|
||||
attrs.Add(
|
||||
Json::JObject({{"Key", item.first}, {"Value", nullptr}}));
|
||||
}
|
||||
obj.SetValue("Attributes", attrs);
|
||||
|
||||
Json::JArray childern;
|
||||
|
||||
for (auto &item : node->children)
|
||||
childern.Add(Html2JToken(item));
|
||||
|
||||
obj.SetValue("Children", childern);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<FileStream> OpenWrite(std::string dest) {
|
||||
if (dest == "-") {
|
||||
return std::make_shared<FileStream>(stdout, false, "w");
|
||||
} else {
|
||||
auto strm = std::make_shared<FileStream>(dest, "w");
|
||||
if (!strm->CanWrite()) {
|
||||
return nullptr;
|
||||
}
|
||||
return strm;
|
||||
}
|
||||
}
|
||||
std::shared_ptr<FileStream> OpenRead(std::string src) {
|
||||
if (src == "-") {
|
||||
return std::make_shared<FileStream>(stdin, false, "r");
|
||||
} else {
|
||||
auto strm = std::make_shared<FileStream>(src, "r");
|
||||
if (!strm->CanRead()) {
|
||||
return nullptr;
|
||||
}
|
||||
return strm;
|
||||
}
|
||||
}
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 3) {
|
||||
std::cout << "USAGE: " << argv[0] << " SRC DEST" << std::endl;
|
||||
std::cout << "SRC: html file or - for stdin to convert to json doc"
|
||||
<< std::endl;
|
||||
std::cout << "DEST: json file or - for stdout" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
auto src = OpenRead(argv[1]);
|
||||
|
||||
auto dest = OpenWrite(argv[2]);
|
||||
|
||||
if (src == nullptr) {
|
||||
|
||||
std::cerr << "ERROR: Input could not be read" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
if (dest == nullptr) {
|
||||
std::cerr << "ERROR: Output could not be read" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
StreamWriter writer(dest);
|
||||
|
||||
auto html = Html::Parse(std::make_shared<StreamReader>(src));
|
||||
|
||||
writer.WriteLine(Json::Json::Encode(Html2JToken(html)));
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "TessesFramework/TessesFramework.hpp"
|
||||
using namespace Tesses::Framework::Streams;
|
||||
using namespace Tesses::Framework::Serialization;
|
||||
using namespace Tesses::Framework::TextStreams;
|
||||
|
||||
std::shared_ptr<Stream> OpenRead(std::string src) {
|
||||
|
||||
if (src == "-") {
|
||||
return std::make_shared<FileStream>(stdin, false, "r");
|
||||
} else if (src.find("http://") == 0 || src.find("https://") == 0) {
|
||||
Tesses::Framework::Http::HttpRequest req;
|
||||
req.url = src;
|
||||
req.method = "GET";
|
||||
Tesses::Framework::Http::HttpResponse resp(req);
|
||||
|
||||
if ((int)resp.statusCode < 200 || (int)resp.statusCode > 299) {
|
||||
throw std::runtime_error(
|
||||
"Response does not indicate success: " +
|
||||
std::to_string((int)resp.statusCode) + " " +
|
||||
Tesses::Framework::Http::HttpUtils::StatusCodeString(
|
||||
resp.statusCode) +
|
||||
".");
|
||||
}
|
||||
return resp.ReadAsStream();
|
||||
} else {
|
||||
auto strm = std::make_shared<FileStream>(src, "r");
|
||||
if (!strm->CanRead()) {
|
||||
return nullptr;
|
||||
}
|
||||
return strm;
|
||||
}
|
||||
}
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) {
|
||||
std::cout << "USAGE: " << argv[0] << " SRC" << std::endl;
|
||||
std::cout << "SRC: html file or url or - for stdin" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
auto src = OpenRead(argv[1]);
|
||||
|
||||
if (src == nullptr) {
|
||||
|
||||
std::cerr << "ERROR: Input could not be read" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto html = Html::Parse(std::make_shared<StreamReader>(src));
|
||||
std::vector<std::shared_ptr<const Html::HtmlNode>> titleTags;
|
||||
html->FindNodes("title", titleTags);
|
||||
|
||||
for (auto &title : titleTags) {
|
||||
Tesses::Framework::Console::WriteView("Title: ");
|
||||
Tesses::Framework::Console::WriteLine(title->InnerText());
|
||||
break;
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<const Html::HtmlNode>> metaTags;
|
||||
html->FindNodes("meta", metaTags);
|
||||
|
||||
for (auto &meta : metaTags) {
|
||||
std::optional<std::string> prop, content;
|
||||
for (auto &[k, v] : meta->attributes) {
|
||||
if (k == "property" && v)
|
||||
prop = *v;
|
||||
if (k == "content" && v)
|
||||
content = *v;
|
||||
}
|
||||
|
||||
if (prop && content && prop->find("og:") == 0) {
|
||||
Tesses::Framework::Console::WriteView(prop.value());
|
||||
Tesses::Framework::Console::WriteView(": ");
|
||||
Tesses::Framework::Console::WriteLineView(content.value());
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "TessesFramework/TessesFramework.hpp"
|
||||
#include <iostream>
|
||||
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=] <url_root>");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Tesses::Framework::Http::HttpServer server(
|
||||
port, std::make_shared<ReverseProxyServer>(args.positional[0]));
|
||||
server.StartAccepting();
|
||||
TF_RunEventLoop();
|
||||
std::cout << "Closing server" << std::endl;
|
||||
TF_Quit();
|
||||
|
||||
return 0;
|
||||
}
|
||||
+8
-1
@@ -1,7 +1,14 @@
|
||||
#include <TessesFramework/TessesFramework.hpp>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ int main(int argc, char **argv) {
|
||||
Args args(argc, argv);
|
||||
if (args.positional.size() < 2)
|
||||
usage(args);
|
||||
|
||||
int64_t num;
|
||||
for (auto &opt : args.options) {
|
||||
if (opt.first == "tracker")
|
||||
trackers.push_back(opt.second);
|
||||
@@ -49,8 +49,9 @@ int main(int argc, char **argv) {
|
||||
comment = opt.second;
|
||||
else if (opt.first == "created_by")
|
||||
created_by = opt.second;
|
||||
else if (opt.first == "piece_length")
|
||||
pieceLength = std::stoll(opt.second);
|
||||
else if (opt.first == "piece_length" &&
|
||||
Serialization::BitConverter::TryParseSigned(opt.second, num))
|
||||
pieceLength = num;
|
||||
}
|
||||
|
||||
for (auto &flag : args.flags) {
|
||||
|
||||
+4
-1
@@ -1,7 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.8
|
||||
Fixed a stoll bug that was there since day one, html parser, fix pkgconfig
|
||||
|
||||
## 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
|
||||
|
||||
+14
-1
@@ -66,7 +66,20 @@ 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}")
|
||||
|
||||
|
||||
add_executable(thtml2json apps/thtml2json.cpp)
|
||||
target_link_libraries(thtml2json PUBLIC tessesframework)
|
||||
install(TARGETS thtml2json DESTINATION "${CMAKE_INSTALL_BINDIR}")
|
||||
|
||||
|
||||
add_executable(topengraph apps/topengraph.cpp)
|
||||
target_link_libraries(topengraph PUBLIC tessesframework)
|
||||
install(TARGETS topengraph DESTINATION "${CMAKE_INSTALL_BINDIR}")
|
||||
endif()
|
||||
@@ -18,21 +18,19 @@ if(NOT mbedtls_POPULATED)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build static" FORCE)
|
||||
if(NOT TESSESFRAMEWORK_STATIC)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON CACHE BOOL "Build PIC" FORCE)
|
||||
set(CMAKE_SKIP_INSTALL_RULES ON)
|
||||
set(DISABLE_PACKAGE_CONFIG_AND_INSTALL ON CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
|
||||
|
||||
FetchContent_Populate(mbedtls)
|
||||
if(NOT TESSESFRAMEWORK_STATIC)
|
||||
install(CODE "set(CMAKE_INSTALL_LOCAL_ONLY TRUE)" ALL_COMPONENTS)
|
||||
endif()
|
||||
|
||||
|
||||
|
||||
if(NOT TESSESFRAMEWORK_STATIC)
|
||||
add_subdirectory(${mbedtls_SOURCE_DIR} ${mbedtls_BINARY_DIR} EXCLUDE_FROM_ALL)
|
||||
else()
|
||||
add_subdirectory(${mbedtls_SOURCE_DIR} ${mbedtls_BINARY_DIR})
|
||||
|
||||
if(NOT TESSESFRAMEWORK_STATIC)
|
||||
set(CMAKE_SKIP_INSTALL_RULES OFF)
|
||||
endif()
|
||||
|
||||
endif()
|
||||
else()
|
||||
if(TESSESFRAMEWORK_MBEDTLS_PORTLIB STREQUAL "")
|
||||
|
||||
@@ -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
|
||||
@@ -20,6 +22,7 @@ src/Serialization/Json.cpp
|
||||
src/Serialization/SQLite.cpp
|
||||
src/Serialization/BitConverter.cpp
|
||||
src/Serialization/Bencode.cpp
|
||||
src/Serialization/Html.cpp
|
||||
src/Platform/Environment.cpp
|
||||
src/Platform/Process.cpp
|
||||
src/Streams/FileStream.cpp
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
set(TESSESFRAMEWORK_MAJOR_VERSION 0)
|
||||
set(TESSESFRAMEWORK_MINOR_VERSION 0)
|
||||
set(TESSESFRAMEWORK_PATCH_VERSION 7)
|
||||
set(TESSESFRAMEWORK_PATCH_VERSION 8)
|
||||
|
||||
@@ -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<Tesses::Framework::Streams::Stream> 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<Tesses::Framework::Streams::Stream> 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<Tesses::Framework::Streams::Stream> 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();
|
||||
};
|
||||
|
||||
|
||||
@@ -316,6 +316,29 @@ typedef enum {
|
||||
*/
|
||||
bool PBKDF2(std::vector<uint8_t> &output, std::string pass,
|
||||
std::vector<uint8_t> &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<uint8_t> &output, std::string personal_str);
|
||||
|
||||
struct CertificateKeyStore {
|
||||
CertificateKeyStore() = default;
|
||||
CertificateKeyStore(std::string certificate, std::string key,
|
||||
std::optional<std::string> chain = std::nullopt,
|
||||
std::string password = "")
|
||||
: certificate(certificate), key(key), chain(chain), password(password) {
|
||||
}
|
||||
|
||||
std::string certificate;
|
||||
std::string key;
|
||||
std::optional<std::string> chain;
|
||||
std::string password;
|
||||
};
|
||||
|
||||
} // namespace Tesses::Framework::Crypto
|
||||
@@ -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) {
|
||||
|
||||
@@ -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> 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> vfs, VFSPath path,
|
||||
std::vector<std::string> &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> vfs, VFSPath path,
|
||||
std::vector<uint8_t> &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> 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<std::string> the file's lines
|
||||
*/
|
||||
std::vector<std::string> ReadAllLines(std::shared_ptr<VFS> vfs, VFSPath path);
|
||||
|
||||
std::vector<uint8_t> ReadAllBytes(std::shared_ptr<VFS> vfs, VFSPath path);
|
||||
void WriteAllText(std::shared_ptr<VFS> vfs, VFSPath path,
|
||||
const std::string &text);
|
||||
|
||||
@@ -38,8 +38,8 @@ class HiddenField {
|
||||
HiddenField(HiddenFieldData *data);
|
||||
void SetField(HiddenFieldData *data);
|
||||
template <typename T> T GetField() { return dynamic_cast<T>(ptr); }
|
||||
template <typename T> T *AllocField() {
|
||||
auto v = new T();
|
||||
template <typename T, typename... TArgs> T *AllocField(TArgs &&...args) {
|
||||
auto v = new T(std::forward<TArgs>(args)...);
|
||||
SetField(v);
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -24,11 +24,15 @@
|
||||
|
||||
namespace Tesses::Framework::Http {
|
||||
class ChangeableServer {
|
||||
std::shared_ptr<IHttpServer> server;
|
||||
Tesses::Framework::Threading::Mutex mtx;
|
||||
|
||||
public:
|
||||
ChangeableServer();
|
||||
ChangeableServer(std::shared_ptr<IHttpServer> original);
|
||||
std::shared_ptr<IHttpServer> server;
|
||||
bool Handle(ServerContext &ctx);
|
||||
void SetServer(std::shared_ptr<IHttpServer> server);
|
||||
std::shared_ptr<IHttpServer> GetServer();
|
||||
~ChangeableServer();
|
||||
};
|
||||
} // namespace Tesses::Framework::Http
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "../Filesystem/VFS.hpp"
|
||||
#include "../Filesystem/VFSFix.hpp"
|
||||
#include "HttpServer.hpp"
|
||||
|
||||
namespace Tesses::Framework::Http {
|
||||
class DomainServer : public IHttpServer {
|
||||
std::shared_ptr<IHttpServer> root;
|
||||
std::map<std::string, std::shared_ptr<IHttpServer>> servers;
|
||||
Tesses::Framework::Threading::Mutex mtx;
|
||||
|
||||
public:
|
||||
DomainServer();
|
||||
DomainServer(std::shared_ptr<IHttpServer> root);
|
||||
void Set(std::string domain, std::shared_ptr<IHttpServer> server);
|
||||
void Unset(std::string domain);
|
||||
void Clear();
|
||||
bool Handle(ServerContext &ctx);
|
||||
};
|
||||
} // namespace Tesses::Framework::Http
|
||||
@@ -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<Crypto::CertificateKeyStore> 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<HttpRequestBody> body;
|
||||
|
||||
static std::shared_ptr<Tesses::Framework::Streams::Stream>
|
||||
EstablishConnection(Uri uri, bool ignoreSSLErrors,
|
||||
std::string trusted_root_cert_bundle);
|
||||
EstablishConnection(
|
||||
Uri uri, bool ignoreSSLErrors, std::string trusted_root_cert_bundle,
|
||||
std::optional<Crypto::CertificateKeyStore> mTLS_keyStore);
|
||||
static std::shared_ptr<Tesses::Framework::Streams::Stream>
|
||||
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<Crypto::CertificateKeyStore> mTLS_keyStore);
|
||||
|
||||
void SendRequest(std::shared_ptr<Tesses::Framework::Streams::Stream> strm);
|
||||
};
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "HttpClient.hpp"
|
||||
#include "HttpServer.hpp"
|
||||
#include <unordered_set>
|
||||
|
||||
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<ReverseProxyAction(ServerContext &, HttpResponse &)> rc);
|
||||
ReverseProxyConnectionBuilder &
|
||||
WithWhitelistedUpgrade(std::string protocol = "websocket");
|
||||
bool Handle();
|
||||
|
||||
private:
|
||||
ServerContext &m_ctx;
|
||||
std::optional<std::string> m_url;
|
||||
HttpDictionary m_reqheaders;
|
||||
std::unordered_set<std::string> m_whitelistedupgrades;
|
||||
std::function<ReverseProxyAction(ServerContext &, HttpResponse &)> m_rc;
|
||||
bool m_fail = false;
|
||||
};
|
||||
|
||||
class ReverseProxyServer : public IHttpServer {
|
||||
private:
|
||||
std::string url;
|
||||
std::function<ReverseProxyAction(ServerContext &,
|
||||
ReverseProxyConnectionBuilder &, Uri &)>
|
||||
request_callback;
|
||||
std::function<ReverseProxyAction(ServerContext &, HttpResponse &)>
|
||||
response_callback;
|
||||
bool essentialheaders;
|
||||
|
||||
public:
|
||||
ReverseProxyServer(
|
||||
std::string url,
|
||||
std::function<ReverseProxyAction(
|
||||
ServerContext &, ReverseProxyConnectionBuilder &, Uri &)>
|
||||
request_callback = nullptr,
|
||||
std::function<ReverseProxyAction(ServerContext &, HttpResponse &)>
|
||||
response_callback = nullptr,
|
||||
bool essentialheaders = true);
|
||||
bool Handle(ServerContext &ctx);
|
||||
};
|
||||
} // namespace Tesses::Framework::Http
|
||||
@@ -98,7 +98,7 @@ class ServerContext {
|
||||
void SendErrorPage(bool showPath);
|
||||
void SendNotFound();
|
||||
void SendBadRequest();
|
||||
void SendException(std::exception &ex);
|
||||
void SendException(const std::exception &ex);
|
||||
void SendServerSentEvents(std::shared_ptr<ServerSentEvents> sse);
|
||||
std::shared_ptr<Tesses::Framework::Streams::Stream> OpenResponseStream();
|
||||
std::shared_ptr<Tesses::Framework::Streams::Stream> OpenRequestStream();
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
#pragma once
|
||||
#include "../Common.hpp"
|
||||
#include "../Date/Date.hpp"
|
||||
|
||||
#include "../Filesystem/VFS.hpp"
|
||||
#include "../Filesystem/VFSFix.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
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 {
|
||||
@@ -146,9 +157,9 @@ class Uri {
|
||||
std::string GetPathAndQuery();
|
||||
uint16_t GetPort();
|
||||
std::string HostPort();
|
||||
bool Relative(std::string url, Uri &uri);
|
||||
bool Relative(std::string_view url, Uri &uri);
|
||||
std::string ToString();
|
||||
static bool TryParse(std::string url, Uri &uri);
|
||||
static bool TryParse(std::string_view url, Uri &uri);
|
||||
std::string scheme;
|
||||
std::string host;
|
||||
uint16_t port;
|
||||
@@ -169,30 +180,42 @@ class HttpUtils {
|
||||
bool isUppercase);
|
||||
static void BytesToHex(std::string &text, const std::vector<uint8_t> &data,
|
||||
bool isUppercase);
|
||||
static std::vector<uint8_t> HexToBytes(const std::string &text);
|
||||
static void HexToBytes(std::vector<uint8_t> &data, const std::string &text);
|
||||
static std::string MimeType(std::filesystem::path p);
|
||||
static std::vector<uint8_t> HexToBytes(std::string_view text);
|
||||
static void HexToBytes(std::vector<uint8_t> &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<std::string> 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<std::string> 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);
|
||||
// not all escapes are supported
|
||||
static std::string HtmlDecode(std::string_view v);
|
||||
static std::string HtmlP(std::string_view text);
|
||||
static void SplitString(std::vector<std::string> &out,
|
||||
std::string_view text, std::string_view delimiter,
|
||||
std::size_t maxCnt = std::string::npos);
|
||||
static std::vector<std::string>
|
||||
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
|
||||
@@ -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<IHttpServer> server);
|
||||
void Unmount(std::string path);
|
||||
bool Handle(ServerContext &ctx);
|
||||
~MountableServer();
|
||||
};
|
||||
} // namespace Tesses::Framework::Http
|
||||
@@ -42,6 +42,7 @@ class RouteServer : public IHttpServer {
|
||||
};
|
||||
std::vector<RouteServerRoute> routes;
|
||||
std::shared_ptr<IHttpServer> 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
|
||||
@@ -83,6 +83,12 @@ class BitConverter {
|
||||
|
||||
static void FromUuid(uint8_t &b, const Uuid &uuid);
|
||||
|
||||
static bool TryParseSigned(const std::string &str, int64_t &val,
|
||||
int base = 10);
|
||||
static bool TryParseUnsigned(const std::string &str, uint64_t &val,
|
||||
int base = 10);
|
||||
static bool TryParseDouble(const std::string &str, double &val);
|
||||
|
||||
static inline bool IsLittleEndian() {
|
||||
uint8_t a[2];
|
||||
a[0] = 0x01;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "../TextStreams/TextReader.hpp"
|
||||
namespace Tesses::Framework::Serialization::Html {
|
||||
|
||||
struct HtmlNode;
|
||||
struct HtmlNode : public std::enable_shared_from_this<HtmlNode> {
|
||||
|
||||
std::string text_or_tag;
|
||||
std::vector<std::pair<std::string, std::optional<std::string>>> attributes;
|
||||
std::vector<std::shared_ptr<HtmlNode>> children;
|
||||
bool isText = false;
|
||||
|
||||
void FindNodes(std::string_view tag,
|
||||
std::vector<std::shared_ptr<const HtmlNode>> &nodes) const;
|
||||
std::vector<std::shared_ptr<const HtmlNode>>
|
||||
FindNodes(std::string_view tag) const;
|
||||
|
||||
void ToString(std::string &str) const;
|
||||
std::string ToString() const;
|
||||
|
||||
void InnerText(std::string &str) const;
|
||||
std::string InnerText() const;
|
||||
static std::shared_ptr<HtmlNode> FromText(std::string_view text);
|
||||
static std::shared_ptr<HtmlNode>
|
||||
FromNode(std::string_view name,
|
||||
std::initializer_list<
|
||||
std::pair<std::string, std::optional<std::string>>>
|
||||
attrs,
|
||||
std::initializer_list<std::shared_ptr<HtmlNode>> nodes);
|
||||
static std::shared_ptr<HtmlNode> FromNode(
|
||||
std::string_view name,
|
||||
std::vector<std::pair<std::string, std::optional<std::string>>> attrs,
|
||||
std::vector<std::shared_ptr<HtmlNode>> nodes);
|
||||
|
||||
template <typename IttrAttrs, typename IttrNodes>
|
||||
static std::shared_ptr<HtmlNode>
|
||||
FromNode(std::string_view name, IttrAttrs attrBegin, IttrAttrs attrEnd,
|
||||
IttrNodes nodesBegin, IttrNodes nodesEnd) {
|
||||
auto node = std::make_shared<HtmlNode>();
|
||||
node->isText = false;
|
||||
node->text_or_tag = std::string{name};
|
||||
node->attributes.insert(node->attributes.end(), attrBegin, attrEnd);
|
||||
node->children.insert(node->children.end(), nodesBegin, nodesEnd);
|
||||
return node;
|
||||
}
|
||||
};
|
||||
|
||||
std::shared_ptr<HtmlNode>
|
||||
Parse(std::shared_ptr<TextStreams::TextReader> reader);
|
||||
} // namespace Tesses::Framework::Serialization::Html
|
||||
@@ -53,7 +53,7 @@ class NetworkStream : public Stream {
|
||||
int32_t sock;
|
||||
bool owns;
|
||||
bool success;
|
||||
bool endOfStream;
|
||||
std::atomic<bool> 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
|
||||
|
||||
@@ -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<Stream> strm, size_t buffSize = 1024);
|
||||
void CopyToLimit(std::shared_ptr<Stream> 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();
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
@@ -50,6 +50,7 @@
|
||||
#include "Random.hpp"
|
||||
#include "Serialization/Bencode.hpp"
|
||||
#include "Serialization/BitConverter.hpp"
|
||||
#include "Serialization/Html.hpp"
|
||||
#include "Serialization/Json.hpp"
|
||||
#include "Serialization/SQLite.hpp"
|
||||
#include "Streams/BufferedStream.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
|
||||
@@ -11,6 +11,8 @@ set(PKGCONFIG_DEPS "")
|
||||
endif()
|
||||
|
||||
configure_file(tessesframework.pc.in tessesframework.pc @ONLY)
|
||||
message(NOTICE "SrcFile ${CMAKE_CURRENT_BINARY_DIR}/tessesframework.pc")
|
||||
message(NOTICE "Outdir ${CMAKE_INSTALL_LIBDIR}/pkgconfig")
|
||||
install(FILES
|
||||
${CMAKE_CURRENT_BINARY_DIR}/tessesframework.pc
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||
|
||||
@@ -45,13 +45,24 @@ class ClientTLSPrivateData {
|
||||
public:
|
||||
bool eos;
|
||||
bool success;
|
||||
bool mTLS;
|
||||
std::shared_ptr<Stream> 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<Tesses::Framework::Streams::Stream> 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<void *>(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<void *>(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<ClientTLSPrivateData *>(this->privateData);
|
||||
@@ -245,4 +378,18 @@ ClientTLSStream::~ClientTLSStream() {
|
||||
delete static_cast<ClientTLSPrivateData *>(this->privateData);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ClientTLSStream::SetSendTimeout(uint64_t seconds) {
|
||||
#if defined(TESSESFRAMEWORK_ENABLE_MBED)
|
||||
static_cast<ClientTLSPrivateData *>(this->privateData)
|
||||
->strm->SetSendTimeout(seconds);
|
||||
#endif
|
||||
}
|
||||
void ClientTLSStream::SetRecvTimeout(uint64_t seconds) {
|
||||
#if defined(TESSESFRAMEWORK_ENABLE_MBED)
|
||||
static_cast<ClientTLSPrivateData *>(this->privateData)
|
||||
->strm->SetRecvTimeout(seconds);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace Tesses::Framework::Crypto
|
||||
+34
-10
@@ -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)
|
||||
|
||||
@@ -296,6 +296,7 @@ VFSPath VFSPath::CollapseRelativeParents() const {
|
||||
newpath.path = parts;
|
||||
return newpath;
|
||||
}
|
||||
|
||||
VFSPath VFSPath::CurrentDirectoryAsRelative() {
|
||||
VFSPath path;
|
||||
path.relative = true;
|
||||
|
||||
@@ -27,9 +27,22 @@ ChangeableServer::ChangeableServer(std::shared_ptr<IHttpServer> original) {
|
||||
this->server = original;
|
||||
}
|
||||
|
||||
std::shared_ptr<IHttpServer> ChangeableServer::GetServer() {
|
||||
mtx.Lock();
|
||||
auto server = this->server;
|
||||
mtx.Unlock();
|
||||
return server;
|
||||
}
|
||||
void ChangeableServer::SetServer(std::shared_ptr<IHttpServer> 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() {}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#include "TessesFramework/Http/DomainServer.hpp"
|
||||
|
||||
namespace Tesses::Framework::Http {
|
||||
DomainServer::DomainServer() {}
|
||||
DomainServer::DomainServer(std::shared_ptr<IHttpServer> root) : root(root) {}
|
||||
void DomainServer::Set(std::string domain,
|
||||
std::shared_ptr<IHttpServer> 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<IHttpServer> 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+34
-22
@@ -65,10 +65,12 @@ StreamHttpRequestBody::StreamHttpRequestBody(std::shared_ptr<Stream> 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<Tesses::Framework::Streams::Stream> 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<Stream>
|
||||
HttpRequest::EstablishConnection(Uri uri, bool ignoreSSLErrors,
|
||||
std::string trusted_root_cert_bundle) {
|
||||
std::shared_ptr<Stream> HttpRequest::EstablishConnection(
|
||||
Uri uri, bool ignoreSSLErrors, std::string trusted_root_cert_bundle,
|
||||
std::optional<Crypto::CertificateKeyStore> mTLS_keyStore) {
|
||||
if (uri.scheme == "http:" || uri.scheme == "ws:") {
|
||||
return std::make_shared<NetworkStream>(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<ClientTLSStream>(
|
||||
netStrm, !ignoreSSLErrors, uri.host, trusted_root_cert_bundle);
|
||||
return mTLS_keyStore
|
||||
? std::make_shared<ClientTLSStream>(
|
||||
netStrm, !ignoreSSLErrors, uri.host,
|
||||
trusted_root_cert_bundle, mTLS_keyStore.value())
|
||||
: std::make_shared<ClientTLSStream>(
|
||||
netStrm, !ignoreSSLErrors, uri.host,
|
||||
trusted_root_cert_bundle);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
std::shared_ptr<Stream>
|
||||
HttpRequest::EstablishUnixPathConnection(std::string unixPath, Uri uri,
|
||||
bool ignoreSSLErrors,
|
||||
std::string trusted_root_cert_bundle) {
|
||||
std::shared_ptr<Stream> HttpRequest::EstablishUnixPathConnection(
|
||||
std::string unixPath, Uri uri, bool ignoreSSLErrors,
|
||||
std::string trusted_root_cert_bundle,
|
||||
std::optional<Crypto::CertificateKeyStore> mTLS_keyStore) {
|
||||
if (uri.scheme == "http:" || uri.scheme == "ws:") {
|
||||
return std::make_shared<NetworkStream>(unixPath, false);
|
||||
} else if (uri.scheme == "https:" || uri.scheme == "wss:") {
|
||||
auto netStrm = std::make_shared<NetworkStream>(unixPath, false);
|
||||
if (netStrm == nullptr)
|
||||
return nullptr;
|
||||
return std::make_shared<ClientTLSStream>(
|
||||
netStrm, !ignoreSSLErrors, uri.host, trusted_root_cert_bundle);
|
||||
return mTLS_keyStore
|
||||
? std::make_shared<ClientTLSStream>(
|
||||
netStrm, !ignoreSSLErrors, uri.host,
|
||||
trusted_root_cert_bundle, mTLS_keyStore.value())
|
||||
: std::make_shared<ClientTLSStream>(
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "TessesFramework/Http/HttpReverseProxy.hpp"
|
||||
namespace Tesses::Framework::Http {
|
||||
|
||||
static std::string CONNECTION = "CONNECTION";
|
||||
static std::string UPGRADE = "UPGRADE";
|
||||
|
||||
static std::initializer_list<std::string> 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<std::string> 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<std::string> &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<std::string> 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<std::string> 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<ReverseProxyAction(ServerContext &, HttpResponse &)> rc) {
|
||||
this->m_rc = rc;
|
||||
return *this;
|
||||
}
|
||||
|
||||
static void
|
||||
StreamExchange(std::shared_ptr<Tesses::Framework::Streams::Stream> server,
|
||||
std::shared_ptr<Tesses::Framework::Streams::Stream> client) {
|
||||
Tesses::Framework::Threading::Thread server2client([server,
|
||||
client]() -> void {
|
||||
try {
|
||||
std::vector<uint8_t> 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<size_t>(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<uint8_t> 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<size_t>(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("<!DOCTYPE html>"
|
||||
"<html>"
|
||||
"<head><meta name=\"color-scheme\" "
|
||||
"content=\"dark light\"><title>500 Internal Server "
|
||||
"Error</title></head>"
|
||||
"<body>"
|
||||
"<h1>500 Internal Server Error</h1><p>" +
|
||||
HttpUtils::HtmlEncode(
|
||||
"Please chain the reverse proxy url with "
|
||||
".WithUrl(\"http://example.com/path/on/backend\")") +
|
||||
"</p>"
|
||||
"</body>"
|
||||
"</html>"
|
||||
|
||||
);
|
||||
|
||||
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<TextHttpRequestBody>(
|
||||
body, "application/x-www-form-urlencoded");
|
||||
} else {
|
||||
req.body = std::make_shared<StreamHttpRequestBody>(
|
||||
m_ctx.OpenRequestStream(), type);
|
||||
}
|
||||
} else if (isNotUpgradeOrGet) {
|
||||
req.body = std::make_shared<StreamHttpRequestBody>(
|
||||
m_ctx.OpenRequestStream(), "application/octet-stream");
|
||||
}
|
||||
std::unique_ptr<HttpResponse> resp;
|
||||
|
||||
try {
|
||||
resp = std::make_unique<HttpResponse>(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<std::string> 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<std::string> 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(
|
||||
"<!DOCTYPE html>"
|
||||
"<html>"
|
||||
"<head><meta name=\"color-scheme\" "
|
||||
"content=\"dark light\"><title>500 "
|
||||
"Internal Server "
|
||||
"Error</title></head>"
|
||||
"<body>"
|
||||
"<h1>500 Internal Server Error</h1><p>"
|
||||
"A redirect went beyond the reverse "
|
||||
"proxy "
|
||||
"root"
|
||||
"</p>"
|
||||
"</body>"
|
||||
"</html>"
|
||||
|
||||
);
|
||||
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<std::string> 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<ReverseProxyAction(ServerContext &,
|
||||
ReverseProxyConnectionBuilder &, Uri &)>
|
||||
request_callback,
|
||||
std::function<ReverseProxyAction(ServerContext &, HttpResponse &)>
|
||||
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
|
||||
+133
-45
@@ -39,6 +39,8 @@
|
||||
#include "TessesFramework/Filesystem/VFS.hpp"
|
||||
#include "TessesFramework/Filesystem/VFSFix.hpp"
|
||||
// clang-format on
|
||||
|
||||
#include "TessesFramework/Serialization/BitConverter.hpp"
|
||||
using FileStream = Tesses::Framework::Streams::FileStream;
|
||||
using Stream = Tesses::Framework::Streams::Stream;
|
||||
using SeekOrigin = Tesses::Framework::Streams::SeekOrigin;
|
||||
@@ -51,6 +53,11 @@ using namespace Tesses::Framework::TextStreams;
|
||||
|
||||
namespace Tesses::Framework::Http {
|
||||
|
||||
static const std::initializer_list<std::string> 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 +290,7 @@ class WSServer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx->requestHeaders.AnyEquals("Upgrade", "websocket")) {
|
||||
if (!ctx->requestHeaders.AnyEqualsCSV("Upgrade", "websocket")) {
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -602,12 +609,34 @@ uint16_t HttpServer::GetPort() {
|
||||
return server->GetPort();
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::shared_ptr<Stream> ServerContext::OpenResponseStream() {
|
||||
if (sent)
|
||||
return nullptr;
|
||||
int64_t length = -1;
|
||||
if (!this->responseHeaders.TryGetFirstInt("Content-Length", length))
|
||||
length = -1;
|
||||
try {
|
||||
if (!this->responseHeaders.TryGetFirstInt("Content-Length", length))
|
||||
length = -1;
|
||||
} catch (std::invalid_argument &invalid) {
|
||||
std::string error =
|
||||
"<!DOCTYPE html><html lang=\"en\"><head><meta "
|
||||
"charset=\"UTF-8\"> <meta name=\"viewport\" "
|
||||
"content=\"width=device-width, "
|
||||
"initial-scale=1.0\"><title>Internal Server Error at " +
|
||||
HttpUtils::HtmlEncode(this->originalPath) +
|
||||
"</title><meta name=\"color-scheme\" content=\"dark "
|
||||
"light\"></head><body><h1>Internal Server Error at " +
|
||||
HttpUtils::HtmlEncode(this->originalPath) +
|
||||
"</h1><p>what(): " + HttpUtils::HtmlEncode(invalid.what()) +
|
||||
"</p></body></html>";
|
||||
this->responseHeaders.SetValue("Content-Length",
|
||||
static_cast<int64_t>(error.size()));
|
||||
this->statusCode = StatusCode::InternalServerError;
|
||||
this->WriteHeaders();
|
||||
this->strm->WriteBlock(reinterpret_cast<const uint8_t *>(error.data()),
|
||||
error.size());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (this->version == "HTTP/1.1" && length == -1)
|
||||
this->responseHeaders.SetValue("Transfer-Encoding", "chunked");
|
||||
@@ -643,11 +672,15 @@ void HttpServer::StartAccepting() {
|
||||
uint16_t port;
|
||||
auto sock = svr->GetStream(ip, port);
|
||||
|
||||
if (!TF_IsRunning()) // we need to die, that continue is not what we
|
||||
// want if we must die from being done
|
||||
break;
|
||||
|
||||
TF_LOG("New Host IP: " + ip + ":" + std::to_string(port));
|
||||
|
||||
if (sock == nullptr) {
|
||||
std::cout << "STREAM ERROR" << std::endl;
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
TF_LOG("Before entering socket thread");
|
||||
|
||||
@@ -703,9 +736,14 @@ ServerContext::ServerContext(std::shared_ptr<Stream> strm, bool debug)
|
||||
}
|
||||
std::shared_ptr<Stream> ServerContext::GetStream() { return this->strm; }
|
||||
void ServerContext::SendBytes(std::vector<uint8_t> buff) {
|
||||
std::shared_ptr<MemoryStream> strm = std::make_shared<MemoryStream>(false);
|
||||
strm->GetBuffer() = buff;
|
||||
SendStream(strm);
|
||||
|
||||
if (sent)
|
||||
return;
|
||||
this->responseHeaders.SetValue("Content-Length",
|
||||
static_cast<int64_t>(buff.size()));
|
||||
auto resp = OpenResponseStream();
|
||||
if (resp)
|
||||
resp->WriteBlock(buff.data(), buff.size());
|
||||
}
|
||||
ServerContext &ServerContext::WithLastModified(Date::DateTime dt) {
|
||||
this->responseHeaders.SetValue("Last-Modified", dt);
|
||||
@@ -713,11 +751,13 @@ ServerContext &ServerContext::WithLastModified(Date::DateTime dt) {
|
||||
}
|
||||
|
||||
void ServerContext::SendText(std::string text) {
|
||||
std::shared_ptr<MemoryStream> strm = std::make_shared<MemoryStream>(false);
|
||||
|
||||
auto &buff = strm->GetBuffer();
|
||||
buff.insert(buff.end(), text.begin(), text.end());
|
||||
SendStream(strm);
|
||||
if (sent)
|
||||
return;
|
||||
this->responseHeaders.SetValue("Content-Length",
|
||||
static_cast<int64_t>(text.size()));
|
||||
auto resp = OpenResponseStream();
|
||||
if (resp)
|
||||
resp->WriteBlock(reinterpret_cast<uint8_t *>(text.data()), text.size());
|
||||
}
|
||||
void ServerContext::SendErrorPage(bool showPath) {
|
||||
if (sent)
|
||||
@@ -787,22 +827,34 @@ void ServerContext::SendStream(std::shared_ptr<Stream> strm) {
|
||||
int64_t begin = 0;
|
||||
int64_t end = -1;
|
||||
|
||||
if (dash.size() == 1 &&
|
||||
res[0].find_first_of('-') != std::string::npos) {
|
||||
// NUMBER-
|
||||
begin = std::stoll(dash[0]);
|
||||
} else if (dash.size() == 2) {
|
||||
if (dash.size() == 2) {
|
||||
// NUMBER-NUMBER
|
||||
// or
|
||||
//-NUMBER
|
||||
|
||||
if (dash[0].size() > 0) {
|
||||
// NUMBER-NUMBER
|
||||
begin = std::stoll(dash[0]);
|
||||
end = std::stoll(dash[1]);
|
||||
} else {
|
||||
if (dash[0].empty()) {
|
||||
//-NUMBER
|
||||
end = std::stoll(dash[1]);
|
||||
|
||||
if (!Serialization::BitConverter::TryParseSigned(
|
||||
dash[1], end))
|
||||
throw std::runtime_error(
|
||||
"Failed to parse the end of the Range header");
|
||||
} else if (dash[1].empty()) {
|
||||
if (!Serialization::BitConverter::TryParseSigned(
|
||||
dash[0], begin))
|
||||
throw std::runtime_error(
|
||||
"Failed to parse the end of the Range header");
|
||||
} else {
|
||||
// NUMBER-NUMBER
|
||||
if (!Serialization::BitConverter::TryParseSigned(
|
||||
dash[0], begin))
|
||||
throw std::runtime_error(
|
||||
"Failed to parse the beginning of the Range "
|
||||
"header");
|
||||
|
||||
if (!Serialization::BitConverter::TryParseSigned(
|
||||
dash[1], end))
|
||||
throw std::runtime_error(
|
||||
"Failed to parse the end of the Range header");
|
||||
}
|
||||
} else {
|
||||
this->statusCode = BadRequest;
|
||||
@@ -881,6 +933,8 @@ void ServerContext::SendStream(std::shared_ptr<Stream> strm) {
|
||||
} else {
|
||||
|
||||
auto chunkedStream = this->OpenResponseStream();
|
||||
if (!chunkedStream)
|
||||
throw std::runtime_error("Can't open response stream");
|
||||
|
||||
if (method != "HEAD")
|
||||
strm->CopyTo(chunkedStream);
|
||||
@@ -904,10 +958,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;
|
||||
}
|
||||
@@ -944,24 +994,28 @@ ServerContext &ServerContext::WithStatusCode(StatusCode code) {
|
||||
this->statusCode = code;
|
||||
return *this;
|
||||
}
|
||||
void ServerContext::SendException(std::exception &ex) {
|
||||
if (this->debug) {
|
||||
void ServerContext::SendException(const std::exception &ex) {
|
||||
try {
|
||||
if (this->debug) {
|
||||
|
||||
this->WithMimeType("text/html")
|
||||
.WithStatusCode(StatusCode::InternalServerError)
|
||||
.SendText("<!DOCTYPE html><html lang=\"en\"><head><meta "
|
||||
"charset=\"UTF-8\"> <meta name=\"viewport\" "
|
||||
"content=\"width=device-width, "
|
||||
"initial-scale=1.0\"><title>Internal Server Error at " +
|
||||
HttpUtils::HtmlEncode(this->originalPath) +
|
||||
"</title><meta name=\"color-scheme\" content=\"dark "
|
||||
"light\"></head><body><h1>Internal Server Error at " +
|
||||
HttpUtils::HtmlEncode(this->originalPath) +
|
||||
"</h1><p>what(): " + HttpUtils::HtmlEncode(ex.what()) +
|
||||
"</p></body></html>");
|
||||
} else {
|
||||
this->WithStatusCode(StatusCode::InternalServerError)
|
||||
.SendErrorPage(true);
|
||||
this->WithMimeType("text/html")
|
||||
.WithStatusCode(StatusCode::InternalServerError)
|
||||
.SendText(
|
||||
"<!DOCTYPE html><html lang=\"en\"><head><meta "
|
||||
"charset=\"UTF-8\"> <meta name=\"viewport\" "
|
||||
"content=\"width=device-width, "
|
||||
"initial-scale=1.0\"><title>Internal Server Error at " +
|
||||
HttpUtils::HtmlEncode(this->originalPath) +
|
||||
"</title><meta name=\"color-scheme\" content=\"dark "
|
||||
"light\"></head><body><h1>Internal Server Error at " +
|
||||
HttpUtils::HtmlEncode(this->originalPath) +
|
||||
"</h1><p>what(): " + HttpUtils::HtmlEncode(ex.what()) +
|
||||
"</p></body></html>");
|
||||
} else {
|
||||
this->WithStatusCode(StatusCode::InternalServerError)
|
||||
.SendErrorPage(true);
|
||||
}
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -985,6 +1039,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 +1056,7 @@ ServerContext &ServerContext::WriteHeaders() {
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
void HttpServer::Process(std::shared_ptr<Stream> strm,
|
||||
std::shared_ptr<IHttpServer> server, std::string ip,
|
||||
uint16_t port, uint16_t serverPort, bool encrypted,
|
||||
@@ -1074,10 +1132,40 @@ void HttpServer::Process(std::shared_ptr<Stream> 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(
|
||||
"<!DOCTYPE html>"
|
||||
"<html>"
|
||||
"<head><meta name=\"color-scheme\" "
|
||||
"content=\"dark light\"><title>400 Bad "
|
||||
"Request</title></head>"
|
||||
"<body>"
|
||||
"<h1>400 Bad Request</h1><p>" +
|
||||
HttpUtils::HtmlEncode(
|
||||
"There are multiple instances of the header \"" + item +
|
||||
"\".") +
|
||||
"</p>"
|
||||
"</body>"
|
||||
"</html>"
|
||||
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(ctx.method == "GET" || ctx.method == "HEAD") &&
|
||||
ctx.requestHeaders.TryGetFirst("Content-Type", type) &&
|
||||
type == "application/x-www-form-urlencoded" &&
|
||||
ctx.requestHeaders.TryGetFirstInt("Content-Length", length)) {
|
||||
|
||||
if (length <= 0) {
|
||||
ctx.statusCode = StatusCode::BadRequest;
|
||||
ctx.SendErrorPage(true);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t len = (size_t)length;
|
||||
std::vector<uint8_t> buffer(len);
|
||||
len = bStrm->ReadBlock(buffer.data(), len);
|
||||
|
||||
+60
-18
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+445
-224
@@ -21,14 +21,19 @@
|
||||
|
||||
#include "TessesFramework/Http/HttpUtils.hpp"
|
||||
#include "TessesFramework/Filesystem/VFS.hpp"
|
||||
#include "TessesFramework/Serialization/BitConverter.hpp"
|
||||
#include "TessesFramework/Text/StringConverter.hpp"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
using VFSPath = Tesses::Framework::Filesystem::VFSPath;
|
||||
namespace Tesses::Framework::Http {
|
||||
|
||||
bool Uri::Relative(std::string url, Uri &uri) {
|
||||
auto index = url.find_first_of("//");
|
||||
bool Uri::Relative(std::string_view url, Uri &uri) {
|
||||
auto path = this->path;
|
||||
if (path.empty())
|
||||
path = "/";
|
||||
auto index = url.find("//");
|
||||
if (index != std::string::npos) {
|
||||
if (Uri::TryParse(url, uri)) {
|
||||
if (index == 0)
|
||||
@@ -63,7 +68,10 @@ bool Uri::Relative(std::string url, Uri &uri) {
|
||||
|
||||
auto fourthPart = HttpUtils::SplitString(thirdPart[1], "?", 2);
|
||||
|
||||
VFSPath p = VFSPath(this->path, fourthPart[0]);
|
||||
VFSPath p =
|
||||
VFSPath(path, (path.back() == '/' || fourthPart[0].empty())
|
||||
? fourthPart[0]
|
||||
: "../" + fourthPart[0]);
|
||||
uri.path =
|
||||
p.CollapseRelativeParents().ToString(); // this should be safe
|
||||
if (fourthPart.size() == 2) {
|
||||
@@ -98,21 +106,33 @@ uint16_t Uri::GetPort() {
|
||||
return 69;
|
||||
return 0;
|
||||
}
|
||||
bool Uri::TryParse(std::string url, Uri &uri) {
|
||||
bool Uri::TryParse(std::string_view url, Uri &uri) {
|
||||
uri.scheme = "";
|
||||
uri.port = 0;
|
||||
auto firstPart = HttpUtils::SplitString(url, "//", 2);
|
||||
if (firstPart.size() == 2)
|
||||
|
||||
uri.scheme = firstPart[0];
|
||||
else if (firstPart.empty())
|
||||
else
|
||||
return false;
|
||||
|
||||
auto secondPart = HttpUtils::SplitString(
|
||||
firstPart.size() == 2 ? firstPart[1] : firstPart[0], "/", 2);
|
||||
auto secondPart = HttpUtils::SplitString(firstPart[1], "/", 2);
|
||||
|
||||
if (secondPart.size() == 1) {
|
||||
uri.path = "/";
|
||||
auto hp = secondPart[0];
|
||||
auto fragPart = HttpUtils::SplitString(hp, "#", 2);
|
||||
if (fragPart.size() == 2) {
|
||||
uri.hash = fragPart[1];
|
||||
hp = fragPart[0];
|
||||
}
|
||||
auto queryPart = HttpUtils::SplitString(hp, "?", 2);
|
||||
if (queryPart.size() == 2) {
|
||||
HttpUtils::QueryParamsDecode(uri.query, queryPart[1]);
|
||||
hp = queryPart[0];
|
||||
}
|
||||
secondPart[0] = hp;
|
||||
|
||||
} else if (secondPart.size() == 2) {
|
||||
auto thirdPart = HttpUtils::SplitString(secondPart[1], "#", 2);
|
||||
if (thirdPart.empty())
|
||||
@@ -135,14 +155,25 @@ bool Uri::TryParse(std::string url, Uri &uri) {
|
||||
if (hostPortPart.empty())
|
||||
return false;
|
||||
if (hostPortPart.size() == 2) {
|
||||
uri.port = (uint16_t)std::stoul(hostPortPart[1]);
|
||||
uint64_t portNum;
|
||||
if (!Serialization::BitConverter::TryParseUnsigned(hostPortPart[1],
|
||||
portNum))
|
||||
return false;
|
||||
if (portNum > 65535)
|
||||
return false;
|
||||
uri.port = static_cast<uint16_t>(portNum);
|
||||
}
|
||||
uri.host = hostPortPart[0];
|
||||
|
||||
return true;
|
||||
}
|
||||
Uri::Uri() : query(true) {}
|
||||
std::string Uri::GetPathAndQuery() { return this->path + this->GetQuery(); }
|
||||
std::string Uri::GetPathAndQuery() {
|
||||
if (this->path.empty() || this->path.front() != '/')
|
||||
return '/' + this->path + this->GetQuery();
|
||||
|
||||
return this->path + this->GetQuery();
|
||||
}
|
||||
std::string Uri::GetQuery() {
|
||||
if (this->query.kvp.empty())
|
||||
return "";
|
||||
@@ -162,17 +193,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 +215,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 +254,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<std::string, std::string> 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 +380,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 +390,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<std::string> ss = SplitString(item, "=", 2);
|
||||
if (ss.size() >= 1) {
|
||||
@@ -338,7 +403,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<std::string> ents) {
|
||||
std::string str = {};
|
||||
bool first = true;
|
||||
@@ -368,41 +433,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<char>(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<uint8_t>(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,142 +477,102 @@ 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<char>(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<uint8_t>(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<std::string> HttpUtils::SplitString(std::string text,
|
||||
std::string delimiter,
|
||||
std::size_t maxCnt) {
|
||||
std::vector<std::string> strs;
|
||||
void HttpUtils::SplitString(std::vector<std::string> &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<std::string> HttpUtils::SplitString(std::string_view text,
|
||||
std::string_view delimiter,
|
||||
std::size_t maxCnt) {
|
||||
std::vector<std::string> 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 = "";
|
||||
|
||||
auto flush = [&]() -> void {
|
||||
if (!builder.empty()) {
|
||||
// Collect trailing punctuation
|
||||
std::string trailing;
|
||||
static const char *punct = ".,;:!?'\"";
|
||||
while (!builder.empty() && std::strchr(punct, builder.back())) {
|
||||
trailing.insert(trailing.begin(), builder.back());
|
||||
builder.pop_back();
|
||||
}
|
||||
if (builder.find("http://") == 0 || builder.find("https://") == 0 ||
|
||||
builder.find("ftp://") == 0 || builder.find("ftps://") == 0 ||
|
||||
builder.find("magnet:") == 0 || builder.find("btmh:") == 0) {
|
||||
builder.find("magnet:") == 0) {
|
||||
newText += "<a href=\"" + HttpUtils::HtmlEncode(builder) +
|
||||
"\">" + HttpUtils::HtmlEncode(builder) + "</a>";
|
||||
} else if (builder.find("mailto:") == 0) {
|
||||
@@ -559,6 +586,7 @@ std::string HttpUtils::HtmlP(std::string text) {
|
||||
} else {
|
||||
newText += HttpUtils::HtmlEncode(builder);
|
||||
}
|
||||
newText += HttpUtils::HtmlEncode(trailing);
|
||||
builder = "";
|
||||
}
|
||||
};
|
||||
@@ -575,7 +603,7 @@ std::string HttpUtils::HtmlP(std::string text) {
|
||||
break;
|
||||
case '\t':
|
||||
flush();
|
||||
newText += "&tab;";
|
||||
newText += " ";
|
||||
break;
|
||||
case '\r':
|
||||
flush();
|
||||
@@ -589,7 +617,107 @@ std::string HttpUtils::HtmlP(std::string text) {
|
||||
|
||||
return newText;
|
||||
}
|
||||
std::string HttpUtils::HtmlEncode(std::string html) {
|
||||
static std::unordered_map<std::string, std::string> htmlencodings = {
|
||||
{""", "\""},
|
||||
{"'", "'"},
|
||||
{"&", "&"},
|
||||
{"<", "<"},
|
||||
{">", ">"},
|
||||
{" ", "\xC2\xA0"},
|
||||
{"©", "\xC2\xA9"},
|
||||
{"®", "\xC2\xAE"},
|
||||
{"™", "\xE2\x84\xA2"},
|
||||
{"—", "\xE2\x80\x94"},
|
||||
{"–", "\xE2\x80\x93"},
|
||||
{"…", "\xE2\x80\xA6"},
|
||||
{"’", "\xE2\x80\x99"},
|
||||
{"‘", "\xE2\x80\x98"},
|
||||
{"”", "\xE2\x80\x9D"},
|
||||
{"“", "\xE2\x80\x9C"},
|
||||
{"€", "\xE2\x82\xAC"},
|
||||
{"£", "\xC2\xA3"},
|
||||
{"¥", "\xC2\xA5"},
|
||||
{"°", "\xC2\xB0"},
|
||||
{"±", "\xC2\xB1"},
|
||||
{"×", "\xC3\x97"},
|
||||
{"÷", "\xC3\xB7"},
|
||||
{"«", "\xC2\xAB"},
|
||||
{"»", "\xC2\xBB"},
|
||||
{"•", "\xE2\x80\xA2"},
|
||||
{"·", "\xC2\xB7"},
|
||||
{"½", "\xC2\xBD"},
|
||||
{"¼", "\xC2\xBC"},
|
||||
{"¾", "\xC2\xBE"},
|
||||
};
|
||||
std::string HttpUtils::HtmlDecode(std::string_view view) {
|
||||
std::string myHtml = {};
|
||||
std::string tmp = {};
|
||||
bool inEscaped = false;
|
||||
for (size_t i = 0; i < view.size(); i++) {
|
||||
if (inEscaped) {
|
||||
if (view[i] == ';') {
|
||||
inEscaped = false;
|
||||
|
||||
if (tmp.size() > 2 && tmp[1] == '#') //&#N
|
||||
{
|
||||
if (tmp[2] == 'x' || tmp[2] == 'X') {
|
||||
auto res = tmp.substr(3);
|
||||
uint64_t code;
|
||||
if (Serialization::BitConverter::TryParseUnsigned(
|
||||
res, code, 16)) {
|
||||
if (code <= 0x10FFFF &&
|
||||
(code < 0xD800 || code > 0xDFFF)) {
|
||||
Text::StringConverter::UTF8::FromUTF32(
|
||||
myHtml, {static_cast<char32_t>(code)});
|
||||
} else {
|
||||
myHtml += "\xEF\xBF\xBD";
|
||||
}
|
||||
} else {
|
||||
myHtml += "\xEF\xBF\xBD";
|
||||
}
|
||||
} else {
|
||||
auto res = tmp.substr(2);
|
||||
uint64_t code;
|
||||
if (Serialization::BitConverter::TryParseUnsigned(
|
||||
res, code, 10)) {
|
||||
if (code <= 0x10FFFF &&
|
||||
(code < 0xD800 || code > 0xDFFF)) {
|
||||
Text::StringConverter::UTF8::FromUTF32(
|
||||
myHtml, {static_cast<char32_t>(code)});
|
||||
} else {
|
||||
myHtml += "\xEF\xBF\xBD";
|
||||
}
|
||||
} else {
|
||||
myHtml += "\xEF\xBF\xBD";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tmp += ';';
|
||||
auto result = htmlencodings.find(tmp);
|
||||
if (result != htmlencodings.end()) {
|
||||
myHtml += result->second;
|
||||
} else {
|
||||
myHtml += "\xEF\xBF\xBD";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tmp += view[i];
|
||||
}
|
||||
} else {
|
||||
if (view[i] == '&') {
|
||||
tmp = "&";
|
||||
inEscaped = true;
|
||||
} else {
|
||||
myHtml += view[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inEscaped)
|
||||
myHtml += tmp;
|
||||
return myHtml;
|
||||
}
|
||||
|
||||
std::string HttpUtils::HtmlEncode(std::string_view html) {
|
||||
std::string myHtml = {};
|
||||
for (auto item : html) {
|
||||
if (item == '\"') {
|
||||
@@ -736,18 +864,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 +878,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<unsigned char>(left[i]);
|
||||
unsigned char senright = static_cast<unsigned char>(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) {
|
||||
@@ -816,16 +977,8 @@ bool HttpDictionary::TryGetFirstInt(std::string key, int64_t &value) {
|
||||
std::string val;
|
||||
if (!TryGetFirst(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;
|
||||
|
||||
return Serialization::BitConverter::TryParseSigned(val, value);
|
||||
}
|
||||
bool HttpDictionary::TryGetFirstDate(std::string key, Date::DateTime &dt) {
|
||||
std::string val;
|
||||
@@ -848,20 +1001,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<unsigned char>(a)) <
|
||||
std::tolower(static_cast<unsigned char>(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 +1131,12 @@ void HttpUtils::BytesToHex(std::string &text, const std::vector<uint8_t> &data,
|
||||
text[i * 2 + 1] += NibbleToHex(data[i], isUpper);
|
||||
}
|
||||
}
|
||||
std::vector<uint8_t> HttpUtils::HexToBytes(const std::string &text) {
|
||||
std::vector<uint8_t> HttpUtils::HexToBytes(std::string_view text) {
|
||||
std::vector<uint8_t> data;
|
||||
HexToBytes(data, text);
|
||||
return data;
|
||||
}
|
||||
void HttpUtils::HexToBytes(std::vector<uint8_t> &data,
|
||||
const std::string &text) {
|
||||
void HttpUtils::HexToBytes(std::vector<uint8_t> &data, std::string_view text) {
|
||||
if (text.empty()) {
|
||||
data.clear();
|
||||
return;
|
||||
|
||||
@@ -51,36 +51,46 @@ MountableServer::MountableServer(std::shared_ptr<IHttpServer> root) {
|
||||
|
||||
void MountableServer::Mount(std::string path,
|
||||
std::shared_ptr<IHttpServer> server) {
|
||||
mtx.Lock();
|
||||
this->servers.insert(
|
||||
this->servers.begin(),
|
||||
std::pair<std::string, std::shared_ptr<IHttpServer>>(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
|
||||
|
||||
@@ -58,22 +58,36 @@ RouteServer::RouteServer(std::shared_ptr<IHttpServer> 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
|
||||
@@ -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_PTR)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((intptr_t)result));
|
||||
}
|
||||
}
|
||||
|
||||
#elif defined(__APPLE__)
|
||||
auto exec = Tesses::Framework::Platform::Environment::GetRealExecutablePath(
|
||||
(std::string) "open");
|
||||
|
||||
@@ -284,4 +284,61 @@ void BitConverter::FromSint16LE(uint8_t &b, int16_t v) {
|
||||
memcpy(&dest, &v, sizeof(uint16_t));
|
||||
FromUint16LE(b, dest);
|
||||
}
|
||||
|
||||
bool BitConverter::TryParseSigned(const std::string &str, int64_t &val,
|
||||
int base) {
|
||||
try {
|
||||
if (str.find_first_not_of(" \t") != 0)
|
||||
return false;
|
||||
size_t pos = 0;
|
||||
int64_t v = std::stoll(str, &pos, base);
|
||||
if (pos < str.size())
|
||||
return false;
|
||||
val = v;
|
||||
return true;
|
||||
} catch (std::out_of_range &arg) {
|
||||
return false;
|
||||
} catch (std::invalid_argument &arg) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool BitConverter::TryParseUnsigned(const std::string &str, uint64_t &val,
|
||||
int base) {
|
||||
try {
|
||||
|
||||
if (str.find_first_not_of(" \t") != 0)
|
||||
return false;
|
||||
if (str[0] == '-' || str[0] == '+')
|
||||
return false;
|
||||
size_t pos = 0;
|
||||
uint64_t v = std::stoull(str, &pos, base);
|
||||
if (pos < str.size())
|
||||
return false;
|
||||
val = v;
|
||||
return true;
|
||||
} catch (std::out_of_range &arg) {
|
||||
return false;
|
||||
} catch (std::invalid_argument &arg) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool BitConverter::TryParseDouble(const std::string &str, double &val) {
|
||||
try {
|
||||
|
||||
if (str.find_first_not_of(" \t") != 0)
|
||||
return false;
|
||||
|
||||
size_t pos = 0;
|
||||
double v = std::stod(str, &pos);
|
||||
if (pos < str.size())
|
||||
return false;
|
||||
val = v;
|
||||
return true;
|
||||
} catch (std::out_of_range &arg) {
|
||||
return false;
|
||||
} catch (std::invalid_argument &arg) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Tesses::Framework::Serialization
|
||||
@@ -0,0 +1,429 @@
|
||||
#include "TessesFramework/Serialization/Html.hpp"
|
||||
#include "TessesFramework/Console.hpp"
|
||||
#include "TessesFramework/Http/HttpUtils.hpp"
|
||||
#include <unordered_set>
|
||||
namespace Tesses::Framework::Serialization::Html {
|
||||
|
||||
static std::unordered_set<std::string> html_nodes_with_no_body = {
|
||||
"area", "base", "br", "col", "embed", "hr", "img",
|
||||
"input", "link", "meta", "source", "track", "wbr",
|
||||
};
|
||||
std::string HtmlNode::ToString() const {
|
||||
std::string text;
|
||||
HtmlNode::ToString(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
void HtmlNode::InnerText(std::string &str) const {
|
||||
if (this->isText) {
|
||||
str += this->text_or_tag;
|
||||
} else {
|
||||
for (const auto &item : this->children)
|
||||
item->InnerText(str);
|
||||
}
|
||||
}
|
||||
|
||||
std::string HtmlNode::InnerText() const {
|
||||
std::string innerText;
|
||||
InnerText(innerText);
|
||||
return innerText;
|
||||
}
|
||||
|
||||
void HtmlNode::ToString(std::string &str) const {
|
||||
if (this->isText) {
|
||||
str += Http::HttpUtils::HtmlEncode(this->text_or_tag);
|
||||
} else {
|
||||
str += "<";
|
||||
str += this->text_or_tag;
|
||||
for (auto &attr : this->attributes) {
|
||||
str += ' ';
|
||||
str += attr.first;
|
||||
if (attr.second) {
|
||||
str += "=\"";
|
||||
str += Http::HttpUtils::HtmlEncode(attr.second.value());
|
||||
str += "\"";
|
||||
}
|
||||
}
|
||||
str += ">";
|
||||
|
||||
if (html_nodes_with_no_body.count(this->text_or_tag) == 0) {
|
||||
for (auto &child : this->children)
|
||||
child->ToString(str);
|
||||
|
||||
str += "</";
|
||||
str += this->text_or_tag;
|
||||
str += ">";
|
||||
}
|
||||
}
|
||||
}
|
||||
std::shared_ptr<HtmlNode> HtmlNode::FromText(std::string_view text) {
|
||||
auto node = std::make_shared<HtmlNode>();
|
||||
node->isText = true;
|
||||
node->text_or_tag = std::string{text};
|
||||
return node;
|
||||
}
|
||||
std::shared_ptr<HtmlNode> HtmlNode::FromNode(
|
||||
std::string_view name,
|
||||
std::initializer_list<std::pair<std::string, std::optional<std::string>>>
|
||||
attrs,
|
||||
std::initializer_list<std::shared_ptr<HtmlNode>> nodes) {
|
||||
return HtmlNode::FromNode(name, attrs.begin(), attrs.end(), nodes.begin(),
|
||||
nodes.end());
|
||||
}
|
||||
|
||||
std::shared_ptr<HtmlNode> HtmlNode::FromNode(
|
||||
std::string_view name,
|
||||
std::vector<std::pair<std::string, std::optional<std::string>>> attrs,
|
||||
std::vector<std::shared_ptr<HtmlNode>> nodes) {
|
||||
return HtmlNode::FromNode(name, attrs.begin(), attrs.end(), nodes.begin(),
|
||||
nodes.end());
|
||||
}
|
||||
|
||||
void HtmlNode::FindNodes(
|
||||
std::string_view tag,
|
||||
std::vector<std::shared_ptr<const HtmlNode>> &nodes) const {
|
||||
if (!this->isText) {
|
||||
if (this->text_or_tag == tag)
|
||||
nodes.push_back(this->shared_from_this());
|
||||
for (const auto &node : this->children)
|
||||
node->FindNodes(tag, nodes);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<const HtmlNode>>
|
||||
HtmlNode::FindNodes(std::string_view tag) const {
|
||||
std::vector<std::shared_ptr<const HtmlNode>> nodes;
|
||||
FindNodes(tag, nodes);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
enum class HtmlLexTokenType { SYMBOL, TEXT, STRING };
|
||||
struct HtmlLexToken {
|
||||
std::string text;
|
||||
HtmlLexTokenType type;
|
||||
HtmlLexToken(HtmlLexTokenType type, std::string_view text)
|
||||
: text(text), type(type) {}
|
||||
};
|
||||
|
||||
struct HtmlLexer {
|
||||
std::shared_ptr<TextStreams::TextReader> reader;
|
||||
std::vector<HtmlLexToken> &tokens;
|
||||
int32_t _peeked = -1;
|
||||
std::string builder;
|
||||
bool ignoreSpaceMode = false;
|
||||
std::string nodeName = {};
|
||||
|
||||
HtmlLexer(std::shared_ptr<TextStreams::TextReader> reader,
|
||||
std::vector<HtmlLexToken> &tokens)
|
||||
: reader(std::move(reader)), tokens(tokens) {}
|
||||
|
||||
int32_t Peek() {
|
||||
if (_peeked != -1)
|
||||
return _peeked;
|
||||
_peeked = Read();
|
||||
return _peeked;
|
||||
}
|
||||
|
||||
int64_t Read() {
|
||||
if (_peeked != -1) {
|
||||
auto tmp = _peeked;
|
||||
_peeked = -1;
|
||||
return tmp;
|
||||
}
|
||||
|
||||
return reader->ReadChar();
|
||||
}
|
||||
|
||||
void Flush() {
|
||||
if (!builder.empty()) {
|
||||
this->tokens.emplace_back(HtmlLexTokenType::TEXT, builder);
|
||||
|
||||
if (ignoreSpaceMode && nodeName.empty())
|
||||
nodeName = builder;
|
||||
|
||||
builder = "";
|
||||
}
|
||||
}
|
||||
|
||||
void SkipTo(std::string_view fmt) {
|
||||
if (fmt.empty())
|
||||
return;
|
||||
std::string window;
|
||||
window.resize(fmt.size());
|
||||
|
||||
while (fmt != window) {
|
||||
int32_t read = Read();
|
||||
if (read == -1)
|
||||
break;
|
||||
for (size_t i = 0; i < window.size() - 1; i++) {
|
||||
window[i] = window[i + 1];
|
||||
}
|
||||
|
||||
window.back() = static_cast<char>(read);
|
||||
}
|
||||
}
|
||||
|
||||
void Lex() {
|
||||
int read;
|
||||
while ((read = Read()) != -1) {
|
||||
int peek = Peek();
|
||||
switch (read) {
|
||||
case '"':
|
||||
case '\'': {
|
||||
if (ignoreSpaceMode) {
|
||||
Flush();
|
||||
char quote = static_cast<char>(read);
|
||||
std::string val;
|
||||
int c;
|
||||
while ((c = Read()) != -1 && c != quote)
|
||||
val += static_cast<char>(c);
|
||||
tokens.emplace_back(HtmlLexTokenType::STRING, val);
|
||||
} else {
|
||||
builder += static_cast<char>(read);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '/': {
|
||||
if (ignoreSpaceMode && peek == '>') {
|
||||
Flush();
|
||||
Read();
|
||||
ignoreSpaceMode = false;
|
||||
tokens.emplace_back(HtmlLexTokenType::SYMBOL, "/>");
|
||||
} else if (ignoreSpaceMode) {
|
||||
builder += '/';
|
||||
} else {
|
||||
builder += '/';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '>': {
|
||||
if (ignoreSpaceMode) {
|
||||
Flush();
|
||||
ignoreSpaceMode = false;
|
||||
tokens.emplace_back(HtmlLexTokenType::SYMBOL, ">");
|
||||
|
||||
if (nodeName == "script" || nodeName == "style") {
|
||||
nodeName = "_";
|
||||
std::string builder2 = "";
|
||||
bool eof = false;
|
||||
while ((read = Read()) != -1) {
|
||||
if (read == '<') {
|
||||
peek = Peek();
|
||||
if (peek == '/') {
|
||||
Read();
|
||||
break;
|
||||
} else if (peek == -1) {
|
||||
eof = true;
|
||||
break;
|
||||
} else {
|
||||
builder2 += '<';
|
||||
}
|
||||
} else {
|
||||
builder2 += static_cast<char>(read);
|
||||
}
|
||||
}
|
||||
|
||||
if (read != -1 && !eof) {
|
||||
tokens.emplace_back(HtmlLexTokenType::TEXT,
|
||||
builder2);
|
||||
|
||||
ignoreSpaceMode = true;
|
||||
tokens.emplace_back(HtmlLexTokenType::SYMBOL, "</");
|
||||
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
builder += '>';
|
||||
}
|
||||
} break;
|
||||
case '<': {
|
||||
Flush();
|
||||
if (peek == '!') {
|
||||
//<!-- -->
|
||||
//<!DOCTYPE html>
|
||||
Read();
|
||||
peek = Peek();
|
||||
if (peek == '-') {
|
||||
Read();
|
||||
peek = Peek();
|
||||
if (peek == '-') {
|
||||
Read();
|
||||
SkipTo("-->");
|
||||
|
||||
} else {
|
||||
SkipTo(">");
|
||||
}
|
||||
} else {
|
||||
SkipTo(">");
|
||||
}
|
||||
|
||||
} else if (peek == '/') {
|
||||
Read();
|
||||
|
||||
ignoreSpaceMode = true;
|
||||
tokens.emplace_back(HtmlLexTokenType::SYMBOL, "</");
|
||||
} else {
|
||||
|
||||
ignoreSpaceMode = true;
|
||||
tokens.emplace_back(HtmlLexTokenType::SYMBOL, "<");
|
||||
nodeName = {};
|
||||
}
|
||||
} break;
|
||||
case '=': {
|
||||
if (!ignoreSpaceMode) {
|
||||
builder += "=";
|
||||
} else {
|
||||
Flush();
|
||||
tokens.emplace_back(HtmlLexTokenType::SYMBOL, "=");
|
||||
}
|
||||
} break;
|
||||
case ' ':
|
||||
case '\n':
|
||||
case '\t':
|
||||
case '\r':
|
||||
if (!ignoreSpaceMode) {
|
||||
builder += static_cast<char>(read);
|
||||
} else {
|
||||
Flush();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
builder += static_cast<char>(read);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Flush();
|
||||
}
|
||||
};
|
||||
|
||||
struct HtmlParser {
|
||||
|
||||
std::vector<HtmlLexToken> &tokens;
|
||||
size_t index = 0;
|
||||
HtmlParser(std::vector<HtmlLexToken> &tokens) : tokens(tokens) {}
|
||||
|
||||
bool IsSymbol(std::string_view type) {
|
||||
if (Done())
|
||||
return false;
|
||||
auto &obj = Peak();
|
||||
if (obj.type != HtmlLexTokenType::SYMBOL)
|
||||
return false;
|
||||
|
||||
if (obj.text != type)
|
||||
return false;
|
||||
index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Done() { return index >= tokens.size(); }
|
||||
|
||||
HtmlLexToken &Read() { return tokens[index++]; }
|
||||
HtmlLexToken &Peak() { return tokens[index]; }
|
||||
|
||||
void Parse(std::string_view name, std::shared_ptr<HtmlNode> &node,
|
||||
bool root = false) {
|
||||
while (!Done()) {
|
||||
auto &curToken = Peak();
|
||||
switch (curToken.type) {
|
||||
case HtmlLexTokenType::TEXT: {
|
||||
Read();
|
||||
node->children.push_back(HtmlNode::FromText(
|
||||
Http::HttpUtils::HtmlDecode(curToken.text)));
|
||||
} break;
|
||||
case HtmlLexTokenType::SYMBOL: {
|
||||
if (curToken.text == "<") {
|
||||
Read();
|
||||
if (Done()) {
|
||||
return;
|
||||
}
|
||||
auto &val = Peak();
|
||||
if (val.type == HtmlLexTokenType::TEXT) {
|
||||
Read();
|
||||
|
||||
std::shared_ptr<HtmlNode> newNode =
|
||||
std::make_shared<HtmlNode>();
|
||||
newNode->isText = false;
|
||||
newNode->text_or_tag = val.text;
|
||||
|
||||
while (!Done()) {
|
||||
auto &k = Peak();
|
||||
|
||||
if (k.type == HtmlLexTokenType::TEXT) {
|
||||
Read();
|
||||
if (IsSymbol("=")) {
|
||||
if (!Done()) {
|
||||
auto &v = Peak();
|
||||
|
||||
if (v.type ==
|
||||
HtmlLexTokenType::STRING) {
|
||||
Read();
|
||||
newNode->attributes.emplace_back(
|
||||
k.text,
|
||||
Http::HttpUtils::HtmlDecode(
|
||||
v.text));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
newNode->attributes.emplace_back(k.text,
|
||||
std::nullopt);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (k.type == HtmlLexTokenType::SYMBOL &&
|
||||
(k.text == ">" || k.text == "/>"))
|
||||
break;
|
||||
}
|
||||
if (!IsSymbol("/>")) {
|
||||
IsSymbol(">");
|
||||
if (html_nodes_with_no_body.count(val.text) == 0)
|
||||
Parse(val.text, newNode);
|
||||
}
|
||||
|
||||
node->children.push_back(newNode);
|
||||
}
|
||||
} else if (curToken.text == "</") {
|
||||
Read();
|
||||
if (Done()) {
|
||||
node->children.push_back(HtmlNode::FromText("</"));
|
||||
return;
|
||||
}
|
||||
auto &val = Peak();
|
||||
Read();
|
||||
IsSymbol(">");
|
||||
|
||||
if (val.text == name && !root)
|
||||
return;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<HtmlNode> Parse() {
|
||||
std::shared_ptr<HtmlNode> node = std::make_shared<HtmlNode>();
|
||||
node->isText = false;
|
||||
node->text_or_tag = ":root";
|
||||
Parse(":root", node, true);
|
||||
return node;
|
||||
}
|
||||
};
|
||||
|
||||
std::shared_ptr<HtmlNode>
|
||||
Parse(std::shared_ptr<TextStreams::TextReader> reader) {
|
||||
std::vector<HtmlLexToken> tokens;
|
||||
|
||||
HtmlLexer lexer(reader, tokens);
|
||||
|
||||
lexer.Lex();
|
||||
|
||||
HtmlParser parser(tokens);
|
||||
|
||||
return parser.Parse();
|
||||
}
|
||||
|
||||
} // namespace Tesses::Framework::Serialization::Html
|
||||
@@ -43,12 +43,15 @@ using HttpUtils = Tesses::Framework::Http::HttpUtils;
|
||||
#else
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <iphlpapi.h>
|
||||
// clang-format off
|
||||
|
||||
#include <winsock2.h>
|
||||
#include <iphlpapi.h>
|
||||
|
||||
#include <ws2tcpip.h>
|
||||
|
||||
#include <windows.h>
|
||||
// clang-format on
|
||||
#if defined __has_include
|
||||
#if __has_include(<afunix.h>)
|
||||
#include <afunix.h>
|
||||
@@ -72,8 +75,8 @@ extern "C" {
|
||||
!defined(__PS2__)
|
||||
#include <sys/un.h>
|
||||
#endif
|
||||
|
||||
#include <poll.h>
|
||||
#include <sys/time.h>
|
||||
}
|
||||
#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; }
|
||||
|
||||
@@ -117,4 +117,22 @@ void Stream::CopyTo(std::shared_ptr<Stream> 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
|
||||
|
||||
+6
-10
@@ -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 "";
|
||||
|
||||
+102
-7
@@ -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<MutexHiddenFieldData>();
|
||||
#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<MutexHiddenFieldData *>();
|
||||
#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<MutexHiddenFieldData *>();
|
||||
#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<MutexHiddenFieldData *>();
|
||||
#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<MutexCondHiddenFieldData>();
|
||||
#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<MutexCondHiddenFieldData *>();
|
||||
auto md = mtx->data.GetField<MutexHiddenFieldData *>();
|
||||
#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<MutexCondHiddenFieldData *>();
|
||||
auto md = mtx->data.GetField<MutexHiddenFieldData *>();
|
||||
#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<MutexCondHiddenFieldData *>();
|
||||
|
||||
#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<MutexCondHiddenFieldData *>();
|
||||
|
||||
#if defined(_WIN32)
|
||||
WakeConditionVariable(&mcd->cond);
|
||||
#else
|
||||
pthread_cond_broadcast(&mcd->cond);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
Cond::~Cond() {}
|
||||
|
||||
}; // namespace Tesses::Framework::Threading
|
||||
|
||||
Reference in New Issue
Block a user