From 848fca7f36e6b7f5c0750fa14b6a47d63d14cc35 Mon Sep 17 00:00:00 2001 From: Mike Nolan Date: Mon, 12 Jan 2026 12:25:06 -0600 Subject: [PATCH] Add route server --- CMakeLists.txt | 1 + examples/webserverex.cpp | 24 +++++ include/TessesFramework/Filesystem/VFS.hpp | 3 +- include/TessesFramework/Http/HttpServer.hpp | 2 + include/TessesFramework/Http/HttpStream.hpp | 1 + include/TessesFramework/Http/RouteServer.hpp | 41 +++++++++ include/TessesFramework/TessesFramework.hpp | 1 + src/Filesystem/VFS.cpp | 32 +++++++ src/Http/HttpServer.cpp | 41 ++++++--- src/Http/HttpStream.cpp | 15 ++- src/Http/RouteServer.cpp | 97 ++++++++++++++++++++ src/Streams/Stream.cpp | 8 +- 12 files changed, 247 insertions(+), 19 deletions(-) create mode 100644 include/TessesFramework/Http/RouteServer.hpp create mode 100644 src/Http/RouteServer.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ef1ee6b..933161f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ src/Random.cpp src/Date/Date.cpp src/Http/FileServer.cpp src/Http/MountableServer.cpp +src/Http/RouteServer.cpp src/Http/CallbackServer.cpp src/Http/HttpServer.cpp src/Http/HttpUtils.cpp diff --git a/examples/webserverex.cpp b/examples/webserverex.cpp index 840268c..c79b342 100644 --- a/examples/webserverex.cpp +++ b/examples/webserverex.cpp @@ -139,11 +139,35 @@ class MyOtherWebServer : public IHttpServer int main(int argc, char** argv) { TF_InitWithConsole(); + std::shared_ptr routeSvr = std::make_shared(); + routeSvr->Get("/name/{name}/greeting",[](ServerContext& ctx)->bool{ + std::string name; + if(ctx.pathArguments.TryGetFirst("name",name)) + { + ctx.WithMimeType("text/plain").SendText("Hello " + name); + } + else { + ctx.WithMimeType("text/plain").SendText("Please provide a name"); + } + return true; + }); + routeSvr->Get("/name/{name}/length",[](ServerContext& ctx)->bool{ + std::string name; + if(ctx.pathArguments.TryGetFirst("name",name)) + { + ctx.WithMimeType("text/plain").SendText("The length of the name is " + std::to_string(name.size())); + } + else { + ctx.WithMimeType("text/plain").SendText("Please provide a name"); + } + return true; + }); std::shared_ptr myo = std::make_shared(); std::shared_ptr mws = std::make_shared(); std::shared_ptr mountable = std::make_shared(myo); mountable->Mount("/mymount/",mws); + mountable->Mount("/routeSvr/", routeSvr); HttpServer server(10001,mountable); server.StartAccepting(); TF_RunEventLoop(); diff --git a/include/TessesFramework/Filesystem/VFS.hpp b/include/TessesFramework/Filesystem/VFS.hpp index 6793e1e..908189b 100644 --- a/include/TessesFramework/Filesystem/VFS.hpp +++ b/include/TessesFramework/Filesystem/VFS.hpp @@ -35,7 +35,8 @@ namespace Tesses::Framework::Filesystem VFSPath(VFSPath p, std::string subent); VFSPath(VFSPath p, VFSPath p2); - + //does not check for ? + static VFSPath ParseUriPath(std::string path); VFSPath GetParent() const; VFSPath CollapseRelativeParents() const; diff --git a/include/TessesFramework/Http/HttpServer.hpp b/include/TessesFramework/Http/HttpServer.hpp index c85ed43..6b9f14e 100644 --- a/include/TessesFramework/Http/HttpServer.hpp +++ b/include/TessesFramework/Http/HttpServer.hpp @@ -23,6 +23,8 @@ namespace Tesses::Framework::Http HttpDictionary requestHeaders; HttpDictionary responseHeaders; HttpDictionary queryParams; + //used by path + HttpDictionary pathArguments; std::string path; std::string originalPath; std::string method; diff --git a/include/TessesFramework/Http/HttpStream.hpp b/include/TessesFramework/Http/HttpStream.hpp index 34267ab..4388802 100644 --- a/include/TessesFramework/Http/HttpStream.hpp +++ b/include/TessesFramework/Http/HttpStream.hpp @@ -25,6 +25,7 @@ namespace Tesses::Framework::Http int64_t GetPosition(); size_t Read(uint8_t* buffer, size_t len); size_t Write(const uint8_t* buffer, size_t len); + void Close(); ~HttpStream(); }; } \ No newline at end of file diff --git a/include/TessesFramework/Http/RouteServer.hpp b/include/TessesFramework/Http/RouteServer.hpp new file mode 100644 index 0000000..c1f2925 --- /dev/null +++ b/include/TessesFramework/Http/RouteServer.hpp @@ -0,0 +1,41 @@ +#pragma once +#include "HttpServer.hpp" +#include "../Filesystem/VFSFix.hpp" +#include "../Filesystem/VFS.hpp" + +namespace Tesses::Framework::Http +{ + using ServerRequestHandler = std::function; + + + class RouteServer : public IHttpServer + { + class RouteServerRoute { + public: + std::vector> parts; + std::string method; + ServerRequestHandler handler; + + RouteServerRoute() = default; + RouteServerRoute(std::string route, std::string method, ServerRequestHandler handler); + bool Equals(Tesses::Framework::Filesystem::VFSPath& path, HttpDictionary& args); + }; + std::vector routes; + std::shared_ptr root; + public: + RouteServer() = default; + RouteServer(std::shared_ptr root); + void Get(std::string pattern, ServerRequestHandler handler); + void Post(std::string pattern, ServerRequestHandler handler); + void Put(std::string pattern, ServerRequestHandler handler); + void Patch(std::string pattern, ServerRequestHandler handler); + + void Delete(std::string pattern, ServerRequestHandler handler); + + void Trace(std::string pattern, ServerRequestHandler handler); + void Options(std::string pattern, ServerRequestHandler handler); + void Add(std::string method, std::string pattern, ServerRequestHandler handler); + bool Handle(ServerContext& ctx); + + }; +} \ No newline at end of file diff --git a/include/TessesFramework/TessesFramework.hpp b/include/TessesFramework/TessesFramework.hpp index d6f624e..d796abf 100644 --- a/include/TessesFramework/TessesFramework.hpp +++ b/include/TessesFramework/TessesFramework.hpp @@ -9,6 +9,7 @@ #include "Http/ChangeableServer.hpp" #include "Http/CGIServer.hpp" #include "Http/BasicAuthServer.hpp" +#include "Http/RouteServer.hpp" #include "Streams/FileStream.hpp" #include "Streams/MemoryStream.hpp" #include "Streams/NetworkStream.hpp" diff --git a/src/Filesystem/VFS.cpp b/src/Filesystem/VFS.cpp index 593854e..21afce9 100644 --- a/src/Filesystem/VFS.cpp +++ b/src/Filesystem/VFS.cpp @@ -330,6 +330,35 @@ namespace Tesses::Framework::Filesystem this->path = p; } + VFSPath VFSPath::ParseUriPath(std::string path) + { + std::string builder = {}; + VFSPath vpath; + vpath.relative=true; + + if(!path.empty() && path[0] == '/') vpath.relative=false; + + for(auto item : path) + { + if(item == '/') + { + if(!builder.empty()) + { + vpath.path.push_back(builder); + builder.clear(); + } + } + else { + builder.push_back(item); + } + } + if(!builder.empty()) + { + vpath.path.push_back(builder); + } + return vpath; + } + bool VFSPath::HasExtension() const { if(this->path.empty()) return false; @@ -377,6 +406,9 @@ namespace Tesses::Framework::Filesystem if(!str.empty()) { if(str.front() == '/') this->relative=false; + #if defined(_WIN32) + if(str.front() == '\\') this->relative=false; + #endif if(!this->path.empty()) { auto firstPartPath = this->path.front(); diff --git a/src/Http/HttpServer.cpp b/src/Http/HttpServer.cpp index c5dd53b..da6ee49 100644 --- a/src/Http/HttpServer.cpp +++ b/src/Http/HttpServer.cpp @@ -566,7 +566,12 @@ namespace Tesses::Framework::Http this->responseHeaders.SetValue("Transfer-Encoding","chunked"); this->WriteHeaders(); - return std::make_shared(this->strm,length,false,version == "HTTP/1.1"); + auto strm = std::make_shared(this->strm,length,false,version == "HTTP/1.1"); + if(method == "HEAD") + { + strm->Close(); + } + return strm; } std::shared_ptr ServerContext::OpenRequestStream() { @@ -788,23 +793,26 @@ namespace Tesses::Framework::Http 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); + if(this->method != "HEAD") + { + strm->Seek(begin,SeekOrigin::Begin); - uint8_t buffer[1024]; + 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()); + 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 @@ -821,6 +829,7 @@ namespace Tesses::Framework::Http this->WithSingleHeader("Accept-Range","bytes"); this->WithSingleHeader("Content-Length",std::to_string(len)); this->WriteHeaders(); + if(this->method != "HEAD") strm->CopyTo(this->strm); } } @@ -828,8 +837,10 @@ namespace Tesses::Framework::Http } else { + auto chunkedStream = this->OpenResponseStream(); + if(method != "HEAD") strm->CopyTo(chunkedStream); diff --git a/src/Http/HttpStream.cpp b/src/Http/HttpStream.cpp index f4f65c8..ef3f10c 100644 --- a/src/Http/HttpStream.cpp +++ b/src/Http/HttpStream.cpp @@ -27,6 +27,7 @@ namespace Tesses::Framework::Http } bool HttpStream::CanWrite() { + if(this->done) return false; if(this->recv) return false; return this->strm->CanWrite(); } @@ -118,6 +119,7 @@ namespace Tesses::Framework::Http } size_t HttpStream::Write(const 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) @@ -153,9 +155,20 @@ namespace Tesses::Framework::Http } } } + void HttpStream::Close() + { + if(this->length == -1 && this->http1_1 && !done && !this->recv) + { + this->done=true; + StreamWriter writer(this->strm); + writer.newline = "\r\n"; + writer.WriteLine("0"); + writer.WriteLine(); + } + } HttpStream::~HttpStream() { - if(this->length == -1 && this->http1_1) + if(this->length == -1 && this->http1_1 && !done && !this->recv) { StreamWriter writer(this->strm); writer.newline = "\r\n"; diff --git a/src/Http/RouteServer.cpp b/src/Http/RouteServer.cpp new file mode 100644 index 0000000..cbcec35 --- /dev/null +++ b/src/Http/RouteServer.cpp @@ -0,0 +1,97 @@ +#include "TessesFramework/Http/RouteServer.hpp" + +namespace Tesses::Framework::Http +{ + + RouteServer::RouteServerRoute::RouteServerRoute(std::string route, std::string method, ServerRequestHandler handler) : method(method), handler(handler) + { + auto path = Tesses::Framework::Filesystem::VFSPath::ParseUriPath(route); + for(auto item : path.path) + { + if(item.size() > 2 && item[0] == '{' && item[item.size()-1] == '}') + { + this->parts.emplace_back( item.substr(1,item.size()-2),true); + } + else { + this->parts.emplace_back(item,false); + } + } + } + bool RouteServer::RouteServerRoute::Equals(Tesses::Framework::Filesystem::VFSPath& path, HttpDictionary& args) + { + if(path.path.size() != this->parts.size()) return false; + + + for(size_t i = 0; i < this->parts.size(); i++) + { + auto& part = this->parts[i]; + if(part.second) + args.SetValue(part.first, Tesses::Framework::Http::HttpUtils::UrlPathDecode(path.path[i])); + else if(part.first != path.path[i]) return false; + + } + + + return true; + } + + RouteServer::RouteServer(std::shared_ptr root) : root(root) + { + + } + + void RouteServer::Add(std::string method, std::string pattern, ServerRequestHandler handler) + { + this->routes.emplace_back(pattern,method,handler); + } + + bool RouteServer::Handle(ServerContext& ctx) + { + auto pathArgs = ctx.pathArguments; + auto path = Tesses::Framework::Filesystem::VFSPath::ParseUriPath(ctx.path); + for(auto& svr : this->routes) + { + if(svr.method != ctx.method) continue; + ctx.pathArguments = pathArgs; + if(svr.Equals(path, ctx.pathArguments) && svr.handler && svr.handler(ctx)) + return true; + + } + ctx.pathArguments = pathArgs; + + if(this->root) + return this->root->Handle(ctx); + return false; + } + + void RouteServer::Get(std::string pattern, ServerRequestHandler handler) + { + Add("GET",pattern,handler); + } + void RouteServer::Post(std::string pattern, ServerRequestHandler handler) + { + Add("POST",pattern,handler); + } + void RouteServer::Put(std::string pattern, ServerRequestHandler handler) + { + Add("PUT",pattern,handler); + } + void RouteServer::Patch(std::string pattern, ServerRequestHandler handler) + { + Add("PATCH",pattern,handler); + } + + void RouteServer::Delete(std::string pattern, ServerRequestHandler handler) + { + Add("DELETE",pattern,handler); + } + + void RouteServer::Trace(std::string pattern, ServerRequestHandler handler) + { + Add("TRACE",pattern,handler); + } + void RouteServer::Options(std::string pattern, ServerRequestHandler handler) + { + Add("OPTIONS",pattern,handler); + } +} \ No newline at end of file diff --git a/src/Streams/Stream.cpp b/src/Streams/Stream.cpp index 2194ba9..3ae4582 100644 --- a/src/Streams/Stream.cpp +++ b/src/Streams/Stream.cpp @@ -48,8 +48,13 @@ namespace Tesses::Framework::Streams { if(len < 1024) read = len; if(read > 0) + { read=this->Write(buffer,read); - + if(read == 0) + { + throw std::out_of_range("Failed to write!"); + } + } buffer += read; @@ -109,7 +114,6 @@ 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;