From 2861fba6f2959ab896f5db921e32d226ea9ae183 Mon Sep 17 00:00:00 2001 From: Mike Nolan Date: Thu, 4 Dec 2025 13:24:23 -0600 Subject: [PATCH] Add both Basic auth and CGI support --- CMakeLists.txt | 2 + apps/tfileserver.cpp | 61 +++++++- .../TessesFramework/Http/BasicAuthServer.hpp | 18 +++ include/TessesFramework/Http/CGIServer.hpp | 23 +++ include/TessesFramework/Http/HttpServer.hpp | 3 +- include/TessesFramework/TessesFramework.hpp | 2 + src/Http/BasicAuthServer.cpp | 47 ++++++ src/Http/CGIServer.cpp | 135 ++++++++++++++++++ src/Http/FileServer.cpp | 3 +- src/Http/HttpServer.cpp | 19 ++- src/Http/HttpStream.cpp | 2 + src/Http/MountableServer.cpp | 2 +- src/Platform/Process.cpp | 3 +- src/Streams/NetworkStream.cpp | 9 +- src/Streams/Stream.cpp | 4 +- 15 files changed, 315 insertions(+), 18 deletions(-) create mode 100644 include/TessesFramework/Http/BasicAuthServer.hpp create mode 100644 include/TessesFramework/Http/CGIServer.hpp create mode 100644 src/Http/BasicAuthServer.cpp create mode 100644 src/Http/CGIServer.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b3e037c..1a356fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/apps/tfileserver.cpp b/apps/tfileserver.cpp index b247bf9..1116d67 100644 --- a/apps/tfileserver.cpp +++ b/apps/tfileserver.cpp @@ -9,7 +9,7 @@ using namespace Tesses::Framework::Threading; void print_help(const char* name) { printf("Tesses FileServer\nUSAGE: %s [OPTIONS] \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 cgi_bin; + std::optional cgi_admin; + std::optional 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 http; + auto fs = std::make_shared(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(fs); + auto cgi = std::make_shared(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; diff --git a/include/TessesFramework/Http/BasicAuthServer.hpp b/include/TessesFramework/Http/BasicAuthServer.hpp new file mode 100644 index 0000000..ed475c9 --- /dev/null +++ b/include/TessesFramework/Http/BasicAuthServer.hpp @@ -0,0 +1,18 @@ +#pragma once +#include "HttpServer.hpp" +namespace Tesses::Framework::Http { + class BasicAuthServer : public Tesses::Framework::Http::IHttpServer + { + public: + std::shared_ptr server; + std::function authorization; + std::string realm; + + BasicAuthServer(); + BasicAuthServer(std::shared_ptr server, std::function auth,std::string realm="Protected Content"); + bool Handle(ServerContext& ctx); + + + static bool GetCreds(ServerContext& ctx, std::string& user, std::string& pass); + }; +} \ No newline at end of file diff --git a/include/TessesFramework/Http/CGIServer.hpp b/include/TessesFramework/Http/CGIServer.hpp new file mode 100644 index 0000000..18e4b9d --- /dev/null +++ b/include/TessesFramework/Http/CGIServer.hpp @@ -0,0 +1,23 @@ +#pragma once +#include "../Filesystem/VFS.hpp" +#include "../Filesystem/VFSFix.hpp" +#include "HttpServer.hpp" +#include +namespace Tesses::Framework::Http { + struct CGIParams { + std::optional document_root; + Tesses::Framework::Filesystem::VFSPath program; + std::optional adminEmail; + std::optional workingDirectory; + }; + class CGIServer : public Tesses::Framework::Http::IHttpServer { + Tesses::Framework::Filesystem::VFSPath dir; + public: + std::optional document_root; + std::optional adminEmail; + std::optional workingDirectory; + CGIServer(Tesses::Framework::Filesystem::VFSPath dir); + bool Handle(ServerContext& ctx); + static bool ServeCGIRequest(ServerContext& ctx, CGIParams& params); + }; +} \ No newline at end of file diff --git a/include/TessesFramework/Http/HttpServer.hpp b/include/TessesFramework/Http/HttpServer.hpp index c2d75db..c85ed43 100644 --- a/include/TessesFramework/Http/HttpServer.hpp +++ b/include/TessesFramework/Http/HttpServer.hpp @@ -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 strm); @@ -99,7 +100,7 @@ namespace Tesses::Framework::Http HttpServer(std::string unixPath, std::shared_ptr http); uint16_t GetPort(); void StartAccepting(); - static void Process(std::shared_ptr strm, std::shared_ptr server, std::string ip, uint16_t port, bool encrypted); + static void Process(std::shared_ptr strm, std::shared_ptr server, std::string ip, uint16_t port,uint16_t serverPort, bool encrypted); ~HttpServer(); }; } \ No newline at end of file diff --git a/include/TessesFramework/TessesFramework.hpp b/include/TessesFramework/TessesFramework.hpp index 07dbaf7..0a6a0de 100644 --- a/include/TessesFramework/TessesFramework.hpp +++ b/include/TessesFramework/TessesFramework.hpp @@ -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" diff --git a/src/Http/BasicAuthServer.cpp b/src/Http/BasicAuthServer.cpp new file mode 100644 index 0000000..166f807 --- /dev/null +++ b/src/Http/BasicAuthServer.cpp @@ -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 server, std::function 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; + } +} \ No newline at end of file diff --git a/src/Http/CGIServer.cpp b/src/Http/CGIServer.cpp new file mode 100644 index 0000000..8a6fff6 --- /dev/null +++ b/src/Http/CGIServer.cpp @@ -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 +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; + } +} \ No newline at end of file diff --git a/src/Http/FileServer.cpp b/src/Http/FileServer.cpp index 0bcd407..ad94f7e 100644 --- a/src/Http/FileServer.cpp +++ b/src/Http/FileServer.cpp @@ -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)) { diff --git a/src/Http/HttpServer.cpp b/src/Http/HttpServer.cpp index ed3c03d..5481ea0 100644 --- a/src/Http/HttpServer.cpp +++ b/src/Http/HttpServer.cpp @@ -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 strm, std::shared_ptr server, std::string ip, uint16_t port, bool encrypted) + void HttpServer::Process(std::shared_ptr strm, std::shared_ptr 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(); } + + } + diff --git a/src/Http/HttpStream.cpp b/src/Http/HttpStream.cpp index dcaf289..f4f65c8 100644 --- a/src/Http/HttpStream.cpp +++ b/src/Http/HttpStream.cpp @@ -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; diff --git a/src/Http/MountableServer.cpp b/src/Http/MountableServer.cpp index 1d26c65..331d43c 100644 --- a/src/Http/MountableServer.cpp +++ b/src/Http/MountableServer.cpp @@ -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() diff --git a/src/Platform/Process.cpp b/src/Platform/Process.cpp index 0476622..c2fc1ec 100644 --- a/src/Platform/Process.cpp +++ b/src/Platform/Process.cpp @@ -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;} diff --git a/src/Streams/NetworkStream.cpp b/src/Streams/NetworkStream.cpp index 87ba692..c41cc5f 100644 --- a/src/Streams/NetworkStream.cpp +++ b/src/Streams/NetworkStream.cpp @@ -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(ifa->ifa_name, StringifyIP(ifa->ifa_addr))); diff --git a/src/Streams/Stream.cpp b/src/Streams/Stream.cpp index e7540d2..2194ba9 100644 --- a/src/Streams/Stream.cpp +++ b/src/Streams/Stream.cpp @@ -1,4 +1,5 @@ #include "TessesFramework/Streams/Stream.hpp" +#include 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);