first commit

This commit is contained in:
2024-12-06 04:58:55 -06:00
commit 856373b396
61 changed files with 5920 additions and 0 deletions

View File

@ -0,0 +1,77 @@
#include "TessesFramework/Http/ContentDisposition.hpp"
#include "TessesFramework/Http/HttpUtils.hpp"
#include <iostream>
namespace Tesses::Framework::Http
{
bool ContentDisposition::TryParse(std::string str, ContentDisposition& cd)
{
auto res = HttpUtils::SplitString(str,"; ");
if(res.empty()) return false;
cd.type = res[0];
bool hasFileNameStar = false;
for(size_t i = 1; i < res.size(); i++)
{
auto res2 = HttpUtils::SplitString(res[i],"=",2);
if(res2.size() == 2)
{
if(res2[0] == "filename*")
{
//cd.filename = res2[1];
//UTF-8''
std::string p = res2[1];
if(p.find("UTF-8''") == 0)
{
hasFileNameStar = true;
p = HttpUtils::UrlPathDecode(p.substr(7));
cd.filename = p;
}
}
else if(res2[0] == "filename" && !hasFileNameStar)
{
std::string p = res2[1];
if(!p.empty() && p[0] == '\"')
{
p = p.substr(1, p.size()-2);
}
p = HttpUtils::UrlPathDecode(p);
cd.filename = p;
}
else if(res2[0] == "name")
{
std::string p = res2[1];
if(!p.empty() && p[0] == '\"')
{
p = p.substr(1, p.size()-2);
}
cd.fieldName = HttpUtils::UrlPathDecode(p);
}
}
}
return true;
}
std::string ContentDisposition::ToString()
{
std::vector<std::string> parts;
parts.push_back(this->type);
if(!this->fieldName.empty())
{
parts.push_back("name=\"" + HttpUtils::UrlPathEncode(this->fieldName,true) + "\"");
}
if(!this->filename.empty())
{
parts.push_back("filename=\"" + HttpUtils::UrlPathEncode(this->filename,true) + "\"");
}
return HttpUtils::Join("; ", parts);
}
}

131
src/Http/FileServer.cpp Normal file
View File

@ -0,0 +1,131 @@
#include "TessesFramework/Http/FileServer.hpp"
#include "TessesFramework/Filesystem/LocalFS.hpp"
#include "TessesFramework/Filesystem/SubdirFilesystem.hpp"
#include <iostream>
#include <unistd.h>
using LocalFilesystem = Tesses::Framework::Filesystem::LocalFilesystem;
using SubdirFilesystem = Tesses::Framework::Filesystem::SubdirFilesystem;
using VFSPath = Tesses::Framework::Filesystem::VFSPath;
using VFS = Tesses::Framework::Filesystem::VFS;
namespace Tesses::Framework::Http
{
FileServer::FileServer(std::filesystem::path path,bool allowListing,bool spa) : FileServer(path,allowListing,spa,{"index.html","default.html","index.htm","default.htm"})
{
}
FileServer::FileServer(std::filesystem::path path,bool allowListing, bool spa, std::vector<std::string> defaultNames)
{
LocalFilesystem* lfs=new LocalFilesystem;
SubdirFilesystem* sdfs=new SubdirFilesystem(lfs,lfs->SystemToVFSPath(path),true);
this->vfs = sdfs;
this->spa = spa;
this->ownsVFS=true;
this->allowListing = allowListing;
this->defaultNames = defaultNames;
}
FileServer::FileServer(Tesses::Framework::Filesystem::VFS* fs, bool owns, bool allowListing,bool spa) : FileServer(fs,owns,allowListing,spa,{"index.html","default.html","index.htm","default.htm"})
{
}
FileServer::FileServer(Tesses::Framework::Filesystem::VFS* fs, bool owns, bool allowListing, bool spa, std::vector<std::string> defaultNames)
{
this->vfs = fs;
this->ownsVFS = owns;
this->allowListing = allowListing;
this->defaultNames = defaultNames;
this->spa = spa;
}
bool FileServer::SendFile(ServerContext& ctx,VFSPath path)
{
auto strm = this->vfs->OpenFile(path,"rb");
bool retVal = false;
if(strm != nullptr)
{
ctx.WithMimeType(HttpUtils::MimeType(path.GetFileName())).SendStream(strm);
retVal = true;
}
delete strm;
return retVal;
}
bool FileServer::Handle(ServerContext& ctx)
{
auto path = HttpUtils::UrlPathDecode(ctx.path);
if(this->vfs->DirectoryExists(path))
{
for(auto f : defaultNames)
{
VFSPath p(path,f);
if(this->vfs->FileExists(p))
return SendFile(ctx,p);
}
if(this->allowListing)
{
std::string p = HttpUtils::HtmlEncode(ctx.originalPath);
std::string html = "<!DOCTYPE html><html><head><title>Index of ";
html.append(p);
html.append("</title></head><body><h1>Index of ");
html.append(p);
html.append("</h1><hr><pre><a href=\"../\">../</a>\r\n");
std::vector<VFSPath> ents;
vfs->GetPaths(path, ents);
for(auto item : ents)
{
if(vfs->DirectoryExists(item))
{
//is dir
std::string path = item.GetFileName();
html.append("<a href=\"");
html.append(HttpUtils::UrlPathEncode(path) + "/");
html.append("\">");
html.append(HttpUtils::HtmlEncode(path) + "/");
html.append("</a>\r\n");
}
else
{
//is file
std::string path = item.GetFileName();
html.append("<a href=\"");
html.append(HttpUtils::UrlPathEncode(path));
html.append("\">");
html.append(HttpUtils::HtmlEncode(path));
html.append("</a>\r\n");
}
}
html.append("</pre><hr></body></html>");
ctx.WithMimeType("text/html").SendText(html);
return true;
}
}
else if(this->vfs->FileExists(path))
{
return SendFile(ctx,path);
}
else if(this->spa)
{
for(auto f : defaultNames)
{
VFSPath p(f);
p.relative=false;
if(this->vfs->FileExists(p))
return SendFile(ctx,p);
}
}
return false;
}
FileServer::~FileServer()
{
if(this->ownsVFS)
delete this->vfs;
}
}

