Add both Basic auth and CGI support

This commit is contained in:
2025-12-04 13:24:23 -06:00
parent abe444d22b
commit 2861fba6f2
15 changed files with 315 additions and 18 deletions

View File

@@ -16,6 +16,8 @@ src/Http/HttpStream.cpp
src/Http/ContentDisposition.cpp
src/Http/WebSocket.cpp
src/Http/ChangeableServer.cpp
src/Http/BasicAuthServer.cpp
src/Http/CGIServer.cpp
src/Mail/Smtp.cpp
src/Serialization/Json.cpp
src/Serialization/SQLite.cpp

View File

@@ -9,7 +9,7 @@ using namespace Tesses::Framework::Threading;
void print_help(const char* name)
{
printf("Tesses FileServer\nUSAGE: %s [OPTIONS] <dir>\n",name);
printf("OPTIONS:\n-p PORT, --port PORT: Change port from 9852\n-l, --listing: Enable listing\n-s, --spa: Enable SPA mode (send \"/\" body instead of not found)\n-h, --help: This Screen\n");
printf("OPTIONS:\n-p PORT, --port PORT: Change port from 9852\n-l, --listing: Enable listing\n-s, --spa: Enable SPA mode (send \"/\" body instead of not found)\n-c, --cgi-bin: Enable cgi (common gateway interface) support (specify a folder like /cgi-bin)\n-a, --cgi-admin: cgi admin email\n-w, --cgi-working: working directory for cgi scripts\n-h, --help: This Screen\n");
exit(1);
}
int main(int argc, char** argv)
@@ -21,6 +21,9 @@ int main(int argc, char** argv)
const char* directory = "wwwroot";
bool spa=false;
bool allowListing = false;
std::optional<std::string> cgi_bin;
std::optional<std::string> cgi_admin;
std::optional<Tesses::Framework::Filesystem::VFSPath> cgi_workdir;
uint16_t port = 9852L;
for(int i = 1; i < argc; i++)
@@ -29,6 +32,41 @@ int main(int argc, char** argv)
{
print_help(argv[0]);
}
else if(strcmp(argv[i], "-c") == 0 || strcmp(argv[i],"--cgi-bin") == 0)
{
if(i+1>=argc)
{
printf("ERROR: Not enough arguments for cgi-bin\n");
print_help(argv[0]);
}
else {
printf("CGI is enabled\n");
cgi_bin = argv[++i];
}
}
else if(strcmp(argv[i], "-a") == 0 || strcmp(argv[i],"--cgi-admin") == 0)
{
if(i+1>=argc)
{
printf("ERROR: Not enough arguments for cgi-admin\n");
print_help(argv[0]);
}
else {
cgi_admin = argv[++i];
}
}
else if(strcmp(argv[i], "-w") == 0 || strcmp(argv[i],"--cgi-working") == 0)
{
if(i+1>=argc)
{
printf("ERROR: Not enough arguments for cgi-working\n");
print_help(argv[0]);
}
else {
cgi_workdir = (Tesses::Framework::Filesystem::VFSPath)argv[++i];
}
}
else if(strcmp(argv[i], "-l") == 0 || strcmp(argv[i], "--listing") == 0)
{
allowListing = true;
@@ -57,10 +95,25 @@ int main(int argc, char** argv)
}
std::cout << "In folder: " << std::filesystem::absolute(directory).string() << std::endl;
std::shared_ptr<IHttpServer> http;
auto fs = std::make_shared<FileServer>(directory,allowListing, spa);
HttpServer server(port,fs);
if(cgi_bin)
{
Tesses::Framework::Filesystem::VFSPath dir = *cgi_bin;
dir.relative = true;
auto svr = std::make_shared<MountableServer>(fs);
auto cgi = std::make_shared<CGIServer>(std::filesystem::absolute(directory).string() / dir);
cgi->adminEmail = cgi_admin;
cgi->workingDirectory = cgi_workdir;
svr->Mount(*cgi_bin, cgi);
http = svr;
}
else {
http = fs;
}
HttpServer server(port,http);
server.StartAccepting();
TF_RunEventLoop();
std::cout << "Closing server" << std::endl;

View File

@@ -0,0 +1,18 @@
#pragma once
#include "HttpServer.hpp"
namespace Tesses::Framework::Http {
class BasicAuthServer : public Tesses::Framework::Http::IHttpServer
{
public:
std::shared_ptr<IHttpServer> server;
std::function<bool(std::string username, std::string password)> authorization;
std::string realm;
BasicAuthServer();
BasicAuthServer(std::shared_ptr<IHttpServer> server, std::function<bool(std::string username, std::string password)> auth,std::string realm="Protected Content");
bool Handle(ServerContext& ctx);
static bool GetCreds(ServerContext& ctx, std::string& user, std::string& pass);
};
}

View File

