diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d02a467 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,48 @@ +name: CI + +on: + push: + branches: [ "dev" ] + paths-ignore: + - '*.md' + - 'docs/**' + pull_request: + branches: [ "dev" ] + paths-ignore: + - '*.md' + - 'docs/**' + +jobs: + unit-tests-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.PAT_TOKEN }} + - name: Install uuid-dev + run: | + sudo apt -y update; + sudo apt -y upgrade; + sudo apt -y install uuid-dev; + - name: building libs + run: | + cd external; + make build-libs; + - name: building/running tests + run: make build CONFIG=test run + + unit-tests-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.PAT_TOKEN }} + - name: building libs + run: | + cd external; + make build-libs; + - name: building/running tests + run: make build CONFIG=test run + diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..00e253b --- /dev/null +++ b/example/index.html @@ -0,0 +1,12 @@ + + + +Page Title + + + +

This is a Heading

+

This is a paragraph.

+ + + diff --git a/external/libs b/external/libs index 075a26b..35461a9 160000 --- a/external/libs +++ b/external/libs @@ -1 +1 @@ -Subproject commit 075a26b8f9ec93110cd3dd22a6e319e970b40519 +Subproject commit 35461a9eae5d0e552707aea329ca1c4beccbcb43 diff --git a/makefile b/makefile index ae0766b..5d4a931 100644 --- a/makefile +++ b/makefile @@ -37,41 +37,41 @@ BIN_PATH = bin/$(CONFIG) BUILD_TYPE = executable SOURCE_EXT = cpp HEADER_EXT = hpp -FILES = \ +FILES = office request response resource log ifeq ($(CONFIG),release) # release -LIBRARIES += \ - external/bin/libs/release/bflibc/libbfc.a \ +LIBRARIES = \ + external/bin/libs/release/bfnet/libbfnet.a \ external/bin/libs/release/bflibcpp/libbfcpp.a \ - external/bin/libs/release/bfnet/libbfnet.a + external/bin/libs/release/bflibc/libbfc.a else -LIBRARIES += \ - external/bin/libs/debug/bflibc/libbfc-debug.a \ +LIBRARIES = \ + external/bin/libs/debug/bfnet/libbfnet-debug.a \ external/bin/libs/debug/bflibcpp/libbfcpp-debug.a \ - external/bin/libs/debug/bfnet/libbfnet-debug.a + external/bin/libs/debug/bflibc/libbfc-debug.a endif -LINKS = $(BF_LIB_C_FLAGS) +LINKS = -lpthread $(BF_LIB_C_FLAGS) -ldl ### Release settings ifeq ($(CONFIG),release) # release MAIN_FILE = src/main.cpp BIN_NAME = http -FLAGS = $(CPPFLAGS) -Isrc/ $(CPPSTD) -Iexternal/bin/libs/release $(OPENSSL_INCLUDE_PATH) +FLAGS = $(CPPFLAGS) -Isrc/ $(CPPSTD) -Iexternal/bin/libs/release ### Debug settings else ifeq ($(CONFIG),debug) # debug MAIN_FILE = src/main.cpp -BIN_NAME = http +BIN_NAME = http-debug #ADDR_SANITIZER = -fsanitize=address -FLAGS = $(CPPFLAGS) -DDEBUG -g -Isrc/ $(ADDR_SANITIZER) $(CPPSTD) -Iexternal/bin/libs/debug $(OPENSSL_INCLUDE_PATH) +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 -FLAGS = $(CPPFLAGS) -DDEBUG -DTESTING -g -Isrc/ $(ADDR_SANITIZER) $(CPPSTD) -Iexternal/bin/libs/debug $(OPENSSL_INCLUDE_PATH) +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/notes.md b/notes.md new file mode 100644 index 0000000..aa9313a --- /dev/null +++ b/notes.md @@ -0,0 +1 @@ +* Using https://developer.mozilla.org/en-US/docs/Web/HTTP as a guide diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..6092514 --- /dev/null +++ b/readme.md @@ -0,0 +1,7 @@ +# http +An http server using POSIX sockets + +## Resources +* [How I Built a Simple HTTP Server from Scratch using C ](https://dev.to/jeffreythecoder/how-i-built-a-simple-http-server-from-scratch-using-c-739) +* [Mozilla HTTP Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP) + diff --git a/src/log.cpp b/src/log.cpp new file mode 100644 index 0000000..1a3a4de --- /dev/null +++ b/src/log.cpp @@ -0,0 +1,66 @@ +/** + * author: brando + * date: 4/9/25 + */ + +#include "log.hpp" +#include +#include +#include +#include + +extern "C" { +#include +} + +void _LogWriteEntry(BFFileWriter * filewriter, int mode, ...) { +#ifndef TESTING + if (!filewriter) return; + + va_list arg0, arg1; + va_start(arg0, mode); + va_start(arg1, mode); + + const char * format = va_arg(arg0, const char *); + if (!format) return; + + char * logstr = BFStringCreateFormatArgListString(format, arg0); + if (!logstr) return; + + BFDateTime dt = {0}; + if (BFTimeGetCurrentDateTime(&dt)) return; + + format = "[%02d/%02d/%04d, %02d:%02d:%02d] - %s"; + + BFFileWriterQueueFormatLine( + filewriter, + format, + dt.month, + dt.day, + dt.year, + dt.hour, + dt.minute, + dt.second, + logstr + ); + + printf( + format, + dt.month, + dt.day, + dt.year, + dt.hour, + dt.minute, + dt.second, + logstr + ); + printf("\n"); + fflush(stdout); + + va_end(arg0); + va_end(arg1); + + BFFree(logstr); +#endif +} + diff --git a/src/log.hpp b/src/log.hpp new file mode 100644 index 0000000..cb8519c --- /dev/null +++ b/src/log.hpp @@ -0,0 +1,72 @@ +/** + * author: brando + * date: 4/9/25 + */ + +#ifndef LOG_HPP +#define LOG_HPP + +extern "C" { +#include +} + +#define CHAT_LOG_PATH "/tmp/http.log" + +extern BFFileWriter gFileWriter; + +/** + * must define in a source file + */ +#define LOG_INIT \ +BFFileWriter gFileWriter = 0; \ +void __LogCallbackBFNet(const char * buf) { \ + LOG_DEBUG("bfnet: %s", buf); \ + LOG_FLUSH; \ +} + +/** + * initializes logs for: + * - http + * - bfnet + */ +#define LOG_OPEN \ +BFFileWriterCreate(&gFileWriter, CHAT_LOG_PATH); \ +BF::Net::Log::SetCallback(__LogCallbackBFNet); + +/** + * `mode`: 'd' for debug, 'e' for error, or 0 for normal + */ +void _LogWriteEntry(BFFileWriter * filewriter, int mode, ...); + +/** + * LOG_WRITE vs LOG_DEBUG vs LOG_ERROR + * + * - each writes a line into the same log file + * - each log entry will explicitly tell you what type of log entry it is + */ + +/** + * writes ent (line) to log file + */ +#define LOG_WRITE(...) _LogWriteEntry(&gFileWriter, 0, __VA_ARGS__) + +#define LOG_ERROR(...) _LogWriteEntry(&gFileWriter, 'e', __VA_ARGS__) + +#ifdef DEBUG +#define LOG_DEBUG(...) _LogWriteEntry(&gFileWriter, 'd', __VA_ARGS__) +#else // DEBUG +#define LOG_DEBUG(...) +#endif // DEBUG + +/** + * flushes write buffers + */ +#define LOG_FLUSH BFFileWriterFlush(&gFileWriter) + +/** + * closes log file + */ +#define LOG_CLOSE BFFileWriterClose(&gFileWriter) + +#endif // LOG_HPP + diff --git a/src/main.cpp b/src/main.cpp index be62cce..f463a26 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,10 +3,87 @@ * date: 2/26/25 */ -#include +#include "office.hpp" +#include "log.hpp" +#include "resource.hpp" +#include +#include +#include +#include + +extern "C" { +#include +} + +#define ARGUMENT_ROOT "-root" + +using namespace BF::Net; +using namespace BF; +using namespace std; + +LOG_INIT; + +void help(const char * toolname) { + printf("usage: %s %s \n", toolname, ARGUMENT_ROOT); + printf("\n"); + printf("Arguments:\n"); + printf(" %s \tThis is the root folder where we will look for resources\n", ARGUMENT_ROOT); + printf("\nCopyright © 2025 Brando. All rights reserved.\n"); +} + +void __NewConnection(Connection * sc) { + LOG_WRITE("new connection made"); +} + +int __ReadArguments(int argc, char * argv[]) { + if (argc == 1) { + help(argv[0]); + return -1; + } + + for (int i = 0; i < argc; i++) { + if (!strcmp(argv[i], ARGUMENT_ROOT)) { + if (!Resource::setRootFolder(argv[++i])) { + LOG_ERROR("'%s' is not accepted as a root folder", argv[i]); + } + } + } -int main() { - printf("Hello world!\n"); return 0; } +int main(int argc, char * argv[]) { + LOG_OPEN; + + if (__ReadArguments(argc, argv)) { + return -1; + } + + int error = 0; + Log::SetCallback(__LogCallbackBFNet); + + Office::start(); + Socket * skt = Socket::create(SOCKET_MODE_SERVER, "0.0.0.0", 8080, &error); + if (!error) { + skt->setInStreamCallback(Office::envelopeReceive); + skt->setNewConnectionCallback(__NewConnection); + skt->setBufferSize(1024 * 1024 * 100); + error = skt->start(); + } + + if (!error) { + cout << "Press any key to stop..."; + cin.get(); + error = skt->stop(); + + cout << "Stopped..." << endl; + } + + BFRelease(skt); + Office::stop(); + + LOG_CLOSE; + + return error; +} + diff --git a/src/office.cpp b/src/office.cpp new file mode 100644 index 0000000..b756ab0 --- /dev/null +++ b/src/office.cpp @@ -0,0 +1,88 @@ +/** + * author: brando + * date: 4/3/25 + */ + +#include "office.hpp" +#include "request.hpp" +#include "response.hpp" +#include "log.hpp" +#include +#include +#include +#include + +using namespace BF::Net; +using namespace BF; +using namespace std; + +/** + * holds all incoming envelopes from open socket + * + * all envelopes are retained, so these should be released + * once popped from queue + */ +Atomic> _incomingRequests; +BFThreadAsyncID _tidRequestQueue = NULL; +BFLock _queueSema; + +void Office::envelopeReceive(Envelope * envelope) { + _incomingRequests.lock(); + + BFRetain(envelope); + _incomingRequests.unsafeget().push(envelope); + + _incomingRequests.unlock(); + BFLockRelease(&_queueSema); +} + +void __IncomingRequestsWorkerThread(void * in) { + while (!BFThreadAsyncIsCanceled(_tidRequestQueue)) { + if (_incomingRequests.get().empty()) { + 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) { + continue; + } + + Request * req = new Request(envelope->data()); + + Response * resp = Response::fromRequest(req); + if (resp) { + const Data * respData = resp->createData(); + envelope->connection()->queueData(respData); + + BFRelease(respData); + } + + BFRelease(resp); + BFRelease(req); + BFRelease(envelope); + } + } +} + +void Office::start() { + BFLockCreate(&_queueSema); + _tidRequestQueue = BFThreadAsync(__IncomingRequestsWorkerThread, NULL); +} + +void Office::stop() { + BFThreadAsyncCancel(_tidRequestQueue); + BFLockRelease(&_queueSema); + BFThreadAsyncWait(_tidRequestQueue); + BFThreadAsyncDestroy(_tidRequestQueue); + + BFLockDestroy(&_queueSema); +} + diff --git a/src/office.hpp b/src/office.hpp new file mode 100644 index 0000000..8436b86 --- /dev/null +++ b/src/office.hpp @@ -0,0 +1,20 @@ +/** + * author: brando + * date: 4/3/25 + */ + +#ifndef OFFICE_HPP +#define OFFICE_HPP + +#include + +namespace Office { + +void envelopeReceive(BF::Net::Envelope * envelope); +void start(); +void stop(); + +} + +#endif // OFFICE_HPP + diff --git a/src/request.cpp b/src/request.cpp new file mode 100644 index 0000000..e860628 --- /dev/null +++ b/src/request.cpp @@ -0,0 +1,138 @@ +/** + * author: brando + * date: 4/3/25 + */ + +#include "request.hpp" +#include "log.hpp" +#include + +extern "C" { +#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()); +} + +Request::~Request() { +} + +String Request::method() const { + std::regex methodRegex(R"((GET|POST|PUT|DELETE|HEAD|OPTIONS|CONNECT|TRACE|PATCH)\s+)"); + std::smatch match; + + 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 + } +} + +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; + + 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 + } +} + +// component: 0=path, 1=query +String __RequestTargetParse(String & target, int component) { + int error = component != 0 && component != 1 ? 1 : 0; + String res; + char * target_copy = NULL; + + if (!error) { + target_copy = target.cStringCopy(); + if (!target_copy) { + res = target; + error = 1; + } + } + + const char * del = "?"; + char * sub = NULL; + int i = 0; + while (!error && component >= 0) { + sub = strtok(i == 0 ? target_copy : NULL, del); + if (!sub) { + res = target; + error = 1; + } + + component--; + i++; + } + + if (!error) { + res = sub; + } + + BFFree(target_copy); + + return res; + +} + +String Request::targetPath() const { + String target = this->target(); + return __RequestTargetParse(target, 0); +} + +int __RequestQueryPairParse(HashMap & map, const char * pair) { + if (!pair) return 1; + + char * pair_copy = BFStringCopyString(pair); + const char * del = "="; + String key = strtok(pair_copy, del); + String value = strtok(NULL, del); + + map.insert(key, value); + + BFFree(pair_copy); + + return 0; +} + +HashMap Request::targetQuery() const { + HashMap res; + String target = this->target(); + String queryString = __RequestTargetParse(target, 1); + char * query_copy = queryString.cStringCopy(); + + const char * del = "&"; + char * sub = NULL; + int i = 0; + while ((sub = strtok(i++ == 0 ? query_copy : NULL, del)) != NULL) { + __RequestQueryPairParse(res, sub); + } + + BFFree(query_copy); + + 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 new file mode 100644 index 0000000..4ae171f --- /dev/null +++ b/src/request.hpp @@ -0,0 +1,42 @@ +/** + * author: brando + * date: 4/3/25 + */ + +#ifndef REQUEST_HPP +#define REQUEST_HPP + +#include +#include +#include +#include + +typedef enum { + kRequestMethodNone = 0, + kRequestMethodGet, + kRequestMethodPost, +} RequestMethod; + +class Request : public BF::Object { +public: + Request(const BF::Data * data); + virtual ~Request(); + + BF::String method() const; + BF::String target() const; + BF::String protocol() const; + + // returns path without query string + BF::String targetPath() const; + + // returns query data + BF::HashMap targetQuery() const; + + BF::String host() const; + +private: + std::string _message; +}; + +#endif // REQUEST_HPP + diff --git a/src/resource.cpp b/src/resource.cpp new file mode 100644 index 0000000..a5c6c1f --- /dev/null +++ b/src/resource.cpp @@ -0,0 +1,35 @@ +/** + * author: brando + * date: 4/9/25 + */ + +#include "resource.hpp" +#include "log.hpp" +#include + +extern "C" { +#include +} + +#ifdef LINUX +#include +#endif +#include + +using namespace BF; + +String _rootFolder; + +const BF::String & Resource::getRootFolder() { + return _rootFolder; +} + +bool Resource::setRootFolder(const String & rootFolder) { + URL url(rootFolder); + if (!BFFileSystemPathExists(url.abspath())) { + return false; + } + _rootFolder = rootFolder; + return true; +} + diff --git a/src/resource.hpp b/src/resource.hpp new file mode 100644 index 0000000..95d0c8d --- /dev/null +++ b/src/resource.hpp @@ -0,0 +1,19 @@ +/** + * author: brando + * date: 4/9/25 + * + * handles resource requests from GET + */ + +#ifndef RESOURCE_HPP +#define RESOURCE_HPP + +#include + +namespace Resource { + bool setRootFolder(const BF::String & rootFolder); + const BF::String & getRootFolder(); +} + +#endif // RESOURCE_HPP + diff --git a/src/response.cpp b/src/response.cpp new file mode 100644 index 0000000..3c660a4 --- /dev/null +++ b/src/response.cpp @@ -0,0 +1,133 @@ +/** + * author: brando + * date: 4/3/25 + */ + +#include "response.hpp" +#include "resource.hpp" +#include "log.hpp" +#include + +extern "C" { +#include +} + +#define HTTP_DATE_BUFSIZE 128 + +using namespace BF; + +Response::Response() { } + +Response::~Response() { + BFRelease(this->_content); +} + +Response * Response::fromRequest(const Request * request) { + if (!request) return NULL; + + Response * res = new Response; + if (!res) return NULL; + res->_statusCode = 200; + + if (request->method() == "GET") { + Response::handleRequestGET(request, res); + } + + return res; +} + +String __ResponseTargetGetContentType(URL & target) { + if (!strcmp(target.extension(), "html")) { + return "text/html"; + } else if (!strcmp(target.extension(), "js")) { + return "text/javascript"; + } else if (!strcmp(target.extension(), "css")) { + return "text/css"; + } else if (!strcmp(target.extension(), "ico")) { + return "image/x-icon"; + } else if (!strcmp(target.extension(), "jpg") || !strcmp(target.extension(), "jpeg") + || !strcmp(target.extension(), "jfif") || !strcmp(target.extension(), "pjpeg") + || !strcmp(target.extension(), "pjp")) { + return "image/jpeg"; + } else { + return "text/plain"; + } +} + +void Response::handleRequestGET(const Request * request, Response * response) { + if (!request || !response) return; + + String target = request->targetPath(); + URL url(Resource::getRootFolder()); + url.append(target); + + if (BFFileSystemPathIsDirectory(url.abspath())) { + url.append("index.html"); + } + + if (BFFileSystemPathIsFile(url.abspath())) { + response->_content = Data::fromFile(url); + response->_contentType = __ResponseTargetGetContentType(url); + } else { + response->_statusCode = 404; + response->_content = new Data("404 Not Found"); + response->_contentType = "text/plain"; + } +} + +const Data * Response::createData() const { + String content; + this->writeStatusLine(content); + this->writeHeader(content); + + content.append("\r\n"); + + if (!this->_content) { + LOG_DEBUG("%s:%d - null content", __func__, __LINE__); + } else { + content.append(*this->_content); + } + + return new Data(content); +} + +void Response::writeStatusLine(String & content) const { + if (this->_statusCode == 404) { + content.append("HTTP/1.1 %d Not Found\r\n", this->_statusCode); + } else { + content.append("HTTP/1.1 %d Ok\r\n", this->_statusCode); + } +} + +void __ResponseGenerateDateString(char * buf) { + if (!buf) return; + + time_t now = time(NULL); + if (now == (time_t)(-1)) { + return; + } + + struct tm timeinfo_gmt; + if (gmtime_r(&now, &timeinfo_gmt) == NULL) { + return; + } + + strftime( + buf, + HTTP_DATE_BUFSIZE, + "%a, %d %b %Y %H:%M:%S GMT", + &timeinfo_gmt); +} + +void Response::writeHeader(String & content) const { + content.append("Server: brando's http server\r\n"); + + char buf[HTTP_DATE_BUFSIZE]; + __ResponseGenerateDateString(buf); + content.append("Date: %s\r\n", buf); + + content.append("Content-Length: %d\r\n", this->_content ? this->_content->size() : 0); + + content.append("Content-Type: %s\r\n", this->_contentType.c_str()); +} + diff --git a/src/response.hpp b/src/response.hpp new file mode 100644 index 0000000..688ca81 --- /dev/null +++ b/src/response.hpp @@ -0,0 +1,38 @@ +/** + * author: brando + * date: 4/3/25 + */ + +#ifndef RESPONSE_HPP +#define RESPONSE_HPP + +#include +#include +#include +#include "request.hpp" + +class Response : public BF::Object { +public: + static Response * fromRequest(const Request *); + virtual ~Response(); + + /** + * the http response in raw text we will send back + * + * caller owns data + */ + const BF::Data * createData() const; +private: + Response(); + + static void handleRequestGET(const Request * request, Response * response); + void writeStatusLine(BF::String & content) const; + void writeHeader(BF::String & content) const; + + BF::Data * _content; + BF::String _contentType; + int _statusCode = 0; +}; + +#endif // RESPONSE_HPP + diff --git a/testbench/request_tests.hpp b/testbench/request_tests.hpp new file mode 100644 index 0000000..362e47c --- /dev/null +++ b/testbench/request_tests.hpp @@ -0,0 +1,90 @@ +/** + * author: Brando + * date: 4/3/25 + */ + +#ifndef REQUEST_TESTS_HPP +#define REQUEST_TESTS_HPP + +#define ASSERT_PUBLIC_MEMBER_ACCESS + +#include +#include "request.hpp" + +extern "C" { +#include +#include +} + +using namespace BF; + +BFTEST_UNIT_FUNC(test_requestInit, 1, { + Data d; + Request * req = new Request(&d); + BFRelease(req); +}) + +BFTEST_UNIT_FUNC(test_simpleClientRequest, 2 << 6, { + 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"); + BFRelease(req); + + req = new Request(&post_buf); + BF_ASSERT(req->method() == "POST"); + BF_ASSERT(req->target() == "/submit.php"); + BF_ASSERT(req->protocol() == "HTTP/1.1"); + BFRelease(req); + + req = new Request(&head_buf); + BF_ASSERT(req->method() == "HEAD"); + 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, { + 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\ + Accept: application/font-woff2;q=1.0,application/font-woff;q=0.9,*/*;q=0.8\r\n\ + Accept-Language: en-US,en;q=0.5\r\n\ + Accept-Encoding: gzip, deflate\r\n\ + Connection: keep-alive\r\n\ + Referer: http://10.0.0.82:8080/assets/css/font-awesome.min.css\r\n\ + "; + Data buf(str); + + Request req(&buf); + BF_ASSERT(req.targetPath() == "/assets/fonts/fontawesome-webfont.ttf"); + + HashMap query = req.targetQuery(); + BF_ASSERT(query["v"] == "4.6.3"); +}) + +BFTEST_COVERAGE_FUNC(request_tests, { + BFTEST_LAUNCH(test_requestInit); + BFTEST_LAUNCH(test_simpleClientRequest); + BFTEST_LAUNCH(test_requestTargetPathAndQuery); +}) + +#endif // REQUEST_TESTS_HPP + diff --git a/testbench/tests.cpp b/testbench/tests.cpp new file mode 100644 index 0000000..b4c89f1 --- /dev/null +++ b/testbench/tests.cpp @@ -0,0 +1,13 @@ +/** + * author: brando + * date: 4/3/25 + */ + +#include "request_tests.hpp" +#include "log.hpp" + +LOG_INIT; +BFTEST_SUITE_FUNC({ + BFTEST_SUITE_LAUNCH(request_tests); +}) + diff --git a/todo.md b/todo.md index 7c0a2f3..5231029 100644 --- a/todo.md +++ b/todo.md @@ -1,2 +1,6 @@ **0.1** -- [ ] server html +- [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 +