226
src/Http/HttpClient.cpp Normal file
View File

@ -0,0 +1,226 @@
#include "TessesFramework/Http/HttpClient.hpp"
#include "TessesFramework/Crypto/ClientTLSStream.hpp"
#include "TessesFramework/Streams/NetworkStream.hpp"
#include "TessesFramework/TextStreams/StreamWriter.hpp"
#include "TessesFramework/TextStreams/StreamReader.hpp"
#include "TessesFramework/Http/HttpStream.hpp"
#include "TessesFramework/Streams/BufferedStream.hpp"
#include <iostream>
using Stream = Tesses::Framework::Streams::Stream;
using NetworkStream = Tesses::Framework::Streams::NetworkStream;
using ClientTLSStream = Tesses::Framework::Crypto::ClientTLSStream;
using StreamReader = Tesses::Framework::TextStreams::StreamReader;
using StreamWriter = Tesses::Framework::TextStreams::StreamWriter;
using BufferedStream = Tesses::Framework::Streams::BufferedStream;
using HttpStream = Tesses::Framework::Http::HttpStream;
namespace Tesses::Framework::Http
{
HttpRequest::HttpRequest()
{
this->body=nullptr;
this->followRedirects=true;
this->ignoreSSLErrors=false;
this->trusted_root_cert_bundle="";
this->method = "GET";
this->requestHeaders.SetValue("Connection","close");
}
void HttpRequest::SendRequest(Tesses::Framework::Streams::Stream* strm)
{
Uri uri;
if(!Uri::TryParse(this->url, uri)) return;
if(body != nullptr)
{
body->HandleHeaders(requestHeaders);
}
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);
request.append(": ");
request.append(item);
request.append("\r\n");
}
}
request.append("\r\n");
StreamWriter writer(strm, false);
writer.Write(request);
if(body != nullptr)
{
body->Write(strm);
}
}
Stream* HttpRequest::EstablishConnection(Uri uri, bool ignoreSSLErrors, std::string trusted_root_cert_bundle)
{
if(uri.scheme == "http:")
{
return new NetworkStream(uri.host,uri.GetPort(),false,false,false);
}
else if(uri.scheme == "https:")
{
auto netStrm = new NetworkStream(uri.host,uri.GetPort(),false,false,false);
if(netStrm == nullptr) return netStrm;
return new ClientTLSStream(netStrm, true, ignoreSSLErrors,uri.host, trusted_root_cert_bundle);
}
return nullptr;
}
HttpResponse::~HttpResponse()
{
if(this->owns)
delete this->handleStrm;
}
HttpResponse::HttpResponse(Stream* strm, bool owns)
{
this->handleStrm=nullptr;
this->owns=owns;
StreamReader reader(strm, false);
std::string statusLine;
if(!reader.ReadLine(statusLine)) return;
auto statusLinesPart = HttpUtils::SplitString(statusLine," ",3);
if(statusLinesPart.size() >= 2)
{
this->version = statusLinesPart[0];
this->statusCode = (StatusCode)std::stoi(statusLinesPart[1]);
}
std::string line;
while(reader.ReadLine(line))
{
if(line.empty()) break;
auto v = HttpUtils::SplitString(line,": ", 2);
if(v.size() != 2)
{
delete strm;
return;
}
this->responseHeaders.AddValue(v[0],v[1]);
line.clear();
}
this->handleStrm = strm;
}
HttpResponse::HttpResponse(HttpRequest& req)
{
this->owns=true;
this->handleStrm = nullptr;
std::string url = req.url;
Uri uri;
while(Uri::TryParse(url, uri))
{
auto strm = HttpRequest::EstablishConnection(uri, req.ignoreSSLErrors,req.trusted_root_cert_bundle);
if(strm == nullptr) return;
auto reqHeaders = req.requestHeaders;
if(req.body != nullptr)
{
req.body->HandleHeaders(reqHeaders);
}
std::string request = req.method + " " + uri.GetPathAndQuery() + " HTTP/1.1\r\nHost: " + uri.HostPort() + "\r\n";
for(auto headers : reqHeaders.kvp)
{
for(auto item : headers.second)
{
request.append(headers.first);
request.append(": ");
request.append(item);
request.append("\r\n");
}
}
request.append("\r\n");
StreamWriter writer(strm, false);
writer.Write(request);
if(req.body != nullptr)
{
req.body->Write(strm);
}
StreamReader reader(strm, false);
std::string statusLine;
if(!reader.ReadLine(statusLine)) break;
auto statusLinesPart = HttpUtils::SplitString(statusLine," ",3);
if(statusLinesPart.size() >= 2)
{
this->version = statusLinesPart[0];
this->statusCode = (StatusCode)std::stoi(statusLinesPart[1]);
}
std::string line;
while(reader.ReadLine(line))
{
if(line.empty()) break;
auto v = HttpUtils::SplitString(line,": ", 2);
if(v.size() != 2)
{
delete strm;
return;
}
this->responseHeaders.AddValue(v[0],v[1]);
line.clear();
}
std::string location;
Uri uri2;
if(req.followRedirects && (this->statusCode == MovedPermanently || this->statusCode == PermanentRedirect || this->statusCode == TemporaryRedirect) && this->responseHeaders.TryGetFirst("Location",location) && uri.Relative(location, uri2))
{
this->responseHeaders.Clear();
url = uri2.ToString();
delete strm;
continue;
}
this->handleStrm = strm;
break;
}
}
std::string HttpResponse::ReadAsString()
{
auto strm = ReadAsStream();
if(strm == nullptr) return {};
StreamReader r(strm, true);
return r.ReadToEnd();
}
void HttpResponse::CopyToStream(Stream* strm)
{
if(strm == nullptr) return;
auto src = ReadAsStream();
if(src == nullptr) return;
src->CopyTo(*strm);
delete src;
}
Stream* HttpResponse::ReadAsStream()
{
if(this->handleStrm == nullptr) return nullptr;
int64_t length = -1;
if(!this->responseHeaders.TryGetFirstInt("Content-Length",length))
{
length = -1;
}
return new HttpStream(this->handleStrm,false,length,true,version=="HTTP/1.1");
}
}

