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
+