@@ -0,0 +1,23 @@
#pragma once
#include "../Filesystem/VFS.hpp"
#include "../Filesystem/VFSFix.hpp"
#include "HttpServer.hpp"
#include <optional>
namespace Tesses::Framework::Http {
struct CGIParams {
std::optional<Tesses::Framework::Filesystem::VFSPath> document_root;
Tesses::Framework::Filesystem::VFSPath program;
std::optional<std::string> adminEmail;
std::optional<Tesses::Framework::Filesystem::VFSPath> workingDirectory;
};
class CGIServer : public Tesses::Framework::Http::IHttpServer {
Tesses::Framework::Filesystem::VFSPath dir;
public:
std::optional<Tesses::Framework::Filesystem::VFSPath> document_root;
std::optional<std::string> adminEmail;
std::optional<Tesses::Framework::Filesystem::VFSPath> workingDirectory;
CGIServer(Tesses::Framework::Filesystem::VFSPath dir);
bool Handle(ServerContext& ctx);
static bool ServeCGIRequest(ServerContext& ctx, CGIParams& params);
};
}

View File

@@ -29,6 +29,7 @@ namespace Tesses::Framework::Http
StatusCode statusCode;
std::string ip;
uint16_t port;
uint16_t serverPort;
std::string version;
bool encrypted;
ServerContext(std::shared_ptr<Tesses::Framework::Streams::Stream> strm);
@@ -99,7 +100,7 @@ namespace Tesses::Framework::Http
HttpServer(std::string unixPath, std::shared_ptr<IHttpServer> http);
uint16_t GetPort();
void StartAccepting();
static void Process(std::shared_ptr<Tesses::Framework::Streams::Stream> strm, std::shared_ptr<IHttpServer> server, std::string ip, uint16_t port, bool encrypted);
static void Process(std::shared_ptr<Tesses::Framework::Streams::Stream> strm, std::shared_ptr<IHttpServer> server, std::string ip, uint16_t port,uint16_t serverPort, bool encrypted);
~HttpServer();
};
}

View File

@@ -7,6 +7,8 @@
#include "Http/MountableServer.hpp"
#include "Http/ContentDisposition.hpp"
#include "Http/ChangeableServer.hpp"
#include "Http/CGIServer.hpp"
#include "Http/BasicAuthServer.hpp"
#include "Streams/FileStream.hpp"
#include "Streams/MemoryStream.hpp"
#include "Streams/NetworkStream.hpp"

View File

@@ -0,0 +1,47 @@
#include "TessesFramework/Http/BasicAuthServer.hpp"
#include "TessesFramework/Crypto/Crypto.hpp"
namespace Tesses::Framework::Http {
BasicAuthServer::BasicAuthServer()
{
}
BasicAuthServer::BasicAuthServer(std::shared_ptr<IHttpServer> server, std::function<bool(std::string username, std::string password)> auth,std::string realm) : server(server), authorization(auth), realm(realm)
{
}
bool BasicAuthServer::Handle(ServerContext& ctx)
{
std::string www_authenticate = "Basic realm=\"" + this->realm + "\", charset=\"UTF-8\"";
std::string user;
std::string pass;
if(!GetCreds(ctx,user,pass) || !this->authorization(user,pass)) {
ctx.responseHeaders.SetValue("WWW-Authenticate",www_authenticate);
ctx.statusCode = Unauthorized;
return false;
}
if(this->server)
return this->server->Handle(ctx);
ctx.statusCode = InternalServerError;
return false;
}
bool BasicAuthServer::GetCreds(ServerContext& ctx, std::string& user, std::string& pass)
{
std::string auth;
if(!ctx.requestHeaders.TryGetFirst("Authorization", auth)) return false;
auto security = HttpUtils::SplitString(auth," ",2);
if(security.size() < 2) return false;
if(security[0] != "Basic") return false;
auto decoded = Crypto::Base64_Decode(security[0]);
std::string decoded_str(decoded.begin(),decoded.end());
security = HttpUtils::SplitString(auth,":",2);
if(security.size() < 2) return false;
user = security[0];
pass = security[1];
return true;
}
}

135
src/Http/CGIServer.cpp Normal file
View File