651
src/Http/HttpServer.cpp Normal file
View File

@ -0,0 +1,651 @@
#include "TessesFramework/Http/HttpServer.hpp"
#include "TessesFramework/Streams/FileStream.hpp"
#include "TessesFramework/Streams/MemoryStream.hpp"
#include "TessesFramework/TextStreams/StreamWriter.hpp"
#include "TessesFramework/TextStreams/StreamReader.hpp"
#include "TessesFramework/Http/ContentDisposition.hpp"
#include "TessesFramework/Streams/BufferedStream.hpp"
#include "TessesFramework/Http/HttpStream.hpp"
#include <iostream>
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 StreamReader = Tesses::Framework::TextStreams::StreamReader;
using StreamWriter = Tesses::Framework::TextStreams::StreamWriter;
using TcpServer = Tesses::Framework::Streams::TcpServer;
using NetworkStream = Tesses::Framework::Streams::NetworkStream;
using BufferedStream = Tesses::Framework::Streams::BufferedStream;
namespace Tesses::Framework::Http
{
/*
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;
}*/
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(Tesses::Framework::Streams::Stream* src, Tesses::Framework::Streams::Stream* dest, std::string boundary)
{
bool hasMore=true;
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<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.ReadLine(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())
{
MemoryStream ms(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);
bool retVal = parseUntillBoundaryEnd(&ctx->GetStream(),strm,boundary);
delete strm;
return retVal;
}
}
return false;
}
void ServerContext::ParseFormData(std::function<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)
{
std::cout << "Not form data" << std::endl;
return;
}
auto res = ct.find("boundary=");
if(res == std::string::npos) return;
ct = "--" + ct.substr(res+9);
}
Stream nullStrm;
parseUntillBoundaryEnd(this->strm,&nullStrm, ct);
while(parseSection(this, ct, cb));
}
HttpServer::HttpServer(uint16_t port, IHttpServer* http, bool owns)
{
this->server = new TcpServer(port, 10);
this->http = http;
this->owns = owns;
this->thrd=nullptr;
this->port = port;
}
HttpServer::HttpServer(uint16_t port, IHttpServer& http) : HttpServer(port,&http,false)
{
}
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();
return new HttpStream(this->strm,false,length,false,version == "HTTP/1.1");
}
Stream* ServerContext::OpenRequestStream()
{
int64_t length = -1;
if(!this->requestHeaders.TryGetFirstInt("Content-Length",length))
length = -1;
return new HttpStream(this->strm,false,length,true,version == "HTTP/1.1");
}
void HttpServer::StartAccepting()
{
fflush(stdout);
if(http == nullptr) return;
auto svr=this->server;
auto http = this->http;
thrd = new Threading::Thread([svr,http]()->void {
while(TF_IsRunning())
{
std::string ip;
uint16_t port;
auto sock =svr->GetStream(ip,port);
if(sock == nullptr) return;
Threading::Thread thrd2([sock,http,ip,port]()->void {
HttpServer::Process(*sock,*http,ip,port,false);
});
thrd2.Detach();
}
});
std::cout << "\e[34mInterfaces:\n";
for(auto _ip : NetworkStream::GetIPs())
{
std::cout << "\e[32m";
std::cout << _ip.first << ": ";
std::cout << "\e[35mhttp://";
std::cout << _ip.second << ":" << std::to_string(this->port) << "/\n";
}
std::cout << "\e[31mAlmost Ready to Listen\e[39m\n";
}
HttpServer::~HttpServer()
{
this->server->Close();
TF_ConnectToSelf(this->port);
if(this->thrd != nullptr)
{
this->thrd->Join();
delete this->thrd;
}
if(this->owns)
delete http;
delete this->server;
}
IHttpServer::~IHttpServer()
{
}
ServerContext::ServerContext(Stream* strm)
{
this->statusCode = OK;
this->strm = strm;
this->sent = false;
this->responseHeaders.AddValue("Server","TessesFrameworkWebServer");
}
Stream& ServerContext::GetStream()
{
return *this->strm;
}
void ServerContext::SendBytes(std::vector<uint8_t> buff)
{
MemoryStream strm(false);
strm.GetBuffer() = buff;
SendStream(strm);
}
void ServerContext::SendText(std::string text)
{
MemoryStream strm(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></head><body><h1>" + std::to_string((int)this->statusCode) + " " + HttpUtils::StatusCodeString(this->statusCode) + "</h1><h4>" + HttpUtils::HtmlEncode(this->originalPath) + "</h4></body></html>") : "";
WithMimeType("text/html").SendText(errorHtml);
}
void ServerContext::SendStream(Stream* strm)
{
if(strm == nullptr) return;
SendStream(*strm);
}
void ServerContext::SendStream(Stream& strm)
{
if(sent) return;
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();
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);
}
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();
strm.CopyTo(*this->strm);
}
}
}
else
{
auto chunkedStream = this->OpenResponseStream();
this->strm->CopyTo(chunkedStream);
delete 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;
}
void ServerContext::SendNotFound()
{
if(sent) return;
statusCode = StatusCode::NotFound;
SendErrorPage(true);
}
void ServerContext::SendBadRequest()
{
if(sent) return;
statusCode = StatusCode::BadRequest;
SendErrorPage(false);
}
void ServerContext::SendException(std::exception& ex)
{
/*<!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 /</title>
</head>
<body>
<h1>Internal Server Error at /</h1>
<p>what(): std::exception</p>
</body>
</html>*/
this->WithMimeType("text/html").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></head><body><h1>Internal Server Error at " + HttpUtils::HtmlEncode(this->originalPath) + "</h1><p>what(): " + HttpUtils::HtmlEncode(ex.what()) + "</p></body></html>");
}
ServerContext& ServerContext::WriteHeaders()
{
if(this->sent) return *this;
this->sent = true;
StreamWriter writer(this->strm,false);
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(Stream& strm, IHttpServer& server, std::string ip, uint16_t port, bool encrypted)
{
while(true)
{
BufferedStream bStrm(strm);
StreamReader reader(bStrm);
ServerContext ctx(&bStrm);
ctx.ip = ip;
ctx.port = port;
ctx.encrypted = encrypted;
try{
bool firstLine = true;
std::string line;
while(reader.ReadLine(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;
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;
uint8_t* buffer = new uint8_t[len];
len = bStrm.ReadBlock(buffer,len);
std::string query((const char*)buffer,len);
delete buffer;
HttpUtils::QueryParamsDecode(ctx.queryParams, query);
}
if(!server.Handle(ctx))
{
ctx.SendNotFound();
}
}
catch(std::exception& ex)
{
ctx.SendException(ex);
}
if(ctx.version != "HTTP/1.1" ) return;
std::string connection;
if(ctx.requestHeaders.TryGetFirst("Connection", connection))
{
if(connection != "keep-alive") return;
}
if(ctx.responseHeaders.TryGetFirst("Connection", connection))
{
if(connection != "keep-alive") return;
}
if(bStrm.EndOfStream()) return;
}
}
}

166
src/Http/HttpStream.cpp Normal file
View File

@ -0,0 +1,166 @@
#include "TessesFramework/Http/HttpStream.hpp"
#include "TessesFramework/TextStreams/StreamWriter.hpp"
#include "TessesFramework/TextStreams/StreamReader.hpp"
#include <sstream>
#include <iostream>
using StreamWriter = Tesses::Framework::TextStreams::StreamWriter;
using StreamReader = Tesses::Framework::TextStreams::StreamReader;
namespace Tesses::Framework::Http
{
HttpStream::HttpStream(Tesses::Framework::Streams::Stream* strm, bool owns, int64_t length, bool recv, bool http1_1)
{
this->strm = strm;
this->owns = owns;
this->length = length;
this->recv = recv;
this->http1_1 = http1_1;
this->offset = 0;
this->read = 0;
this->position = 0;
this->done=false;
}
HttpStream::HttpStream(Tesses::Framework::Streams::Stream& strm, int64_t length, bool recv,bool http1_1) : HttpStream(&strm,false,length,recv,http1_1)
{
}
bool HttpStream::CanRead()
{
if(this->done) return false;
if(!this->recv) return false;
if(this->offset < this->read) return true;
return this->strm->CanRead();
}
bool HttpStream::CanWrite()
{
if(this->recv) return false;
return this->strm->CanWrite();
}
bool HttpStream::EndOfStream()
{
if(this->done) return true;
if(!this->recv) return true;
if(this->offset < this->read) return false;
return this->strm->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(this->done) return 0;
if(!this->recv) return 0;
if(this->length == 0) return 0;
if(this->length > 0)
{
len = std::min((size_t)(this->length - this->position), len);
if(len > 0)
len = this->strm->Read(buff,len);
this->position += len;
return len;
}
else
{
if(this->http1_1)
{
if(this->offset < this->read)
{
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;
if(this->offset >= this->read)
{
StreamReader reader(this->strm, false);
reader.ReadLine();
}
return len;
}
else
{
StreamReader reader(this->strm, false);
std::string line = reader.ReadLine();
if(!line.empty())
{
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);
this->offset += len;
this->position += len;
return len;
}
}
return 0;
}
}
else
{
return this->strm->Read(buff,len);
}
}
}
size_t HttpStream::Write(const uint8_t* buff, size_t len)
{
if(this->recv) return 0;
if(this->length == 0) return 0;
if(this->length > 0)
{
len = std::min((size_t)(this->length - this->position), len);
if(len > 0)
len = this->strm->Write(buff,len);
this->position += len;
return len;
}
else
{
if(this->http1_1)
{
std::stringstream strm;
strm << std::hex << len;
StreamWriter writer(this->strm,false);
writer.newline = "\r\n";
writer.WriteLine(strm.str());
this->strm->WriteBlock(buff, len);
writer.WriteLine();
return len;
}
else
{
return this->strm->Write(buff,len);
}
}
}
HttpStream::~HttpStream()
{
if(this->length == -1 && this->http1_1)
{
StreamWriter writer(this->strm,false);
writer.newline = "\r\n";
writer.WriteLine("0");
writer.WriteLine();
}
if(this->owns) delete this->strm;
}
}

