1187 lines
38 KiB
C++
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
|