@@ -0,0 +1,135 @@
#include "TessesFramework/Http/CGIServer.hpp"
#include "TessesFramework/Filesystem/LocalFS.hpp"
#include "TessesFramework/Platform/Process.hpp"
#include "TessesFramework/Http/BasicAuthServer.hpp"
#include "TessesFramework/TextStreams/StreamReader.hpp"
#include <iostream>
namespace Tesses::Framework::Http {
CGIServer::CGIServer(Tesses::Framework::Filesystem::VFSPath dir)
{
this->dir = dir;
}
bool CGIServer::Handle(ServerContext& ctx)
{
Tesses::Framework::Filesystem::VFSPath execPath = ctx.path;
execPath.relative=true;
CGIParams params;
params.document_root = this->document_root ? *this->document_root : this->dir;
params.adminEmail = this->adminEmail;
params.workingDirectory = this->workingDirectory;
params.program = this->dir / execPath.CollapseRelativeParents();
return ServeCGIRequest(ctx,params);
}
bool CGIServer::ServeCGIRequest(ServerContext& ctx, CGIParams& params)
{
using namespace Tesses::Framework::Filesystem;
auto program = params.program.MakeAbsolute();
if(!LocalFS->FileExists(program)) return false;
Tesses::Framework::Platform::Process p;
Tesses::Framework::Filesystem::VFSPath p0=ctx.originalPath;
p.env.emplace_back("SCRIPT_FILENAME",LocalFS->VFSPathToSystem(program));
p.env.emplace_back("SCRIPT_NAME",p0.CollapseRelativeParents().ToString());
if(ctx.encrypted)
p.env.emplace_back("HTTPS","on");
std::string query;
for(auto& item : ctx.queryParams.kvp)
{
for(auto& val : item.second)
{
if(!query.empty()) query += "&";
query += HttpUtils::UrlEncode(item.first);
query += "=";
query += HttpUtils::UrlEncode(val);
}
}
p.env.emplace_back("QUERY_STRING",query);
p.env.emplace_back("REQUEST_URI",ctx.GetOriginalPathWithQuery());
p.env.emplace_back("REQUEST_METHOD",ctx.method);
p.env.emplace_back("REMOTE_HOST",ctx.ip);
p.env.emplace_back("REMOTE_ADDR",ctx.ip);
p.env.emplace_back("REMOTE_PORT",std::to_string(ctx.port));
std::string user;
std::string pass;
if(BasicAuthServer::GetCreds(ctx,user,pass))
p.env.emplace_back("REMOTE_USER",user);
p.env.emplace_back("SERVER_SOFTWARE","TessesFrameworkWebServer");
p.env.emplace_back("SERVER_PORT",std::to_string(ctx.serverPort));
p.env.emplace_back("GATEWAY_INTERFACE","CGI/1.1");
p.env.emplace_back("SERVER_PROTOCOL",ctx.version);
if(params.document_root)
p.env.emplace_back("DOCUMENT_ROOT",params.document_root->ToString());
if(params.adminEmail)
p.env.emplace_back("SERVER_ADMIN",*params.adminEmail);
for(auto& hdr : ctx.requestHeaders.kvp)
{
std::string hdr_name = HttpUtils::ToUpper(hdr.first);
if(hdr_name == "CONTENT-LENGTH")
{
if(!hdr.second.empty())
p.env.emplace_back("CONTENT_LENGTH",hdr.second.front());
}
else if(hdr_name == "CONTENT-TYPE")
{
if(!hdr.second.empty())
p.env.emplace_back("CONTENT_LENGTH",hdr.second.front());
}
else {
if(!hdr.second.empty())
p.env.emplace_back("HTTP_"+hdr.first,hdr.second.front());
}
}
p.redirectStdIn=true;
p.redirectStdOut=true;
p.name = program.ToString();
if(params.workingDirectory)
{
p.workingDirectory = params.workingDirectory->MakeAbsolute().ToString();
}
if(p.Start())
{
auto strm = p.GetStdinStream();
if(ctx.method != "GET") ctx.ReadStream(strm);
p.CloseStdInNow();
auto stout = p.GetStdoutStream();
Tesses::Framework::TextStreams::StreamReader reader(stout);
std::string line;
while(reader.ReadLine(line))
{
auto v = HttpUtils::SplitString(line,": ", 2);
if(v.size() == 2)
{
if(HttpUtils::ToLower(v[0]) == "status")
{
auto v2 = HttpUtils::SplitString(v[1]," ",2);
if(v2.empty())
{
ctx.statusCode = StatusCode::InternalServerError;
throw std::runtime_error("Status response is empty");
}
ctx.statusCode= (StatusCode)std::stoi(v2[0]);
}
else {
ctx.responseHeaders.AddValue(v[0],v[1]);
}
}
else throw std::runtime_error("Corrupted header: " + line);
line.clear();
}
ctx.SendStream(stout);
return true;
}
return false;
}
}

View File

