Files
tessesframework/src/Http/HttpServer.cpp
Mike Nolan 4aef8c0c1a
Some checks failed
Build and Deploy on Tag / 🔨 Build for Arch Linux/Static Builds (push) Successful in 1m11s
Build and Deploy on Tag / 🔨 Build win32 and update the tap 🍺 (push) Failing after 1m6s
Overhaul cmake configuration, add console api, fix http code that caused issues with cgi-bin
2026-05-27 04:04:39 -05:00

1187 lines
38 KiB
C++

/*
TessesFramework a library to make C++ easier for me, used in CrossLang:
https://git.tesses.org/tesses50/crosslang Copyright (C) 2026 Mike Nolan
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/HttpServer.hpp"
#include "TessesFramework/Common.hpp"
#include "TessesFramework/Crypto/Crypto.hpp"
#include "TessesFramework/Http/ContentDisposition.hpp"
#include "TessesFramework/Http/HttpStream.hpp"
#include "TessesFramework/Streams/BufferedStream.hpp"
#include "TessesFramework/Streams/FileStream.hpp"
#include "TessesFramework/Streams/MemoryStream.hpp"
#include "TessesFramework/TextStreams/StdIOWriter.hpp"
#include "TessesFramework/TextStreams/StreamReader.hpp"
#include "TessesFramework/TextStreams/StreamWriter.hpp"
#include "TessesFramework/Threading/Mutex.hpp"
#include <TessesFramework/Http/HttpUtils.hpp>
#include <iostream>
// clang-format off
#include "TessesFramework/Filesystem/VFS.hpp"
#include "TessesFramework/Filesystem/VFSFix.hpp"
// clang-format on
using FileStream = Tesses::Framework::Streams::FileStream;
using Stream = Tesses::Framework::Streams::Stream;
using SeekOrigin = Tesses::Framework::Streams::SeekOrigin;
using MemoryStream = Tesses::Framework::Streams::MemoryStream;
using TcpServer = Tesses::Framework::Streams::TcpServer;
using NetworkStream = Tesses::Framework::Streams::NetworkStream;
using BufferedStream = Tesses::Framework::Streams::BufferedStream;
using namespace Tesses::Framework::TextStreams;
namespace Tesses::Framework::Http {
void ServerSentEvents::SendEventRaw(const std::string &evt) {
this->mtx.Lock();
for (auto &item : this->strms) {
try {
StreamWriter writer(item);
writer.newline = "\r\n";
writer.WriteLine(evt);
} catch (...) {
}
}
this->mtx.Unlock();
}
void ServerSentEvents::SendId(const std::string &id) {
SendCustomEvent("id", id);
}
void ServerSentEvents::SendData(const std::string &message) {
SendCustomEvent("data", message);
}
void ServerSentEvents::SendComment(const std::string &comment) {
SendCustomEvent("", comment);
}
void ServerSentEvents::SendCustomEvent(const std::string &etype,
const std::string &message) {
std::string text = etype + ": ";
for (auto item : message) {
if (item == '\r')
continue;
if (item == '\n') {
text += "\r\n" + etype + ": ";
} else
text += item;
}
SendEventRaw(text);
}
void ServerSentEvents::SendData(const std::string &message,
const std::string &mtype) {
std::string text = "event: " + mtype + "\r\ndata: ";
for (auto item : message) {
if (item == '\r')
continue;
if (item == '\n') {
text += "\r\ndata: ";
} else
text += item;
}
SendEventRaw(text);
}
void ServerSentEvents::SendRetry(uint32_t ms) {
SendCustomEvent("retry", std::to_string(ms));
}
void ServerSentEvents::SendRetry(std::chrono::milliseconds ms) {
SendCustomEvent("retry", std::to_string(ms.count()));
}
void ServerSentEvents::SendRetry(Tesses::Framework::Date::TimeSpan ts) {
SendRetry((uint32_t)ts.TotalSeconds() * 1000);
}
class WSServer {
public:
Threading::Mutex mtx;
ServerContext *ctx;
std::shared_ptr<WebSocketConnection> conn;
std::shared_ptr<Stream> strm;
std::atomic_bool hasInit;
std::atomic<bool> closed;
void close() {
mtx.Lock();
closed = true;
uint8_t finField = 0b10000000;
uint8_t firstByte = finField | 0x8;
strm->WriteByte(firstByte);
strm->WriteByte(0);
this->strm = nullptr;
mtx.Unlock();
this->conn->OnClose(true);
}
void write_len_bytes(uint64_t len) {
if (len < 126) {
strm->WriteByte((uint8_t)len);
} else if (len < 65535) {
uint8_t b[3];
b[0] = 126;
b[1] = (uint8_t)(len >> 8);
b[2] = (uint8_t)len;
strm->WriteBlock(b, sizeof(b));
} else {
uint8_t b[9];
b[0] = 127;
b[1] = (uint8_t)(len >> 56);
b[2] = (uint8_t)(len >> 48);
b[3] = (uint8_t)(len >> 40);
b[4] = (uint8_t)(len >> 32);
b[5] = (uint8_t)(len >> 24);
b[6] = (uint8_t)(len >> 16);
b[7] = (uint8_t)(len >> 8);
b[8] = (uint8_t)len;
strm->WriteBlock(b, sizeof(b));
}
}
uint64_t get_long() {
uint8_t buff[8];
if (strm->ReadBlock(buff, sizeof(buff)) != sizeof(buff))
return 0;
uint64_t v = 0;
v |= (uint64_t)buff[0] << 56;
v |= (uint64_t)buff[1] << 48;
v |= (uint64_t)buff[2] << 40;
v |= (uint64_t)buff[3] << 32;
v |= (uint64_t)buff[4] << 24;
v |= (uint64_t)buff[5] << 16;
v |= (uint64_t)buff[6] << 8;
v |= (uint64_t)buff[7];
return v;
}
uint16_t get_short() {
uint8_t buff[2];
if (strm->ReadBlock(buff, sizeof(buff)) != sizeof(buff))
return 0;
uint16_t v = 0;
v |= (uint16_t)buff[0] << 8;
v |= (uint16_t)buff[1];
return v;
}
void send_msg(WebSocketMessage *msg) {
while (!hasInit)
;
mtx.Lock();
uint8_t opcode = msg->isBinary ? 0x2 : 0x1;
size_t lengthLastByte = msg->data.size() % 4096;
size_t fullPackets = (msg->data.size() - lengthLastByte) / 4096;
size_t noPackets = lengthLastByte > 0 ? fullPackets + 1 : fullPackets;
size_t offset = 0;
for (size_t i = 0; i < noPackets; i++) {
bool fin = i == noPackets - 1;
uint8_t finField = fin ? 0b10000000 : 0;
uint8_t opcode2 = i == 0 ? opcode : 0;
uint8_t firstByte = finField | (opcode2 & 0xF);
size_t len = std::min((size_t)4096, msg->data.size() - offset);
strm->WriteByte(firstByte);
write_len_bytes((uint64_t)len);
strm->WriteBlock(msg->data.data() + offset, len);
offset += len;
}
mtx.Unlock();
}
void ping_send(std::vector<uint8_t> &pData) {
mtx.Lock();
uint8_t finField = 0b10000000;
uint8_t firstByte = finField | 0x9;
strm->WriteByte(firstByte);
write_len_bytes((uint64_t)pData.size());
strm->WriteBlock(pData.data(), pData.size());
mtx.Unlock();
}
void pong_send(std::vector<uint8_t> &pData) {
mtx.Lock();
uint8_t finField = 0b10000000;
uint8_t firstByte = finField | 0xA;
strm->WriteByte(firstByte);
write_len_bytes((uint64_t)pData.size());
strm->WriteBlock(pData.data(), pData.size());
mtx.Unlock();
}
bool read_packet(uint8_t len, std::vector<uint8_t> &data) {
uint8_t realLen = len & 127;
bool masked = (len & 0b10000000) > 0;
uint64_t reallen2 =
realLen >= 126 ? realLen > 126 ? get_long() : get_short() : realLen;
uint8_t mask[4];
if (masked) {
if (strm->ReadBlock(mask, sizeof(mask)) != sizeof(mask))
return false;
}
size_t offset = data.size();
data.resize(offset + (size_t)reallen2);
if (data.size() < ((uint64_t)offset + reallen2))
return false;
if (strm->ReadBlock(data.data() + offset, (size_t)reallen2) !=
(size_t)reallen2)
return false;
if (masked) {
for (size_t i = 0; i < (size_t)reallen2; i++) {
data[i + offset] ^= mask[i % 4];
}
}
return true;
}
WSServer(ServerContext *ctx, std::shared_ptr<WebSocketConnection> conn) {
this->ctx = ctx;
this->conn = conn;
this->strm = this->ctx->GetStream();
this->hasInit = false;
}
void Start() {
this->closed = false;
std::string key;
if (ctx->requestHeaders.TryGetFirst("Sec-WebSocket-Key", key) &&
!key.empty()) {
key.append("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
auto res = Crypto::Sha1::ComputeHash((const uint8_t *)key.c_str(),
key.size());
if (res.empty())
return;
key = Crypto::Base64_Encode(res);
} else {
return;
}
if (!ctx->requestHeaders.AnyEquals("Upgrade", "websocket")) {
return;
}
if (!ctx->requestHeaders.AnyEquals("Sec-WebSocket-Version", "13")) {
return;
}
ctx->statusCode = StatusCode::SwitchingProtocols;
ctx->responseHeaders.SetValue("Connection", "Upgrade");
ctx->responseHeaders.SetValue("Upgrade", "websocket");
ctx->responseHeaders.SetValue("Sec-WebSocket-Accept", key);
ctx->WriteHeaders();
bool hasMessage = false;
WebSocketMessage message;
message.isBinary = false;
message.data = {};
hasInit = true;
while (!strm->EndOfStream()) {
uint8_t frame_start[2];
if (strm->ReadBlock(frame_start, 2) != 2)
return;
uint8_t opcode = frame_start[0] & 0xF;
bool fin = (frame_start[0] & 0b10000000) > 0;
switch (opcode) {
case 0x0:
if (!hasMessage)
break;
read_packet(frame_start[1], message.data);
break;
case 0x1:
case 0x2:
hasMessage = true;
message.data = {};
message.isBinary = opcode == 0x2;
read_packet(frame_start[1], message.data);
break;
case 0x8:
if (!this->closed)
this->close();
this->conn->OnClose(true);
return;
case 0x9: {
std::vector<uint8_t> b;
read_packet(frame_start[1], b);
pong_send(b);
} break;
case 0xA: {
std::vector<uint8_t> b;
read_packet(frame_start[1], b);
}
}
if (fin && hasMessage) {
hasMessage = false;
this->conn->OnReceive(message);
message.data = {};
}
}
this->conn->OnClose(false);
}
};
/*
static int _header_field(multipart_parser* p, const char *at, size_t length)
{
UploadData* data = static_cast<UploadData*>(multipart_parser_get_data(p));
data->currentHeaderKey = std::string(at,length);
return 0;
}
static int _header_value(multipart_parser* p, const char *at, size_t length)
{
UploadData* data = static_cast<UploadData*>(multipart_parser_get_data(p));
data->currentHeaders.AddValue(data->currentHeaderKey,
std::string(at,length)); std::cout << data->currentHeaderKey << ": " <<
std::string(at,length) << std::endl; return 0;
}
static int _part_begin(multipart_parser* p)
{
UploadData* data = static_cast<UploadData*>(multipart_parser_get_data(p));
std::string cd0;
ContentDisposition cd1;
std::string ct;
if(!data->currentHeaders.TryGetFirst("Content-Type",ct))
ct = "application/octet-stream";
if(data->currentHeaders.TryGetFirst("Content-Disposition", cd0) &&
ContentDisposition::TryParse(cd0,cd1))
{
if(cd1.filename.empty())
{
data->isFile=false;
data->key = cd1.fieldName;
data->currentBody = new MemoryStream(true);
}
else
{
data->isFile = true;
data->currentBody = data->cb(ct, cd1.filename, cd1.fieldName);
}
}
return 0;
}
static int _part_end(multipart_parser* p)
{
UploadData* data = static_cast<UploadData*>(multipart_parser_get_data(p));
if(data->currentBody == nullptr) return 0;
if(data->isFile)
{
delete data->currentBody;
data->currentBody = nullptr;
}
else
{
MemoryStream* ms = dynamic_cast<MemoryStream*>(data->currentBody);
if(ms != nullptr)
{
ms->Seek(0, SeekOrigin::Begin);
auto& buff = ms->GetBuffer();
data->ctx->queryParams.AddValue(data->key,
std::string(buff.begin(),buff.end()));
}
delete data->currentBody;
data->currentBody=nullptr;
}
data->currentHeaders.Clear();
return 0;
}*/
std::string ServerContext::GetUrlWithQuery() {
if (this->queryParams.kvp.empty())
return this->path;
return this->path + "?" + HttpUtils::QueryParamsEncode(this->queryParams);
}
std::string ServerContext::GetOriginalPathWithQuery() {
if (this->queryParams.kvp.empty())
return this->originalPath;
return this->originalPath + "?" +
HttpUtils::QueryParamsEncode(this->queryParams);
}
void ServerContext::ReadStream(std::shared_ptr<Stream> strm) {
if (strm == nullptr)
return;
auto strm2 = this->OpenRequestStream();
if (strm2 != nullptr) {
strm2->CopyTo(strm);
}
}
void ServerContext::SendServerSentEvents(
std::shared_ptr<ServerSentEvents> sse) {
this->responseHeaders.SetValue("X-Accel-Buffering", "no");
this->responseHeaders.SetValue("Content-Type", "text/event-stream");
this->responseHeaders.SetValue("Cache-Control", "no-cache");
auto strm = this->OpenResponseStream();
if (strm == nullptr || this->method == "HEAD")
return;
sse->mtx.Lock();
sse->strms.push_back(strm);
sse->mtx.Unlock();
while (!this->strm->EndOfStream()) {
TF_Sleep(10);
}
sse->mtx.Lock();
for (auto index = sse->strms.begin(); index != sse->strms.end(); index++) {
if (*index == strm) {
sse->strms.erase(index);
break;
}
}
sse->mtx.Unlock();
}
std::string ServerContext::ReadString() {
if (strm == nullptr)
return {};
auto strm2 = this->OpenRequestStream();
if (strm2 != nullptr) {
TextStreams::StreamReader reader(strm2);
return reader.ReadToEnd();
}
return {};
}
bool ServerContext::NeedToParseFormData() {
std::string ct;
if (this->requestHeaders.TryGetFirst("Content-Type", ct)) {
if (ct.find("multipart/form-data") == 0) {
return true;
}
}
return false;
}
static bool
parseUntillBoundaryEnd(std::shared_ptr<Tesses::Framework::Streams::Stream> src,
std::shared_ptr<Tesses::Framework::Streams::Stream> dest,
std::string boundary) {
bool hasMore = true;
std::vector<uint8_t> checkBuffer(boundary.size());
int b;
size_t i = 0;
size_t i2 = 0;
uint8_t buffer[1024];
size_t offsetInMem = 0;
while ((b = src->ReadByte()) != -1) {
if (i == boundary.size()) {
if (b == '-') {
i2++;
if (i2 == 2)
hasMore = false;
}
if (b == '\n')
break;
else if (b != '-')
i2 = 0;
continue;
}
i2 = 0;
if (b == boundary[i]) // start filling the check buffer
{
checkBuffer[i] = (uint8_t)b;
i++;
} else {
size_t idx = 0;
while (idx < i) // write the buffer data to stream
{
if (offsetInMem >= sizeof(buffer)) {
dest->Write(buffer, sizeof(buffer));
offsetInMem = 0;
}
buffer[offsetInMem++] = checkBuffer[idx];
idx++;
}
i = 0;
if (offsetInMem >= sizeof(buffer)) {
dest->Write(buffer, sizeof(buffer));
offsetInMem = 0;
}
buffer[offsetInMem++] = (uint8_t)b;
}
}
if (offsetInMem > 0) {
dest->Write(buffer, offsetInMem);
}
return hasMore;
}
static bool
parseSection(ServerContext *ctx, std::string boundary,
std::function<std::shared_ptr<Tesses::Framework::Streams::Stream>(
std::string mime, std::string filename, std::string name)>
cb) {
HttpDictionary req;
StreamReader reader(ctx->GetStream());
std::string line;
while (reader.ReadLineHttp(line)) {
auto v = HttpUtils::SplitString(line, ": ", 2);
if (v.size() == 2)
req.AddValue(v[0], v[1]);
line.clear();
}
std::string cd0;
ContentDisposition cd1;
std::string ct;
if (!req.TryGetFirst("Content-Type", ct))
ct = "application/octet-stream";
if (req.TryGetFirst("Content-Disposition", cd0) &&
ContentDisposition::TryParse(cd0, cd1)) {
if (cd1.filename.empty()) {
std::shared_ptr<MemoryStream> ms =
std::make_shared<MemoryStream>(true);
bool retVal =
parseUntillBoundaryEnd(ctx->GetStream(), ms, boundary);
auto &buff = ms->GetBuffer();
ctx->queryParams.AddValue(cd1.fieldName,
std::string(buff.begin(), buff.end()));
return retVal;
} else {
auto strm = cb(ct, cd1.filename, cd1.fieldName);
if (strm == nullptr)
strm = std::make_shared<Stream>();
bool retVal =
parseUntillBoundaryEnd(ctx->GetStream(), strm, boundary);
return retVal;
}
}
return false;
}
void ServerContext::ParseFormData(
std::function<std::shared_ptr<Tesses::Framework::Streams::Stream>(
std::string mime, std::string filename, std::string name)>
cb) {
std::string ct;
if (this->requestHeaders.TryGetFirst("Content-Type", ct)) {
if (ct.find("multipart/form-data") != 0) {
throw std::runtime_error("Not form data");
}
auto res = ct.find("boundary=");
if (res == std::string::npos)
return;
ct = "--" + ct.substr(res + 9);
}
std::shared_ptr<Stream> nullStrm = std::make_shared<Stream>();
parseUntillBoundaryEnd(this->strm, nullStrm, ct);
while (parseSection(this, ct, cb))
;
}
HttpServer::HttpServer(
std::shared_ptr<Tesses::Framework::Streams::TcpServer> tcpServer,
std::shared_ptr<IHttpServer> http, bool showIPs, bool debug) {
this->server = tcpServer;
this->http = http;
this->showIPs = showIPs;
this->thrd = nullptr;
this->showARTL = showIPs;
this->debug = debug;
}
HttpServer::HttpServer(uint16_t port, std::shared_ptr<IHttpServer> http,
bool showIPs, bool debug)
: HttpServer(std::make_shared<TcpServer>(port, 10), http, showIPs, debug) {}
HttpServer::HttpServer(std::string unixPath, std::shared_ptr<IHttpServer> http,
bool debug)
: HttpServer(std::make_shared<TcpServer>(unixPath, 10), http, false,
debug) {
this->showARTL = true;
}
uint16_t HttpServer::GetPort() {
if (server != nullptr)
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;
if (this->version == "HTTP/1.1" && length == -1)
this->responseHeaders.SetValue("Transfer-Encoding", "chunked");
this->WriteHeaders();
auto strm = std::make_shared<HttpStream>(this->strm, length, false,
version == "HTTP/1.1");
if (method == "HEAD") {
strm->Close();
}
return strm;
}
std::shared_ptr<Stream> ServerContext::OpenRequestStream() {
int64_t length = -1;
if (!this->requestHeaders.TryGetFirstInt("Content-Length", length))
length = -1;
return std::make_shared<HttpStream>(this->strm, length, true,
version == "HTTP/1.1");
}
void HttpServer::StartAccepting() {
fflush(stdout);
if (http == nullptr || server == nullptr)
return;
auto svr = this->server;
auto serverPort = this->server->GetPort();
auto http = this->http;
TF_LOG("Before Creating Thread");
bool debug = this->debug;
thrd = new Threading::Thread([svr, http, serverPort, debug]() -> void {
while (TF_IsRunning()) {
TF_LOG("after TF_IsRunning");
std::string ip;
uint16_t port;
auto sock = svr->GetStream(ip, port);
TF_LOG("New Host IP: " + ip + ":" + std::to_string(port));
if (sock == nullptr) {
std::cout << "STREAM ERROR" << std::endl;
return;
}
TF_LOG("Before entering socket thread");
Threading::Thread thrd2(
[sock, http, ip, port, serverPort, debug]() -> void {
TF_LOG("In thread to process");
HttpServer::Process(sock, http, ip, port, serverPort, false,
debug);
TF_LOG("In thread after process");
});
TF_LOG("Before attach");
thrd2.Detach();
TF_LOG("After attach");
}
});
if (this->showIPs) {
TF_LOG("Before printing interfaces");
StdOut() << "\x1B[34mInterfaces:" << NewLine();
for (auto _ip : NetworkStream::GetIPs()) {
StdOut() << "\x1B[32m" << _ip.first << ": "
<< "\x1B[35mhttp://" << _ip.second << ":"
<< (uint64_t)this->GetPort() << "/" << NewLine();
}
}
if (this->showARTL) {
if (!svr->IsValid())
StdOut() << "\x1B[31mError, we failed to bind or something\x1B[39m"
<< NewLine();
StdOut() << "\x1B[31mAlmost Ready to Listen\x1B[39m" << NewLine();
}
TF_LOG("After printing interfaces");
}
HttpServer::~HttpServer() {
auto port = this->GetPort();
this->server->Close();
TF_ConnectToSelf(port);
if (this->thrd != nullptr) {
this->thrd->Join();
delete this->thrd;
}
}
IHttpServer::~IHttpServer() {}
ServerContext::ServerContext(std::shared_ptr<Stream> strm, bool debug) {
this->statusCode = OK;
this->strm = strm;
this->debug = debug;
this->sent = false;
this->queryParams.SetCaseSensitive(true);
this->pathArguments.SetCaseSensitive(true);
this->responseHeaders.AddValue("Server", "TessesFrameworkWebServer");
}
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);
}
ServerContext &ServerContext::WithLastModified(Date::DateTime dt) {
this->responseHeaders.SetValue("Last-Modified", dt);
return *this;
}
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);
}
void ServerContext::SendErrorPage(bool showPath) {
if (sent)
return;
std::string errorHtml =
showPath ? ("<html><head><title>File " +
HttpUtils::HtmlEncode(this->originalPath) + " " +
HttpUtils::StatusCodeString(this->statusCode) +
"</title><meta name=\"color-scheme\" content=\"dark "
"light\"></head><body><h1>" +
std::to_string((int)this->statusCode) + " " +
HttpUtils::StatusCodeString(this->statusCode) +
"</h1><h4>" + HttpUtils::HtmlEncode(this->originalPath) +
"</h4></body></html>")
: ("<html><head><title>" + std::to_string(this->statusCode) +
" " + HttpUtils::StatusCodeString(this->statusCode) +
"</title><meta name=\"color-scheme\" content=\"dark "
"light\"></head><body><h1>" +
std::to_string((int)this->statusCode) + " " +
HttpUtils::StatusCodeString(this->statusCode) +
"</h1></body></html>");
WithMimeType("text/html").SendText(errorHtml);
}
ServerContext::~ServerContext() {
for (auto item : this->data) {
delete item.second;
}
}
ServerContextData::~ServerContextData() {}
void ServerContext::SendStream(std::shared_ptr<Stream> strm) {
if (sent)
return;
if (!strm->CanRead())
throw std::runtime_error("Cannot read from stream");
if (strm->EndOfStream())
throw std::runtime_error("End of stream");
if (strm->CanSeek()) {
int64_t len = strm->GetLength();
std::string range = {};
if (this->requestHeaders.TryGetFirst("Range", range)) {
auto res = HttpUtils::SplitString(range, "=", 2);
if (res.size() == 2) {
if (res[0] != "bytes") {
this->statusCode = BadRequest;
this->WriteHeaders();
return;
}
res = HttpUtils::SplitString(res[1], ", ", 2);
if (res.size() != 1) {
this->statusCode = BadRequest;
this->WriteHeaders();
return;
}
auto dash = HttpUtils::SplitString(res[0], "-", 2);
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) {
// NUMBER-NUMBER
// or
//-NUMBER
if (dash[0].size() > 0) {
// NUMBER-NUMBER
begin = std::stoll(dash[0]);
end = std::stoll(dash[1]);
} else {
//-NUMBER
end = std::stoll(dash[1]);
}
} else {
this->statusCode = BadRequest;
this->WriteHeaders();
return;
}
if (end == -1) {
end = len - 1;
}
if (end > len - 1) {
this->statusCode = RangeNotSatisfiable;
this->WithSingleHeader("Content-Range",
"bytes */" + std::to_string(len));
this->WriteHeaders();
return;
}
if (begin >= end) {
this->statusCode = RangeNotSatisfiable;
this->WithSingleHeader("Content-Range",
"bytes */" + std::to_string(len));
this->WriteHeaders();
return;
}
int64_t myLen = (end - begin) + 1;
this->WithSingleHeader("Accept-Ranges", "bytes");
this->WithSingleHeader("Content-Length", std::to_string(myLen));
this->WithSingleHeader("Content-Range",
"bytes " + std::to_string(begin) + "-" +
std::to_string(end) + "/" +
std::to_string(len));
this->statusCode = PartialContent;
this->WriteHeaders();
if (this->method != "HEAD") {
strm->Seek(begin, SeekOrigin::Begin);
uint8_t buffer[1024];
size_t read = 0;
do {
read = sizeof(buffer);
myLen = (end - begin) + 1;
if (myLen < read)
read = (size_t)myLen;
if (read == 0)
break;
read = strm->Read(buffer, read);
if (read == 0)
break;
this->strm->WriteBlock(buffer, read);
begin += read;
} while (read > 0 && !this->strm->EndOfStream());
}
} else {
this->statusCode = BadRequest;
this->SendErrorPage(true);
return;
}
} else {
if (len > -1) {
this->WithSingleHeader("Accept-Range", "bytes");
this->WithSingleHeader("Content-Length", std::to_string(len));
this->WriteHeaders();
if (this->method != "HEAD")
strm->CopyTo(this->strm);
}
}
} else {
auto chunkedStream = this->OpenResponseStream();
if (method != "HEAD")
strm->CopyTo(chunkedStream);
}
}
ServerContext &ServerContext::WithHeader(std::string key, std::string value) {
this->responseHeaders.AddValue(key, value);
return *this;
}
ServerContext &ServerContext::WithSingleHeader(std::string key,
std::string value) {
this->responseHeaders.SetValue(key, value);
return *this;
}
ServerContext &ServerContext::WithMimeType(std::string mime) {
this->responseHeaders.SetValue("Content-Type", mime);
return *this;
}
ServerContext &ServerContext::WithContentDisposition(std::string filename,
bool isInline) {
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;
}
ServerContext &ServerContext::WithLocationHeader(std::string url) {
return WithSingleHeader("Location", this->MakeAbsolute(url));
}
ServerContext &ServerContext::WithLocationHeader(std::string url,
StatusCode sc) {
this->statusCode = sc;
return WithSingleHeader("Location", this->MakeAbsolute(url));
}
void ServerContext::SendRedirect(std::string url) {
WithLocationHeader(url);
this->WriteHeaders();
}
void ServerContext::SendRedirect(std::string url, StatusCode sc) {
WithLocationHeader(url, sc);
this->WriteHeaders();
}
void ServerContext::SendNotFound() {
if (sent)
return;
statusCode = StatusCode::NotFound;
SendErrorPage(true);
}
void ServerContext::SendBadRequest() {
if (sent)
return;
statusCode = StatusCode::BadRequest;
SendErrorPage(false);
}
ServerContext &ServerContext::WithStatusCode(StatusCode code) {
this->statusCode = code;
return *this;
}
void ServerContext::SendException(std::exception &ex) {
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);
}
}
ServerContext &
ServerContext::WithHeaderIntercepter(std::function<bool(ServerContext &)> cb) {
this->headerhandlers.push(cb);
return *this;
}
ServerContext &ServerContext::WriteHeaders() {
if (this->sent)
return *this;
while (!this->headerhandlers.empty()) {
auto header = this->headerhandlers.front();
this->headerhandlers.pop();
if (header(*this)) {
break;
}
}
if (this->sent)
return *this;
this->sent = true;
StreamWriter writer(this->strm);
writer.newline = "\r\n";
writer.WriteLine("HTTP/1.1 " + std::to_string((int)statusCode) + " " +
HttpUtils::StatusCodeString(statusCode));
for (auto &hdr : responseHeaders.kvp) {
auto &key = hdr.first;
for (auto &val : hdr.second) {
writer.WriteLine(key + ": " + val);
}
}
writer.WriteLine();
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,
bool debug) {
TF_LOG("In process");
std::shared_ptr<BufferedStream> bStrm =
std::make_shared<BufferedStream>(strm);
StreamReader reader(bStrm);
ServerContext ctx(bStrm);
ctx.ip = ip;
ctx.port = port;
ctx.encrypted = encrypted;
ctx.serverPort = serverPort;
try {
bool firstLine = true;
std::string line;
while (reader.ReadLineHttp(line)) {
if (firstLine) {
auto v = HttpUtils::SplitString(line, " ", 3);
if (v.size() != 3) {
ctx.statusCode = BadRequest;
ctx.WithMimeType("text/plain")
.SendText("First line is not 3 elements");
return;
}
ctx.method = v[0];
auto pp = HttpUtils::SplitString(v[1], "?", 2);
pp.resize(2);
ctx.originalPath = pp[0];
ctx.path = ctx.originalPath;
TF_LOG(ctx.method + " with path " + ctx.path);
auto queryPart = pp[1];
if (!queryPart.empty()) {
HttpUtils::QueryParamsDecode(ctx.queryParams, queryPart);
}
ctx.version = v[2];
} else {
auto v = HttpUtils::SplitString(line, ": ", 2);
if (v.size() != 2) {
ctx.statusCode = BadRequest;
ctx.WithMimeType("text/plain")
.SendText("Header line is not 2 elements");
return;
}
ctx.requestHeaders.AddValue(v[0], v[1]);
}
line.clear();
firstLine = false;
}
if (firstLine)
return;
std::string type;
int64_t length;
if (ctx.requestHeaders.TryGetFirst("Content-Type", type) &&
type == "application/x-www-form-urlencoded" &&
ctx.requestHeaders.TryGetFirstInt("Content-Length", length)) {
size_t len = (size_t)length;
std::vector<uint8_t> buffer(len);
len = bStrm->ReadBlock(buffer.data(), len);
std::string query((const char *)buffer.data(), len);
HttpUtils::QueryParamsDecode(ctx.queryParams, query);
}
if (!server->Handle(ctx)) {
if ((int)ctx.statusCode < 400)
ctx.SendNotFound();
else
ctx.SendErrorPage(true);
}
}
catch (std::exception &ex) {
ctx.SendException(ex);
} catch (std::string &ex) {
std::runtime_error re(ex);
ctx.SendException(re);
} catch (const char *ex) {
std::runtime_error re(ex);
ctx.SendException(re);
} catch (...) {
std::runtime_error ex("An unknown error occurred");
ctx.SendException(ex);
}
}
bool ServerContext::Debug() { return this->debug; }
ServerContext &ServerContext::WithDebug(bool debug) {
this->debug = debug;
return *this;
}
WebSocketConnection::~WebSocketConnection() {}
void ServerContext::StartWebSocketSession(
std::function<void(std::function<void(WebSocketMessage &)>,
std::function<void()>, std::function<void()>)>
onOpen,
std::function<void(WebSocketMessage &)> onReceive,
std::function<void(bool)> onClose) {
std::shared_ptr<CallbackWebSocketConnection> wsc =
std::make_shared<CallbackWebSocketConnection>(onOpen, onReceive,
onClose);
StartWebSocketSession(wsc);
}
void ServerContext::StartWebSocketSession(
std::shared_ptr<WebSocketConnection> connection) {
WSServer svr(this, connection);
Threading::Thread thrd([&svr, &connection]() -> void {
try {
connection->OnOpen(
[&svr](WebSocketMessage &msg) -> void { svr.send_msg(&msg); },
[&svr]() -> void {
std::vector<uint8_t> p = {(uint8_t)'p', (uint8_t)'i',
(uint8_t)'n', (uint8_t)'g'};
svr.ping_send(p);
},
[&svr]() -> void { svr.close(); });
} catch (...) {
}
});
svr.Start();
thrd.Join();
}
std::string ServerContext::GetServerRoot() {
if (this->originalPath == this->path)
return "/";
Tesses::Framework::Filesystem::VFSPath originalPath = this->originalPath;
Tesses::Framework::Filesystem::VFSPath path = this->path;
if (originalPath.path.size() <= path.path.size())
return "/";
originalPath.path.resize(originalPath.path.size() - path.path.size());
return originalPath.ToString();
}
std::string ServerContext::MakeAbsolute(std::string path) {
if (path.find("http://") == 0 || path.find("https://") == 0 ||
path.find("/") == 0)
return path;
Tesses::Framework::Filesystem::VFSPath path2 = GetServerRoot();
path2 = path2 / path;
return path2.CollapseRelativeParents().ToString();
}
} // namespace Tesses::Framework::Http