diff --git a/external/libs b/external/libs index 35461a9..6235794 160000 --- a/external/libs +++ b/external/libs @@ -1 +1 @@ -Subproject commit 35461a9eae5d0e552707aea329ca1c4beccbcb43 +Subproject commit 623579471a0e3f3087173102ee3c9470718ba03e diff --git a/makefile b/makefile index 5d4a931..7f0ff18 100644 --- a/makefile +++ b/makefile @@ -63,14 +63,14 @@ FLAGS = $(CPPFLAGS) -Isrc/ $(CPPSTD) -Iexternal/bin/libs/release else ifeq ($(CONFIG),debug) # debug MAIN_FILE = src/main.cpp BIN_NAME = http-debug -#ADDR_SANITIZER = -fsanitize=address +ADDR_SANITIZER = -fsanitize=address FLAGS = $(CPPFLAGS) -DDEBUG -g -Isrc/ $(ADDR_SANITIZER) $(CPPSTD) -Iexternal/bin/libs/debug ### Test settings else ifeq ($(CONFIG),test) # test MAIN_FILE = testbench/tests.cpp BIN_NAME = http-test -#ADDR_SANITIZER = -fsanitize=address +ADDR_SANITIZER = -fsanitize=address FLAGS = $(CPPFLAGS) -DDEBUG -DTESTING -g -Isrc/ $(ADDR_SANITIZER) $(CPPSTD) -Iexternal/bin/libs/debug LIBRARIES += external/bin/libs/debug/bftest/libbftest-debug.a endif # ($(CONFIG),...) diff --git a/src/log.cpp b/src/log.cpp index 1a3a4de..47da217 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -44,6 +44,7 @@ void _LogWriteEntry(BFFileWriter * filewriter, int mode, ...) { logstr ); +#ifdef ENABLE_LOG_CONSOLE_PRINT printf( format, dt.month, @@ -56,6 +57,7 @@ void _LogWriteEntry(BFFileWriter * filewriter, int mode, ...) { ); printf("\n"); fflush(stdout); +#endif // ENABLE_LOG_CONSOLE_PRINT va_end(arg0); va_end(arg1); diff --git a/src/log.hpp b/src/log.hpp index cb8519c..29a68d3 100644 --- a/src/log.hpp +++ b/src/log.hpp @@ -10,6 +10,8 @@ extern "C" { #include } +//#define ENABLE_LOG_CONSOLE_PRINT + #define CHAT_LOG_PATH "/tmp/http.log" extern BFFileWriter gFileWriter; diff --git a/src/main.cpp b/src/main.cpp index f463a26..3d65080 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -32,7 +32,7 @@ void help(const char * toolname) { } void __NewConnection(Connection * sc) { - LOG_WRITE("new connection made"); + LOG_DEBUG("new connection made"); } int __ReadArguments(int argc, char * argv[]) { diff --git a/src/office.cpp b/src/office.cpp index b756ab0..d6306c8 100644 --- a/src/office.cpp +++ b/src/office.cpp @@ -11,6 +11,7 @@ #include #include #include +#include using namespace BF::Net; using namespace BF; @@ -23,35 +24,50 @@ using namespace std; * once popped from queue */ Atomic> _incomingRequests; -BFThreadAsyncID _tidRequestQueue = NULL; + +/** + * requests will come in asynchronously from clients, we will queue + * each one and have our worker threads pick off the queue + * + * we have n worker threads, working concurrently on every available request + * + * i don't see a performance boost on the client side between 2 and 4 threads + */ +const unsigned char numWorkerThreads = 2; +BFThreadAsyncID _tidRequestQueue[numWorkerThreads]; + BFLock _queueSema; void Office::envelopeReceive(Envelope * envelope) { - _incomingRequests.lock(); - - BFRetain(envelope); - _incomingRequests.unsafeget().push(envelope); + int attempts = 0; + int threshold = 25; + while (_incomingRequests.get([=] (auto & q) { + return q.size(); + }) >= threshold) { + usleep(500); + } - _incomingRequests.unlock(); + _incomingRequests.get([=] (Queue & q) { + BFRetain(envelope); + q.push(envelope); + }); BFLockRelease(&_queueSema); } void __IncomingRequestsWorkerThread(void * in) { - while (!BFThreadAsyncIsCanceled(_tidRequestQueue)) { - if (_incomingRequests.get().empty()) { + const BFThreadAsyncID tid = BFThreadAsyncGetID(); + while (!BFThreadAsyncIsCanceled(tid)) { + _incomingRequests.lock(); + if (_incomingRequests.unsafeget().empty()) { + _incomingRequests.unlock(); BFLockWait(&_queueSema); } else { - _incomingRequests.lock(); - - // get first item from the queue Envelope * envelope = _incomingRequests.unsafeget().front(); - - // pop off _incomingRequests.unsafeget().pop(); - _incomingRequests.unlock(); if (envelope->data()->size() == 0) { + BFRelease(envelope); continue; } @@ -74,15 +90,32 @@ void __IncomingRequestsWorkerThread(void * in) { void Office::start() { BFLockCreate(&_queueSema); - _tidRequestQueue = BFThreadAsync(__IncomingRequestsWorkerThread, NULL); + + for (int i = 0; i < numWorkerThreads; i++) { + _tidRequestQueue[i] = BFThreadAsync(__IncomingRequestsWorkerThread, NULL); + } } void Office::stop() { - BFThreadAsyncCancel(_tidRequestQueue); - BFLockRelease(&_queueSema); - BFThreadAsyncWait(_tidRequestQueue); - BFThreadAsyncDestroy(_tidRequestQueue); + for (int i = 0; i < numWorkerThreads; i++) { + BFLockRelease(&_queueSema); + BFThreadAsyncCancel(_tidRequestQueue[i]); + } + + for (int i = 0; i < numWorkerThreads; i++) { + BFLockRelease(&_queueSema); + BFThreadAsyncWait(_tidRequestQueue[i]); + BFThreadAsyncDestroy(_tidRequestQueue[i]); + } + // flush q + _incomingRequests.lock(); + while (!_incomingRequests.unsafeget().empty()) { + Envelope * e = _incomingRequests.unsafeget().front(); + _incomingRequests.unsafeget().pop(); + BFRelease(e); + } + _incomingRequests.unlock(); BFLockDestroy(&_queueSema); } diff --git a/src/request.cpp b/src/request.cpp index e860628..d1baf54 100644 --- a/src/request.cpp +++ b/src/request.cpp @@ -11,38 +11,110 @@ extern "C" { #include } -#include +#include using namespace BF; Request::Request(const Data * data) : _message(data == NULL ? 0 : (const char *) data->buffer(), data == NULL ? 0 : data->size()) { - LOG_WRITE("Request length = %ld", _message.size()); - LOG_WRITE("Request content = \n%s", _message.c_str()); + LOG_DEBUG("Request length = %ld", _message.size()); + LOG_DEBUG("Request content = \n%s", _message.c_str()); + + this->parse(); +} + +Request::~Request() { } + +void __RequestParseStatusLine(std::string & in, String & method, String & target, String & protocol) { + std::stringstream ss(in); + std::string buf; + char del = ' '; + + if (getline(ss, buf, del)) { + method = buf; + } + + if (getline(ss, buf, del)) { + target = buf; + } + + if (getline(ss, buf, del)) { + protocol = buf; + } +} + +void __RequestParseHeader(std::string & in, HashMap & header) { + std::string del = ": "; + auto pos = in.find(del); + std::string key, value; + + if (pos == std::string::npos) { + return; + } + + key = in.substr(0, pos); + in.erase(0, pos + del.length()); + pos = in.find(del); + + value = in.substr(0, pos); + + header.insert(key, value); } -Request::~Request() { +void Request::parse() { + if (this->_message.empty()) { + return; + } + + std::stringstream ss(this->_message); + std::string buf; + char del = '\n'; + int statusLineParsed = false; + int headerParsed = false; + while (getline(ss, buf, del)) { + buf.erase( + std::remove_if( + buf.begin(), + buf.end(), + [](unsigned char c) { + return c == '\t' || c == '\n' || c == '\r'; + } + ), + buf.end() + ); + + if (!statusLineParsed) { + __RequestParseStatusLine(buf, this->_method, this->_target, this->_protocol); + statusLineParsed = true; + } else if (!headerParsed) { + if (buf.empty()) { + headerParsed = true; + } else { + __RequestParseHeader(buf, this->_header); + } + } else { // read the body + this->_body.append(buf); + } + } } -String Request::method() const { - std::regex methodRegex(R"((GET|POST|PUT|DELETE|HEAD|OPTIONS|CONNECT|TRACE|PATCH)\s+)"); - std::smatch match; +const String & Request::method() const { + return this->_method; +} - if (std::regex_search(this->_message, match, methodRegex)) { - return match[1].str(); // The second capturing group contains the target - } else { - return ""; // Or throw an exception if no target is found - } +const String & Request::target() const { + return this->_target; } -String Request::target() const { - std::regex targetRegex(R"((?:GET|POST|PUT|DELETE|HEAD|OPTIONS|CONNECT|TRACE|PATCH)\s+([^\s]+)\s+HTTP/\d\.\d)"); - std::smatch match; +const String & Request::protocol() const { + return this->_protocol; +} - if (std::regex_search(this->_message, match, targetRegex)) { - return match[1].str(); // The second capturing group contains the target - } else { - return ""; // Or throw an exception if no target is found - } +const HashMap & Request::header() const { + return this->_header; +} + +const String & Request::body() const { + return this->_body; } // component: 0=path, 1=query @@ -121,18 +193,3 @@ HashMap Request::targetQuery() const { return res; } -String Request::protocol() const { - std::regex protocolRegex(R"((?:GET|POST|PUT|DELETE|HEAD|OPTIONS|CONNECT|TRACE|PATCH)\s+[^\s]+\s+(HTTP/\d\.\d))"); - std::smatch match; - - if (std::regex_search(this->_message, match, protocolRegex)) { - return match[1].str(); // The second capturing group contains the protocol - } else { - return ""; // Or throw an exception if no protocol is found - } -} - -String Request::host() const { - return ""; -} - diff --git a/src/request.hpp b/src/request.hpp index 4ae171f..d8617ca 100644 --- a/src/request.hpp +++ b/src/request.hpp @@ -11,6 +11,11 @@ #include #include +#ifdef LINUX +#include +#endif +#include + typedef enum { kRequestMethodNone = 0, kRequestMethodGet, @@ -22,9 +27,12 @@ class Request : public BF::Object { Request(const BF::Data * data); virtual ~Request(); - BF::String method() const; - BF::String target() const; - BF::String protocol() const; + const BF::String & method() const; + const BF::String & target() const; + const BF::String & protocol() const; + + const BF::HashMap & header() const; + const BF::String & body() const; // returns path without query string BF::String targetPath() const; @@ -32,10 +40,19 @@ class Request : public BF::Object { // returns query data BF::HashMap targetQuery() const; - BF::String host() const; - private: + void parse(); + std::string _message; + + // status line + BF::String _method; + BF::String _target; + BF::String _protocol; + + BF::HashMap _header; + + BF::String _body; }; #endif // REQUEST_HPP diff --git a/src/resource.cpp b/src/resource.cpp index a5c6c1f..61e4e9a 100644 --- a/src/resource.cpp +++ b/src/resource.cpp @@ -33,3 +33,8 @@ bool Resource::setRootFolder(const String & rootFolder) { return true; } +bool Resource::targetValid(const URL & target) { + URL rootURL = _rootFolder; + return target.absURL().isSubPath(rootURL.absURL()); +} + diff --git a/src/resource.hpp b/src/resource.hpp index 95d0c8d..3d628d4 100644 --- a/src/resource.hpp +++ b/src/resource.hpp @@ -9,10 +9,12 @@ #define RESOURCE_HPP #include +#include namespace Resource { bool setRootFolder(const BF::String & rootFolder); const BF::String & getRootFolder(); + bool targetValid(const BF::URL & target); } #endif // RESOURCE_HPP diff --git a/src/response.cpp b/src/response.cpp index 3c660a4..15dd375 100644 --- a/src/response.cpp +++ b/src/response.cpp @@ -58,16 +58,22 @@ void Response::handleRequestGET(const Request * request, Response * response) { if (!request || !response) return; String target = request->targetPath(); - URL url(Resource::getRootFolder()); - url.append(target); + + URL targetURL(Resource::getRootFolder()); + targetURL.append(target); - if (BFFileSystemPathIsDirectory(url.abspath())) { - url.append("index.html"); + if (BFFileSystemPathIsDirectory(targetURL.abspath())) { + targetURL.append("index.html"); } + + if (!Resource::targetValid(targetURL)) { + response->_statusCode = 403; + response->_content = new Data("HTTP/1.1 403 Forbidden"); + response->_contentType = "text/plain"; - if (BFFileSystemPathIsFile(url.abspath())) { - response->_content = Data::fromFile(url); - response->_contentType = __ResponseTargetGetContentType(url); + } else if (BFFileSystemPathIsFile(targetURL.abspath())) { + response->_content = Data::fromFile(targetURL); + response->_contentType = __ResponseTargetGetContentType(targetURL); } else { response->_statusCode = 404; response->_content = new Data("404 Not Found"); diff --git a/testbench/request_tests.hpp b/testbench/request_tests.hpp index 362e47c..8ff0a40 100644 --- a/testbench/request_tests.hpp +++ b/testbench/request_tests.hpp @@ -18,28 +18,26 @@ extern "C" { using namespace BF; -BFTEST_UNIT_FUNC(test_requestInit, 1, { +BFTEST_UNIT_FUNC(test_requestInit, 2 << 10, { Data d; Request * req = new Request(&d); BFRelease(req); }) -BFTEST_UNIT_FUNC(test_simpleClientRequest, 2 << 6, { +BFTEST_UNIT_FUNC(test_simpleClientRequest, 2 << 10, { String get_str = "GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n"; Data get_buf(get_str); String post_str = "POST /submit.php HTTP/1.1\r\nHost: another.com\r\nContent-Length: 10\r\n\r\ndata=value"; Data post_buf(post_str); String head_str = "HEAD /static/image.png HTTP/1.0\r\n\r\n"; Data head_buf(head_str); - String invalid_str = "Invalid Request Line"; - Data invalid_buf(invalid_str); Request * req = NULL; req = new Request(&get_buf); BF_ASSERT(req->method() == "GET"); - BF_ASSERT(req->target() == "/index.html"); - BF_ASSERT(req->protocol() == "HTTP/1.1"); + BF_ASSERT(req->target() == "/index.html", "read '%s'", req->target().c_str()); + BF_ASSERT(req->protocol() == "HTTP/1.1", "read: '%s'", req->protocol().c_str()); BFRelease(req); req = new Request(&post_buf); @@ -53,15 +51,9 @@ BFTEST_UNIT_FUNC(test_simpleClientRequest, 2 << 6, { BF_ASSERT(req->target() == "/static/image.png"); BF_ASSERT(req->protocol() == "HTTP/1.0"); BFRelease(req); - - req = new Request(&invalid_buf); - BF_ASSERT(req->method() == ""); - BF_ASSERT(req->target().empty()); - BF_ASSERT(req->protocol().empty()); - BFRelease(req); }) -BFTEST_UNIT_FUNC(test_requestTargetPathAndQuery, 2 << 8, { +BFTEST_UNIT_FUNC(test_requestTargetPathAndQuery, 2 << 10, { String str = "GET /assets/fonts/fontawesome-webfont.ttf?v=4.6.3 HTTP/1.1\r\n\ Host: 10.0.0.82:8080\r\n\ User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0\r\n\ @@ -78,6 +70,9 @@ BFTEST_UNIT_FUNC(test_requestTargetPathAndQuery, 2 << 8, { HashMap query = req.targetQuery(); BF_ASSERT(query["v"] == "4.6.3"); + + const HashMap & header = req.header(); + BF_ASSERT(header["Host"] == "10.0.0.82:8080"); }) BFTEST_COVERAGE_FUNC(request_tests, { diff --git a/testbench/resource_tests.hpp b/testbench/resource_tests.hpp new file mode 100644 index 0000000..f28ba6e --- /dev/null +++ b/testbench/resource_tests.hpp @@ -0,0 +1,55 @@ +/** + * author: Brando + * date: 4/17/25 + */ + +#ifndef RESOURCE_TESTS_HPP +#define RESOURCE_TESTS_HPP + +#define ASSERT_PUBLIC_MEMBER_ACCESS + +#include +#include "resource.hpp" +#include + +extern "C" { +#include +#include +} + +using namespace BF; + +BFTEST_UNIT_FUNC(test_resourceRootFolder, 2 << 10, { + char tmp[PATH_MAX]; + URL url = __FILE__; + strcpy(tmp, url.abspath()); + + char * rootFolder = dirname(tmp); + Resource::setRootFolder(rootFolder); + + BF_ASSERT(Resource::targetValid(url.abspath())); + + char * parent = dirname(rootFolder); + URL parentURL(parent); + BF_ASSERT(!Resource::targetValid(parentURL.abspath())); + + URL challenge(Resource::getRootFolder()); + challenge.append("hello"); + challenge.append(".."); + challenge.append(".."); + BF_ASSERT(!Resource::targetValid(challenge.standardURL().abspath())); + + URL challenge1(Resource::getRootFolder()); + challenge1.append("hello"); + challenge1.append("world"); + challenge1.append(".."); + challenge1.append("validfile.txt"); + BF_ASSERT(Resource::targetValid(challenge1.standardURL().abspath()), "path '%s' not in root '%s'", challenge1.standardURL().abspath(), Resource::getRootFolder().c_str()); +}) + +BFTEST_COVERAGE_FUNC(resource_tests, { + BFTEST_LAUNCH(test_resourceRootFolder); +}) + +#endif // RESOURCE_TESTS_HPP + diff --git a/testbench/tests.cpp b/testbench/tests.cpp index b4c89f1..9893f27 100644 --- a/testbench/tests.cpp +++ b/testbench/tests.cpp @@ -4,10 +4,12 @@ */ #include "request_tests.hpp" +#include "resource_tests.hpp" #include "log.hpp" LOG_INIT; BFTEST_SUITE_FUNC({ BFTEST_SUITE_LAUNCH(request_tests); + BFTEST_SUITE_LAUNCH(resource_tests); }) diff --git a/todo.md b/todo.md index 5231029..998ff54 100644 --- a/todo.md +++ b/todo.md @@ -2,5 +2,8 @@ - [x] serve html - [x] receive http requests from client (be sure to assemble the packets) - [x] Handle GET requests for resources -- [ ] prevent requests from targetting anything outside of the root folder +- [x] prevent requests from targetting anything outside of the root folder +- [x] improve request parsing +- [x] multithread request packet handling +- [x] seg fault issue when you spam the reload page button