@@ -51,7 +51,8 @@ namespace Tesses::Framework::Http
bool FileServer::Handle(ServerContext& ctx)
{
auto path = HttpUtils::UrlPathDecode(ctx.path);
auto path = ((VFSPath)HttpUtils::UrlPathDecode(ctx.path)).CollapseRelativeParents();
if(this->vfs->DirectoryExists(path))
{

View File

@@ -515,8 +515,7 @@ namespace Tesses::Framework::Http
{
if(ct.find("multipart/form-data") != 0)
{
std::cout << "Not form data" << std::endl;
return;
throw std::runtime_error("Not form data");
}
auto res = ct.find("boundary=");
if(res == std::string::npos) return;
@@ -581,9 +580,10 @@ namespace Tesses::Framework::Http
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");
thrd = new Threading::Thread([svr,http]()->void {
thrd = new Threading::Thread([svr,http,serverPort]()->void {
while(TF_IsRunning())
{
TF_LOG("after TF_IsRunning");
@@ -599,9 +599,9 @@ namespace Tesses::Framework::Http
return;
}
TF_LOG("Before entering socket thread");
Threading::Thread thrd2([sock,http,ip,port]()->void {
Threading::Thread thrd2([sock,http,ip,port,serverPort]()->void {
TF_LOG("In thread to process");
HttpServer::Process(sock,http,ip,port,false);
HttpServer::Process(sock,http,ip,port,serverPort,false);
TF_LOG("In thread after process");
});
@@ -829,7 +829,8 @@ namespace Tesses::Framework::Http
else
{
auto chunkedStream = this->OpenResponseStream();
this->strm->CopyTo(chunkedStream);
strm->CopyTo(chunkedStream);
}
@@ -928,7 +929,7 @@ namespace Tesses::Framework::Http
return *this;
}
void HttpServer::Process(std::shared_ptr<Stream> strm, std::shared_ptr<IHttpServer> server, std::string ip, uint16_t port, bool encrypted)
void HttpServer::Process(std::shared_ptr<Stream> strm, std::shared_ptr<IHttpServer> server, std::string ip, uint16_t port,uint16_t serverPort, bool encrypted)
{
TF_LOG("In process");
while(true)
@@ -939,6 +940,7 @@ namespace Tesses::Framework::Http
ctx.ip = ip;
ctx.port = port;
ctx.encrypted = encrypted;
ctx.serverPort = serverPort;
try{
bool firstLine = true;
std::string line;
@@ -1093,4 +1095,7 @@ namespace Tesses::Framework::Http
path2 = path2 / path;
return path2.CollapseRelativeParents().ToString();
}
}

View File

@@ -124,6 +124,7 @@ namespace Tesses::Framework::Http
{
len = std::min((size_t)(this->length - this->position), len);
if(len > 0)
len = this->strm->Write(buff,len);
this->position += len;
@@ -131,6 +132,7 @@ namespace Tesses::Framework::Http
}
else
{
if(len == 0) return 0;
if(this->http1_1)
{
std::stringstream strm;

View File

@@ -65,7 +65,7 @@ bool MountableServer::Handle(ServerContext& ctx)
}
}
ctx.path=oldPath;
if(this->root != nullptr && this->root->Handle(ctx)) return true;
if(this->root && this->root->Handle(ctx)) return true;
return false;
}
MountableServer::~MountableServer()

View File

@@ -180,8 +180,9 @@ namespace Tesses::Framework::Platform {
#elif defined(GEKKO) || defined(__PS2__) || defined(__SWITCH__) || !defined(TESSESFRAMEWORK_ENABLE_PROCESS)
return 0;
#else
if(this->strm < 0 || this->eos && writing) return 0;
if(this->strm < 0 || this->eos && writing) return 0;
auto r = read(this->strm,buff,sz);
if(r == -1) return 0;
if(r == 0 && sz != 0) { this->eos=true; return 0;}

View File

@@ -129,9 +129,16 @@ namespace Tesses::Framework::Streams {
#else
struct ifaddrs *ifAddrStruct = NULL;
getifaddrs(&ifAddrStruct);
errno = 0;
if(getifaddrs(&ifAddrStruct) == -1)
{
freeifaddrs(ifAddrStruct);
return {};
}
for (struct ifaddrs *ifa = ifAddrStruct; ifa != NULL; ifa = ifa->ifa_next) {
if (ifa->ifa_addr == NULL)
continue;
if (ifa->ifa_addr->sa_family == AF_INET) { // IPv4
ipConfig.push_back(std::pair<std::string,std::string>(ifa->ifa_name, StringifyIP(ifa->ifa_addr)));

View File

@@ -1,4 +1,5 @@
#include "TessesFramework/Streams/Stream.hpp"
#include <iostream>
namespace Tesses::Framework::Streams {
int32_t Stream::ReadByte()
@@ -108,6 +109,7 @@ namespace Tesses::Framework::Streams {
read = (size_t)std::min(len-offset,(uint64_t)buffSize);
read = this->Read(buffer,read);
strm->WriteBlock(buffer, read);
offset += read;
@@ -122,8 +124,6 @@ namespace Tesses::Framework::Streams {
{
size_t read;
uint8_t* buffer = new uint8_t[buffSize];
do {
read = this->Read(buffer,buffSize);
strm->WriteBlock(buffer, read);