732
src/Http/HttpUtils.cpp Normal file
View File

@ -0,0 +1,732 @@
#include "TessesFramework/Http/HttpUtils.hpp"
#include "TessesFramework/Filesystem/VFS.hpp"
using VFSPath = Tesses::Framework::Filesystem::VFSPath;
namespace Tesses::Framework::Http {
bool Uri::Relative(std::string url, Uri& uri)
{
auto index = url.find_first_of("//");
if(index != std::string::npos)
{
if(Uri::TryParse(url,uri))
{
if(index == 0)
uri.scheme = this->scheme;
return true;
}
}
else if(!url.empty())
{
if(url[0] == '/')
{
auto thirdPart = HttpUtils::SplitString(url,"#",2);
if(thirdPart.empty()) return false;
if(thirdPart.size() == 2)
{
uri.hash=thirdPart[1];
}
auto fourthPart = HttpUtils::SplitString(thirdPart[1],"?",2);
VFSPath p = fourthPart[0];
uri.path = p.CollapseRelativeParents().ToString(); //this should be safe
if(fourthPart.size() == 2)
{
HttpUtils::QueryParamsDecode(uri.query, fourthPart[1]);
}
}
else
{
auto thirdPart = HttpUtils::SplitString(url,"#",2);
if(thirdPart.empty()) return false;
if(thirdPart.size() == 2)
{
uri.hash=thirdPart[1];
}
auto fourthPart = HttpUtils::SplitString(thirdPart[1],"?",2);
VFSPath p = VFSPath(this->path,fourthPart[0]);
uri.path = p.CollapseRelativeParents().ToString(); //this should be safe
if(fourthPart.size() == 2)
{
HttpUtils::QueryParamsDecode(uri.query, fourthPart[1]);
}
}
uri.scheme = this->scheme;
uri.host = this->host;
uri.port = this->port;
return true;
}
return false;
}
std::string Uri::HostPort()
{
if(this->port != 0) return this->host + ":" + std::to_string(this->port);
return this->host;
}
uint16_t Uri::GetPort()
{
if(this->port != 0) return this->port;
if(this->scheme == "http:")
return 80;
if(this->scheme == "https:")
return 443;
if(this->scheme == "sftp:")
return 22;
if(this->scheme == "ftp:")
return 21;
if(this->scheme == "tftp:")
return 69;
return 0;
}
bool Uri::TryParse(std::string 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())
return false;
auto secondPart = HttpUtils::SplitString(firstPart.size() == 2 ? firstPart[1] : firstPart[0] ,"/",2);
if(secondPart.size() == 1)
{
uri.path = "/";
}
else if(secondPart.size() == 2)
{
auto thirdPart = HttpUtils::SplitString(secondPart[1],"#",2);
if(thirdPart.empty()) return false;
if(thirdPart.size() == 2)
{
uri.hash=thirdPart[1];
}
auto fourthPart = HttpUtils::SplitString(thirdPart[0],"?",2);
uri.path = "/" + fourthPart[0]; //this should be safe
if(fourthPart.size() == 2)
{
HttpUtils::QueryParamsDecode(uri.query, fourthPart[1]);
}
}
else
{
return false;
}
auto hostPortPart = HttpUtils::SplitString(secondPart[0],":",2);
if(hostPortPart.empty()) return false;
if(hostPortPart.size() == 2)
{
uri.port = (uint16_t)std::stoul(hostPortPart[1]);
}
uri.host = hostPortPart[0];
return true;
}
std::string Uri::GetPathAndQuery()
{
return this->path + this->GetQuery();
}
std::string Uri::GetQuery()
{
if(this->query.kvp.empty()) return "";
std::string queryStr = "?";
queryStr.append(HttpUtils::QueryParamsEncode(query));
return queryStr;
}
std::string Uri::ToString()
{
std::string uri = this->scheme;
uri.append("//");
uri.append(this->host);
if(this->port > 0)
{
uri.push_back(':');
uri.append(std::to_string(this->port));
}
uri.append(this->GetPathAndQuery());
return uri;
}
char HttpUtils::NibbleToHex(uint8_t b)
{
b %= 16;
if(b >= 0 && b <= 9)
return b + '0';
if(b >= 10 && b <= 15)
return b + ('a' - 10);
return 0;
}
uint8_t HttpUtils::HexToNibble(char c)
{
if(c >= '0' && c <= '9')
return (uint8_t)(c - '0');
if(c >= 'A' && c <= 'F')
return (uint8_t)(c - 55);
if(c >= 'a' && c <= 'f')
return (uint8_t)(c - 87);
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";
}
return "application/octet-stream";
}
bool HttpUtils::Invalid(char c)
{
//just do windows because it is the strictist when it comes to windows, mac and linux
if(c >= 0 && c < 32) return true;
if(c == 127) return true;
if(c == '\\') return true;
if(c == '*') return true;
if(c == '/') return true;
if(c == '|') return true;
if(c == ':') return true;
if(c == '<') return true;
if(c == '>') return true;
if(c == '\"') return true;
if(c == '?') return true;
return false;
}
std::string HttpUtils::Sanitise(std::string text)
{
std::string myStr={};
for(auto item : text)
{
if(Invalid(item)) continue;
myStr.push_back(item);
}
return myStr;
}
void HttpUtils::QueryParamsDecode(HttpDictionary& dict,std::string query)
{
for(auto item : SplitString(query,"&"))
{
std::vector<std::string> ss=SplitString(item,"=",2);
if(ss.size() >= 1)
{
std::string value = {};
if(ss.size() == 2)
{
value = UrlDecode(ss[1]);
}
dict.AddValue(UrlDecode(ss[0]),value);
}
}
}
std::string HttpUtils::Join(std::string joinStr, std::vector<std::string> ents)
{
std::string str={};
bool first=true;
for(auto item : ents)
{
if(!first) str.append(joinStr);
str.append(item);
first=false;
}
return str;
}
std::string HttpUtils::QueryParamsEncode(HttpDictionary& dict)
{
std::string s={};
bool first = true;
for(auto item : dict.kvp)
{
for(auto item2 : item.second)
{
if(!first)
{
s.push_back('&');
}
s.insert(s.size(),UrlEncode(item.first));
s.push_back('=');
s.insert(s.size(),UrlEncode(item2));
first=false;
}
}
return s;
}
std::string HttpUtils::UrlDecode(std::string 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);
}
else
s.push_back(v[i]);
}
return s;
}
std::string HttpUtils::UrlPathEncode(std::string v,bool ignoreSpace)
{
std::string s = {};
for(auto item : v)
{
if(item >= 'A' && item <= 'Z')
s.push_back(item);
else if(item >= 'a' && item <= 'z')
s.push_back(item);
else if(item >= '0' && item <= '9')
s.push_back(item);
else if(item == '-' || item == '_' || item == '.' || item == '~')
s.push_back(item);
else
{
if(item != ' ' || !ignoreSpace)
{
s.push_back('%');
s.push_back(NibbleToHex((item >> 4) & 0xF));
s.push_back(NibbleToHex((item) & 0xF));
}
else
{
s.push_back(' ');
}
}
}
return s;
}
std::string HttpUtils::UrlPathDecode(std::string 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);
}
else
s.push_back(v[i]);
}
return s;
}
std::string HttpUtils::UrlEncode(std::string v)
{
std::string s = {};
for(auto item : v)
{
if(item == ' ')
s.push_back('+');
else if(item >= 'A' && item <= 'Z')
s.push_back(item);
else if(item >= 'a' && item <= 'z')
s.push_back(item);
else if(item >= '0' && item <= '9')
s.push_back(item);
else if(item == '-' || item == '_' || item == '.' || item == '~')
s.push_back(item);
else
{
s.push_back('%');
s.push_back(NibbleToHex((item >> 4) & 0xF));
s.push_back(NibbleToHex((item) & 0xF));
}
}
return s;
}
std::vector<std::string> HttpUtils::SplitString(std::string text, std::string delimiter,std::size_t maxCnt)
{
std::vector<std::string> strs;
std::size_t i = 1;
while(text.length() > 0)
{
if(i == maxCnt)
{
strs.push_back(text);
break;
}
std::size_t index= text.find(delimiter);
if(index == std::string::npos)
{
strs.push_back(text);
break;
}
else
{
std::string left = text.substr(0,index);
text = text.substr(index+delimiter.size());
strs.push_back(left);
}
i++;
}
return strs;
}
std::string HttpUtils::HtmlEncode(std::string html)
{
std::string myHtml = {};
for(auto item : html)
{
if(item == '\"')
{
myHtml.append("&quot;");
}
else if(item == '\'')
{
myHtml.append("&apos;");
}
else if(item == '&')
{
myHtml.append("&amp;");
}
else if(item == '<')
{
myHtml.append("&lt;");
}
else if(item == '>')
{
myHtml.append("&gt;");
}
else
{
myHtml.push_back(item);
}
}
return myHtml;
}
std::string HttpUtils::StatusCodeString(StatusCode code)
{
switch(code)
{
case StatusCode::Continue:
return "Continue";
case StatusCode::SwitchingProtocols:
return "Switching Protocols";
case StatusCode::Processing:
return "Processing";
case StatusCode::EarlyHints:
return "Early Hints";
case StatusCode::OK:
return "OK";
case StatusCode::Created:
return "Created";
case StatusCode::Accepted:
return "Accepted";
case StatusCode::NonAuthoritativeInformation:
return "Non-Authoritative Information";
case StatusCode::NoContent:
return "No Content";
case StatusCode::ResetContent:
return "Reset Content";
case StatusCode::PartialContent:
return "PartialContent";
case StatusCode::MultiStatus:
return "Multi-Status";
case StatusCode::AlreadyReported:
return "Already Reported";
case StatusCode::IMUsed:
return "IM Used";
case StatusCode::MultipleChoices:
return "Multiple Choices";
case StatusCode::MovedPermanently:
return "Moved Permanently";
case StatusCode::Found:
return "Found";
case StatusCode::SeeOther:
return "See Other";
case StatusCode::NotModified:
return "Not Modified";
case StatusCode::UseProxy:
return "Use Proxy";
case StatusCode::TemporaryRedirect:
return "Temporary Redirect";
case StatusCode::PermanentRedirect:
return "Permanent Redirect";
case StatusCode::BadRequest:
return "Bad Request";
case StatusCode::Unauthorized:
return "Unauthorized";
case StatusCode::PaymentRequired:
return "Payment Required";
case StatusCode::Forbidden:
return "Forbidden";
case StatusCode::NotFound:
return "Not Found";
case StatusCode::MethodNotAllowed:
return "Method Not Allowed";
case StatusCode::NotAcceptable:
return "Not Acceptable";
case StatusCode::ProxyAuthenticationRequired:
return "Proxy Authentication Required";
case StatusCode::RequestTimeout:
return "Request Timeout";
case StatusCode::Conflict:
return "Conflict";
case StatusCode::Gone:
return "Gone";
case StatusCode::LengthRequired:
return "Length Required";
case StatusCode::PreconditionFailed:
return "Precondition Failed";
case StatusCode::PayloadTooLarge:
return "Payload Too Large";
case StatusCode::URITooLong:
return "URI Too Long";
case StatusCode::UnsupportedMediaType:
return "Unsupported Media Type";
case StatusCode::RangeNotSatisfiable:
return "Range Not Satisfiable";
case StatusCode::ExpectationFailed:
return "Expectation Failed";
case StatusCode::ImATeapot:
return "I'm a teapot";
case StatusCode::MisdirectedRequest:
return "Misdirected Request";
case StatusCode::UnprocessableContent:
return "Unprocessable Content";
case StatusCode::Locked:
return "Locked";
case StatusCode::FailedDependency:
return "Failed Dependency";
case StatusCode::TooEarly:
return "Too Early";
case StatusCode::UpgradeRequired:
return "Upgrade Required";
case StatusCode::PreconditionRequired:
return "Precondition Required";
case StatusCode::TooManyRequests:
return "Too Many Requests";
case StatusCode::RequestHeaderFieldsTooLarge:
return "Request Header Fields Too Large";
case StatusCode::UnavailableForLegalReasons:
return "Unavailable For Legal Reasons";
case StatusCode::InternalServerError:
return "Internal Server Error";
case StatusCode::NotImplemented:
return "Not Implemented";
case StatusCode::ServiceUnavailable:
return "Service Unavailable";
case StatusCode::GatewayTimeout:
return "Gateway Timeout";
case StatusCode::HTTPVersionNotSupported:
return "HTTP Version Not Supported";
case StatusCode::VariantAlsoNegotiates:
return "Variant Also Negotiates";
case StatusCode::InsufficientStorage:
return "Insufficient Storage";
case StatusCode::LoopDetected:
return "Loop Detected";
case StatusCode::NotExtended:
return "Not Extended";
case StatusCode::NetworkAuthenticationRequired:
return "Network Authentication Required";
default:
return "";
}
}
void HttpDictionary::Clear()
{
kvp.clear();
}
void HttpDictionary::Clear(std::string key, bool kvpExistsAfter)
{
if(kvpExistsAfter)
{
kvp[key].clear();
}
else
{
if(kvp.count(key) == 0) return;
kvp[key].clear();
kvp.erase(key);
}
}
void HttpDictionary::SetValue(std::string key, std::string value)
{
kvp[key] = {value};
}
void HttpDictionary::SetValue(std::string key, std::vector<std::string> value)
{
kvp[key] = value;
}
void HttpDictionary::AddValue(std::string key, std::string value)
{
kvp[key].push_back(value);
}
void HttpDictionary::AddValue(std::string key, std::vector<std::string> value)
{
auto& ls = kvp[key];
ls.insert(ls.end(), value.begin(), value.end());
}
bool HttpDictionary::TryGetFirst(std::string key, std::string& value)
{
if(kvp.count(key) == 0) return false;
auto& ls = kvp[key];
if(ls.empty()) return false;
value = ls.front();
return true;
}
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;
}
bool HttpDictionary::TryGetFirstDouble(std::string key, double& value)
{
std::string val;
if(!TryGetFirst(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::GetFirstBoolean(std::string key)
{
std::string val;
if(!TryGetFirst(key,val)) return false;
return val == "true" || val == "on";
}
}