diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 7f9adcf..fa9e0e9 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,9 +1,9 @@ name: C Build and Test - on: pull_request: push: - branch: main + branches: + - main env: BUILD_TYPE: debug @@ -12,19 +12,39 @@ env: jobs: build-and-test: runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + container: image: koutoftimer/oduortoni-c-http-server-actions:latest - + steps: - - name: Checkout repository - uses: actions/checkout@v4 # Action to clone your repository's code - - - name: Build C code - run: | - make compile_templates - make bin/server - - - name: Run tests - run: | - make test # unit tests - ./tests/integration-tests.sh + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.PAT_TOKEN }} # Use PAT instead of GITHUB_TOKEN + fetch-depth: 0 + ref: ${{ github.head_ref || github.ref }} + + - name: Auto-format code + run: | + git config --global --add safe.directory $PWD + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + pre-commit run --all-files || true + if ! git diff --quiet; then + git add -A + git commit -m "style: auto-format code with clang-format [skip ci]" + git push + fi + + - name: Build C code + run: | + make compile_templates + make bin/server + + - name: Run tests + run: | + make test + ./tests/integration-tests.sh \ No newline at end of file diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml new file mode 100644 index 0000000..1b3e3a7 --- /dev/null +++ b/.github/workflows/clang-format.yml @@ -0,0 +1,36 @@ +name: Auto-format C code + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + clang-format: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Python (needed for pre-commit) + uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - name: Install pre-commit + run: pip install pre-commit + + - name: Run clang-format via pre-commit + run: pre-commit run --all-files --show-diff-on-failure --color=always + + - name: Commit formatted files + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -u + git commit -m "chore: auto-format with clang-format" || echo "No changes to commit" + git push + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml deleted file mode 100644 index 12ebb2e..0000000 --- a/.github/workflows/pre-commit.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Enforce C Code Style - -on: - pull_request: - push: - branch: main - -jobs: - pre-commit: - runs-on: ubuntu-latest - container: - image: koutoftimer/oduortoni-c-http-server-actions:latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Ceck all files - run: | - git config --global --add safe.directory $PWD - pre-commit run --all-files diff --git a/README.md b/README.md index 60b3037..f2bae07 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ Comprehensive documentation is available in the [docs](docs) folder. The documen - [Core Documentation](docs/DOCUMENTATION.md) - Beginner-friendly explanation of how the server works - [Visual Guide](docs/VISUAL_GUIDE.md) - Diagrams illustrating the server architecture - [Practical Examples](docs/EXAMPLES.md) - Code examples for extending the server +- [Template System](docs/TEMPLATE_SYSTEM.md) - Instructions of how to use the template system +- [Routing System](docs/ROUTING.md) - Instructions on how to use the routing system See the [docs README](docs/README.md) for more information. diff --git a/docs/ROUTING.md b/docs/ROUTING.md new file mode 100644 index 0000000..0f8e9dd --- /dev/null +++ b/docs/ROUTING.md @@ -0,0 +1,486 @@ +# Routing System Guide + +## Overview + +The routing system maps URL paths to handler functions using regex patterns. It supports dynamic path segments, sub-routers, and automatic 404 handling. + +--- + +## Quick Start + +### 1. Create a Router + +```c +Router* router = router_create_regex(); +``` + +### 2. Register Routes + +```c +router_add(router, "^/$", Index); +router_add(router, "^/about$", About); +router_add(router, "^/users/([0-9]+)$", UserProfile); +``` + +### 3. Start the Server + +```c +http.ListenAndServe("127.0.0.1:9000", router); +``` + +--- + +## Handler Functions + +Handlers process requests and write responses: + +```c +int Index(ResponseWriter* w, Request* r) +{ + SetStatus(w, 200, "OK"); + SetHeader(w, "Content-Type", "text/html"); + w->WriteString(w, "

Welcome!

"); + return EXIT_SUCCESS; +} +``` + +**Handler Signature:** +- `ResponseWriter* w` - Write response data +- `Request* r` - Read request data (method, path, headers, body) +- Returns `int` - `EXIT_SUCCESS` or error code + +--- + +## Route Patterns + +Routes use POSIX Extended Regular Expressions: + +### Static Routes + +```c +router_add(router, "^/$", Index); // Exact match: / +router_add(router, "^/about$", About); // Exact match: /about +router_add(router, "^/contact$", Contact); // Exact match: /contact +``` + +**Important:** Always use `^` (start) and `$` (end) anchors for exact matching. + +### Dynamic Routes (Capture Groups) + +```c +// Match: /users/123, /users/456 +router_add(router, "^/users/([0-9]+)$", UserProfile); + +// Match: /posts/hello-world, /posts/my-post +router_add(router, "^/posts/([a-z-]+)$", BlogPost); + +// Match: /static/style.css, /static/images/logo.png +router_add(router, "^/static/(.*)$", StaticFiles); +``` + +### Accessing Captured Values + +Use `regmatch_t` to extract captured segments: + +```c +int UserProfile(ResponseWriter* w, Request* r) +{ + // Extract user ID from path + regmatch_t match = r->path_matches[1]; // First capture group + int start = match.rm_so; + int end = match.rm_eo; + + char user_id[32]; + snprintf(user_id, sizeof(user_id), "%.*s", end - start, r->path.data + start); + + SetStatus(w, 200, "OK"); + w->WriteString(w, "User ID: "); + w->WriteString(w, user_id); + + return EXIT_SUCCESS; +} +``` + +**Note:** `path_matches[0]` is the full match, `path_matches[1]` is the first capture group, etc. + +--- + +## Request Object + +Access request data through the `Request` struct: + +```c +int Handler(ResponseWriter* w, Request* r) +{ + // HTTP method + if (strcmp(r->method, "POST") == 0) { + // Handle POST request + } + + // Request path + printf("Path: %.*s\n", (int)r->path.size, r->path.data); + + // Request body + if (r->body.size > 0) { + printf("Body: %.*s\n", (int)r->body.size, r->body.data); + } + + // Headers (iterate through headers array) + for (size_t i = 0; i < r->headers.len; i++) { + printf("Header: %.*s = %.*s\n", + (int)r->headers.items[i].name.size, + r->headers.items[i].name.data, + (int)r->headers.items[i].value.size, + r->headers.items[i].value.data); + } + + return EXIT_SUCCESS; +} +``` + +--- + +## Response Writer + +Build responses using the `ResponseWriter` object: + +### Set Status + +```c +SetStatus(w, 200, "OK"); +SetStatus(w, 404, "Not Found"); +SetStatus(w, 500, "Internal Server Error"); +``` + +### Set Headers + +```c +SetHeader(w, "Content-Type", "text/html"); +SetHeader(w, "Content-Type", "application/json"); +SetHeader(w, "Cache-Control", "no-cache"); +``` + +### Write Body + +```c +// Write string +w->WriteString(w, "

Hello World

"); + +// Write formatted data +char buffer[256]; +snprintf(buffer, sizeof(buffer), "User ID: %d", user_id); +w->WriteString(w, buffer); + +// Write raw data +response_write(w, data, data_length); +``` + +--- + +## Form Data + +Parse form submissions: + +```c +int ContactForm(ResponseWriter* w, Request* r) +{ + if (strcmp(r->method, "POST") == 0) { + FormData form_data = {0}; + parse_form_data(r->body, &form_data); + + const char* name = get_form_value(&form_data, "name"); + const char* email = get_form_value(&form_data, "email"); + const char* message = get_form_value(&form_data, "message"); + + if (!name || !email || !message) { + SetStatus(w, 400, "Bad Request"); + w->WriteString(w, "Missing required fields"); + return -1; + } + + // Process form data... + SetStatus(w, 200, "OK"); + w->WriteString(w, "Form submitted successfully!"); + return EXIT_SUCCESS; + } + + // Show form for GET requests + SetStatus(w, 200, "OK"); + SetHeader(w, "Content-Type", "text/html"); + w->WriteString(w, "
...
"); + return EXIT_SUCCESS; +} +``` + +--- + +## Sub-Routers + +Organize routes into modules using sub-routers: + +### Create a Sub-Router + +**`src/app/blog/router.c`** +```c +#include "header.h" +#include "http/header.h" + +Router* BlogRouter() +{ + Router* router = router_create_regex(); + router_add(router, "^/blog$", BlogIndex); + router_add(router, "^/blog/([0-9]+)$", BlogPost); + router_add(router, "^/blog/new$", BlogNew); + return router; +} +``` + +### Mount Sub-Router + +**`src/main.c`** +```c +Router* router = router_create_regex(); +router_add(router, "^/$", Index); + +// Mount blog router at /apps prefix +// Routes become: /apps/blog, /apps/blog/123, /apps/blog/new +router_mount(router, "/apps", BlogRouter()); + +http.ListenAndServe("127.0.0.1:9000", router); +``` + +**How it works:** +- Child pattern `^/blog$` + prefix `/apps` → `^/apps/blog$` +- Child pattern `^/blog/([0-9]+)$` + prefix `/apps` → `^/apps/blog/([0-9]+)$` + +--- + +## 404 Handling + +### Custom 404 Handler + +```c +int Error404(ResponseWriter* w, Request* r) +{ + SetStatus(w, 404, "Not Found"); + SetHeader(w, "Content-Type", "text/html"); + w->WriteString(w, "

404 - Page Not Found

"); + return EXIT_SUCCESS; +} + +// Register 404 handler +router_add(router, "^/404$", Error404); +``` + +The router automatically uses the `^/404$` route when no other route matches. + +### Default 404 + +If no `^/404$` route is registered, the server returns a plain text 404 response. + +--- + +## Complete Example + +**`src/main.c`** +```c +#include "app/header.h" +#include "http/header.h" + +int main() +{ + // Create main router + Router* router = router_create_regex(); + + // Static routes + router_add(router, "^/$", Index); + router_add(router, "^/about$", About); + router_add(router, "^/contact$", Contact); + + // Dynamic routes + router_add(router, "^/users/([0-9]+)$", UserProfile); + router_add(router, "^/posts/([a-z0-9-]+)$", BlogPost); + + // Static files + router_add(router, "^/static/(.*)$", StaticFiles); + + // 404 handler + router_add(router, "^/404$", Error404); + + // Mount sub-routers + router_mount(router, "/api", ApiRouter()); + router_mount(router, "/admin", AdminRouter()); + + // Start server + printf("Server listening on http://127.0.0.1:9000\n"); + http.ListenAndServe("127.0.0.1:9000", router); + + // Cleanup (never reached due to infinite loop) + router_free(router); + + return 0; +} +``` + +**`src/app/handlers.c`** +```c +#include "header.h" + +int Index(ResponseWriter* w, Request* r) +{ + SetStatus(w, 200, "OK"); + SetHeader(w, "Content-Type", "text/html"); + w->WriteString(w, "

Welcome to C HTTP Server

"); + return EXIT_SUCCESS; +} + +int UserProfile(ResponseWriter* w, Request* r) +{ + // Extract user ID from URL + regmatch_t match = r->path_matches[1]; + char user_id[32]; + snprintf(user_id, sizeof(user_id), "%.*s", + match.rm_eo - match.rm_so, + r->path.data + match.rm_so); + + SetStatus(w, 200, "OK"); + SetHeader(w, "Content-Type", "text/html"); + + char response[256]; + snprintf(response, sizeof(response), + "

User Profile

User ID: %s

", user_id); + w->WriteString(w, response); + + return EXIT_SUCCESS; +} + +int Error404(ResponseWriter* w, Request* r) +{ + SetStatus(w, 404, "Not Found"); + SetHeader(w, "Content-Type", "text/html"); + w->WriteString(w, + "" + "

404 - Page Not Found

" + "

The requested page does not exist.

" + ""); + return EXIT_SUCCESS; +} +``` + +--- + +## Common Patterns + +### REST API Routes + +```c +router_add(router, "^/api/users$", UsersIndex); // GET /api/users +router_add(router, "^/api/users/([0-9]+)$", UsersShow); // GET /api/users/123 +router_add(router, "^/api/posts$", PostsIndex); // GET /api/posts +router_add(router, "^/api/posts/([0-9]+)$", PostsShow); // GET /api/posts/456 +``` + +Handle different HTTP methods in the handler: + +```c +int UsersIndex(ResponseWriter* w, Request* r) +{ + if (strcmp(r->method, "GET") == 0) { + // List users + } else if (strcmp(r->method, "POST") == 0) { + // Create user + } else { + SetStatus(w, 405, "Method Not Allowed"); + return -1; + } + return EXIT_SUCCESS; +} +``` + +### File Extensions + +```c +// Match: /page.html, /about.html +router_add(router, "^/([a-z]+)\\.html$", HtmlPages); + +// Match: /api/v1/users, /api/v2/users +router_add(router, "^/api/(v[0-9]+)/users$", ApiUsers); +``` + +### Optional Segments + +```c +// Match: /search or /search/query +router_add(router, "^/search(/.*)?$", Search); +``` + +--- + +## Best Practices + +1. **Always use anchors:** Start patterns with `^` and end with `$` +2. **Order matters:** More specific routes should come before general ones +3. **Validate input:** Always check captured values before using them +4. **Return proper status codes:** Use appropriate HTTP status codes +5. **Set Content-Type:** Always set the correct content type header +6. **Handle errors:** Return error codes when operations fail +7. **Free resources:** Clean up allocated memory in handlers +8. **Use sub-routers:** Organize related routes into modules + +--- + +## Regex Quick Reference + +| Pattern | Matches | Example | +|---------|---------|---------| +| `^/path$` | Exact path | `/path` | +| `[0-9]+` | One or more digits | `123`, `456` | +| `[a-z]+` | One or more lowercase letters | `hello`, `world` | +| `[a-zA-Z0-9-]+` | Alphanumeric with hyphens | `my-post-123` | +| `.*` | Any characters (greedy) | `anything/here` | +| `[^/]+` | Any characters except `/` | `filename` | +| `(/.*)?` | Optional path segment | `/optional` or empty | +| `\\.` | Literal dot | `.html` | + +--- + +## Troubleshooting + +### Route Not Matching + +- Check that pattern has `^` and `$` anchors +- Test regex pattern with online tools +- Verify route is registered before starting server +- Check route order (specific before general) + +### Capture Groups Not Working + +- Ensure pattern has parentheses: `([0-9]+)` +- Use correct index: `path_matches[1]` for first group +- Check `rm_so` and `rm_eo` are valid before accessing + +### 404 Always Returned + +- Verify handler function signature is correct +- Check that route pattern matches the URL exactly +- Ensure router is passed to `ListenAndServe` + +--- + +## Memory Management + +The router automatically manages memory for routes. Always free the router when done: + +```c +router_free(router); +``` + +**Note:** In the current implementation, the server runs indefinitely, so `router_free` is never reached. For testing or cleanup, you can call it explicitly. + +--- + +## See Also + +- [Server Architecture](SERVER_ARCHITECTURE.md) - Overall system design +- [Examples](EXAMPLES.md) - More code examples +- [Template System](TEMPLATE_SYSTEM.md) - Rendering HTML responses diff --git a/docs/template-system-specification.md b/docs/TEMPLATE_SYSTEM.md similarity index 83% rename from docs/template-system-specification.md rename to docs/TEMPLATE_SYSTEM.md index b05f2db..cbf61b8 100644 --- a/docs/template-system-specification.md +++ b/docs/TEMPLATE_SYSTEM.md @@ -1,3 +1,55 @@ +# The Template System And The ZC Compiler + +## Install The Compiler + +### 1. System-Wide Install (Requires superuser permissions) + +```bash +git clone https://github.com/koutoftimer/zc.git /tmp/zc +cd /tmp/zc && make +sudo cp /tmp/zc/build/zc /usr/local/bin/zc +``` + +Verify installation: +```bash +which zc +# Output: /usr/local/bin/zc +``` + +### 2. Rootless Install (User directory) + +```bash +git clone https://github.com/koutoftimer/zc.git /tmp/zc +cd /tmp/zc && make +mkdir -p ~/bin +cp /tmp/zc/build/zc ~/bin/zc +``` + +Add to your shell profile (`~/.bashrc` or `~/.zshrc`): +```bash +export PATH="$HOME/bin:$PATH" +``` + +Reload your shell: +```bash +source ~/.bashrc # or source ~/.zshrc +``` + +Verify installation: +```bash +which zc +# Output: /home/username/bin/zc +``` + +**Note:** If using rootless install, update the Makefile: +```makefile +ZC_TOOL_PATH ?= $(HOME)/bin/zc +``` + + + +## How It Works + Templates are the files with an extension `*.th` ("template header" files). **`index-template.th`** diff --git a/src/app/about.c b/src/app/about.c deleted file mode 100644 index 1dede08..0000000 --- a/src/app/about.c +++ /dev/null @@ -1,32 +0,0 @@ -#include "header.h" - -// clang-format off -static char const html[] = -"" -"" -"" -" About Page" -" " -"" -"" -"

About This Server

" -"

This is a simple HTTP server written in C.

" -"

It supports basic HTTP requests and serves static content.

" -"" -""; -// clang-format on - -int -About(ResponseWriter* w, [[maybe_unused]] Request* r) -{ - // Set status and headers - SetStatus(w, 200, "OK"); - SetHeader(w, "Content-Type", "text/html"); - - // Write the About page to the response - w->WriteString(w, html); - - return EXIT_SUCCESS; -} diff --git a/src/app/header.h b/src/app/header.h index 548397e..1a4c31c 100644 --- a/src/app/header.h +++ b/src/app/header.h @@ -2,7 +2,6 @@ #define _APP_H #include "http/header.h" -#include "template/header.h" #define TEMPLATE_PATH "src/app/templates/" @@ -10,5 +9,6 @@ int Index(ResponseWriter* w, Request* r); int About(ResponseWriter* w, Request* r); int Error404(ResponseWriter* w, Request* r); int Static(ResponseWriter* w, Request* r); +int TestHandler(ResponseWriter* w, Request* r); #endif diff --git a/src/app/index.c b/src/app/index.c index ce84359..30c6408 100644 --- a/src/app/index.c +++ b/src/app/index.c @@ -1,51 +1,20 @@ #include "header.h" -#include "utils/logging/header.h" -#include "utils/string-builder/header.h" - -struct Context { - char const* name; - char const* email; - char const* message; - bool is_method_post; -}; - -#define IMPLEMENTATION -#include "templates/index.h" int Index(ResponseWriter* w, Request* r) { - struct Context ctx = {0}; - FormData form_data = {0}; - - if (strcmp(r->method, "POST") == 0) { - parse_form_data(r->body, &form_data); - - ctx.name = get_form_value(&form_data, "name"); - ctx.email = get_form_value(&form_data, "email"); - ctx.message = get_form_value(&form_data, "message"); - ctx.is_method_post = true; - - debug("name: %s\nemail: %s\nmessage: %s", ctx.name, ctx.email, - ctx.message); - - if (!ctx.name || !ctx.email || !ctx.message) { - SetStatus(w, 400, "Missing form fields"); - return -1; - } - } - - struct StringBuilder sb = {0}; - bool ok = render_template(&ctx, &sb); - if (!ok) { - SetStatus(w, 500, "Failed to render the page"); - return -1; - } - SetStatus(w, 200, "OK"); SetHeader(w, "Content-Type", "text/html"); - w->WriteString(w, sb.ascii.data); - free(sb.ascii.data); + char buffer[300]; + snprintf(buffer, 300, + "

A " + "Minimalistic C Server

Request path: " + "%s

", + r->path.data); + w->WriteString(w, buffer); return EXIT_SUCCESS; } diff --git a/src/app/templates/index.html b/src/app/templates/index.html deleted file mode 100644 index de84ce3..0000000 --- a/src/app/templates/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - C HTTP Server Form - - - - -

Sample Form

-
-
- - -
-
- - -
-
- - -
- -
-
- - {{ if ($.is_method_post) { -}} -
-

Thank you for your submission!

-

Name: {{ $.name }}

-

Email: {{ $.email }}

-

Message: {{ $.message }}

-
- {{ } -}} - - - diff --git a/src/lib/http/header.h b/src/lib/http/header.h index eba24aa..1a8af46 100644 --- a/src/lib/http/header.h +++ b/src/lib/http/header.h @@ -1,7 +1,6 @@ #ifndef _HTTP_HEADER_H #define _HTTP_HEADER_H -#include #include #include "net/header.h" @@ -130,10 +129,32 @@ char* BuildResponse(ResponseWriter* rw); typedef int (*HandlerFunc)(ResponseWriter* w, Request* r); +/*------------------------------------------------ Routing + * -------------------------------------------------------------*/ + +struct Dispatcher { + HandlerFunc (*match)(void* impl_data, const char* path, Request* req); + void (*add_route)(void* impl_data, const char* pattern, + HandlerFunc handler); + void (*mount)(void* parent_data, const char* prefix, void* child_data); + void (*free)(void* impl_data); +}; +typedef struct Dispatcher Dispatcher; + +struct Route { + char* pattern; + regex_t compiled_pattern; + HandlerFunc handler; +}; +typedef struct Route Route; + struct Router { char* patterns[50]; regex_t regex_patterns[50]; HandlerFunc handlers[50]; + int route_count; + const Dispatcher* dispatcher; + void* impl_data; }; typedef struct Router Router; @@ -146,6 +167,17 @@ typedef enum RouterStatus { ROUTER_INVALID_REGEX = -5, } RouterStatus; +HandlerFunc router_match(Router* router, const char* path, Request* req); +void router_add(Router* router, const char* pattern, HandlerFunc handler); +void router_mount(Router* parent, const char* prefix, Router* child); +HandlerFunc router_match_dispatcher(Router* router, const char* path, + Request* req); +void router_free(Router* router); +Router* router_create_regex(void); + +/*------------------------------------------------ END OF Routing + * ----------------------------------*/ + struct HttpServer { int (*ListenAndServe)(char* host, Router* router); RouterStatus (*HandleFunc)(const char* pattern, HandlerFunc handler); @@ -167,5 +199,6 @@ ProtocolResponse http_handle_connection(RequestContext* context, size_t request_len); extern HttpServer http; +extern Router router; #endif // _HTTP_HEADER_H diff --git a/src/lib/http/http_form_parser.c b/src/lib/http/http_form_parser.c index a7b90ae..75abf15 100644 --- a/src/lib/http/http_form_parser.c +++ b/src/lib/http/http_form_parser.c @@ -1,3 +1,5 @@ +#include + #include "header.h" #include "utils/logging/header.h" diff --git a/src/lib/http/http_handle.c b/src/lib/http/http_handle.c index cb0651b..d5cffb2 100644 --- a/src/lib/http/http_handle.c +++ b/src/lib/http/http_handle.c @@ -30,24 +30,25 @@ http_handle(Router* router, const char* request_data) InitResponseWriter(&rw); // Find and call handler - HandlerFunc handler = nullptr; - for (size_t i = 0; - i < ARRAY_LEN(router->patterns) && router->patterns[i]; i++) { - req->path_regex = &router->regex_patterns[i]; - if (!regexec(req->path_regex, req->path.data, - ARRAY_LEN(req->path_matches), req->path_matches, - 0)) { - info("Using '%s' handler", router->patterns[i]); - handler = router->handlers[i]; - break; + HandlerFunc handler = router_match(router, req->path.data, req); + + // If no handler found, try 404 + if (handler == nullptr) { + printf("ROUTE NOT FOUND\n"); + req->path_regex = nullptr; + for (int i = 0; i < router->route_count; i++) { + if (strcmp("^/404$", router->patterns[i]) == 0) { + info("Using '%s' handler", router->patterns[i]); + handler = router->handlers[i]; + break; + } } } // If no handler found, try 404 if (handler == nullptr) { req->path_regex = nullptr; - for (size_t i = 0; - i < ARRAY_LEN(router->patterns) && router->patterns[i]; + for (int i = 0; i < router->route_count && router->patterns[i]; i++) { if (strcmp("^/404$", router->patterns[i]) == 0) { info("Using '%s' handler", router->patterns[i]); diff --git a/src/lib/http/http_handlerfunc.c b/src/lib/http/http_handlerfunc.c index 11693d1..ebee180 100644 --- a/src/lib/http/http_handlerfunc.c +++ b/src/lib/http/http_handlerfunc.c @@ -1,5 +1,4 @@ #include "header.h" -#include "utils/macros.h" RouterStatus handleFunc(const char* pattern, HandlerFunc handler) @@ -13,30 +12,33 @@ handleFunc(const char* pattern, HandlerFunc handler) if (!http.router) { return ROUTER_NOMEM; } + http.router->route_count = 0; } - for (size_t i = 0; i < ARRAY_LEN(http.router->patterns); i++) { - if (http.router->patterns[i]) { - // prevent duplicate route registration - if (strcmp(http.router->patterns[i], pattern) == 0) { - return ROUTER_DUPLICATE; - } - continue; + for (int i = 0; i < http.router->route_count; i++) { + // prevent duplicate route registration + if (strcmp(http.router->patterns[i], pattern) == 0) { + return ROUTER_DUPLICATE; } + continue; + } - if (regcomp(&http.router->regex_patterns[i], pattern, - REG_EXTENDED)) { - return ROUTER_INVALID_REGEX; - } + if (http.router->route_count >= + 50) { // TODO: replace with dynamic ARRAY_LEN equivalent + return ROUTER_FULL; + } - http.router->patterns[i] = strdup(pattern); - if (!http.router->patterns[i]) { - return ROUTER_NOMEM; - } + int i = http.router->route_count; + if (regcomp(&http.router->regex_patterns[i], pattern, REG_EXTENDED)) { + return ROUTER_INVALID_REGEX; + } - http.router->handlers[i] = handler; - return ROUTER_OK; + http.router->patterns[i] = strdup(pattern); + if (!http.router->patterns[i]) { + return ROUTER_NOMEM; } - return ROUTER_FULL; + http.router->handlers[i] = handler; + http.router->route_count++; + return ROUTER_OK; } diff --git a/src/lib/http/http_req_parser.c b/src/lib/http/http_req_parser.c index c040bc3..dc40e24 100644 --- a/src/lib/http/http_req_parser.c +++ b/src/lib/http/http_req_parser.c @@ -1,4 +1,5 @@ #include +#include #include #include "header.h" diff --git a/src/lib/http/http_router.c b/src/lib/http/http_router.c new file mode 100644 index 0000000..20f312a --- /dev/null +++ b/src/lib/http/http_router.c @@ -0,0 +1,65 @@ +#include "header.h" +#include "utils/macros.h" + +HandlerFunc +router_match(Router* router, const char* path, Request* req) +{ + if (router->dispatcher) { + return router->dispatcher->match(router->impl_data, path, req); + } + + // leave the old logic as a fallback + for (int i = 0; i < router->route_count; i++) { + req->path_regex = &router->regex_patterns[i]; + if (!regexec(req->path_regex, path, + ARRAY_LEN(req->path_matches), req->path_matches, + 0)) { + return router->handlers[i]; + } + } + return nullptr; +} + +void +router_add(Router* router, const char* pattern, HandlerFunc handler) +{ + if (router->dispatcher) { + router->dispatcher->add_route(router->impl_data, pattern, + handler); + } +} + +void +router_mount(Router* parent, const char* prefix, Router* child) +{ + if (parent->dispatcher && child->dispatcher && + parent->dispatcher == child->dispatcher) { + parent->dispatcher->mount(parent->impl_data, prefix, + child->impl_data); + router_free(child); + } +} + +HandlerFunc +router_match_dispatcher(Router* router, const char* path, Request* req) +{ + if (router->dispatcher) { + return router->dispatcher->match(router->impl_data, path, req); + } + return nullptr; +} + +void +router_free(Router* router) +{ + if (router->dispatcher) { + router->dispatcher->free(router->impl_data); + } else { + // if there is no dispatcher, this is a fallback for old arrays + for (int i = 0; i < router->route_count; i++) { + free(router->patterns[i]); + regfree(&router->regex_patterns[i]); + } + } + free(router); +} diff --git a/src/lib/http/http_server.c b/src/lib/http/http_server.c index 43e8490..6d40792 100644 --- a/src/lib/http/http_server.c +++ b/src/lib/http/http_server.c @@ -10,15 +10,34 @@ int listenAndServe(char* host, Router* router) { if (router == NULL) { + // if no router is provided, use global http router router = http.router; + } else { + // if pprovided, make global point to it + http.router = router; } puts("http/server.c"); - for (size_t i = 0; - i < ARRAY_LEN(router->patterns) && router->patterns[i]; i++) { - printf("SERVERT: %s\n", router->patterns[i]); + + if (router == NULL) { + printf("ERROR: Router is NULL\n"); + return -1; + } + + puts("http/server.c"); + printf("Router address: %p\n", (void*)router); + printf("Dispatcher address: %p\n", (void*)router->dispatcher); + + if (!router->dispatcher) { + for (size_t i = 0; + i < ARRAY_LEN(router->patterns) && router->patterns[i]; + i++) { + printf("SERVERT: %s\n", router->patterns[i]); + } + } else { + printf("Using dispatcher-based router\n"); } - // printf("RouteD %s\n", router->patterns[1]); - RequestContext context = {.router = router}; + printf("RouteD\n"); + RequestContext context = {.router = http.router}; net_serve(host, http_handle_connection, &context); return 0; diff --git a/src/lib/http/route_dispatchers/header.h b/src/lib/http/route_dispatchers/header.h new file mode 100644 index 0000000..57fd1dd --- /dev/null +++ b/src/lib/http/route_dispatchers/header.h @@ -0,0 +1,17 @@ +#ifndef HTTP_ROUTE_DISPATHER_H +#define HTTP_ROUTE_DISPATHER_H + +#include + +#include "http/header.h" + +struct RegexRouterData { + struct Route* items; + size_t len; + size_t capacity; +}; +typedef struct RegexRouterData RegexRouterData; + +Router* router_create_regex(void); + +#endif // HTTP_ROUTE_DISPATHER_H diff --git a/src/lib/http/route_dispatchers/regex_dispatcher.c b/src/lib/http/route_dispatchers/regex_dispatcher.c new file mode 100644 index 0000000..b9d00fa --- /dev/null +++ b/src/lib/http/route_dispatchers/regex_dispatcher.c @@ -0,0 +1,97 @@ +#include "header.h" +#include "http/header.h" +#include "utils/macros.h" + +HandlerFunc +regex_match(void* impl_data, const char* path, Request* req) +{ + printf("Search for route '%s'\n", path); + + RegexRouterData* data = (RegexRouterData*)impl_data; + for (size_t i = 0; i < data->len; i++) { + req->path_regex = &data->items[i].compiled_pattern; + if (!regexec(req->path_regex, path, + ARRAY_LEN(req->path_matches), req->path_matches, + 0)) { + return data->items[i].handler; + } + } + return nullptr; +} + +/* FIX ISSUES*/ +static void +regex_add_route(void* impl_data, const char* pattern, HandlerFunc handler) +{ + RegexRouterData* data_ptr = (RegexRouterData*)impl_data; + RegexRouterData data; + + data.items = data_ptr->items; + data.capacity = data_ptr->capacity; + data.len = data_ptr->len; + + struct Route route = {.pattern = strdup(pattern), .handler = handler}; + + regcomp(&route.compiled_pattern, pattern, REG_EXTENDED); + + da_append(data, route); + + data_ptr->items = data.items; + data_ptr->capacity = data.capacity; + data_ptr->len = data.len; +} + +static void +regex_mount(void* parent_data, const char* prefix, void* child_data) +{ + RegexRouterData* child = (RegexRouterData*)child_data; + + for (size_t i = 0; i < child->len; i++) { + char* child_pattern = child->items[i].pattern; + + // Remove ^ from start and $ from end of child pattern + char* pattern_body = child_pattern + 1; // Skip ^ + size_t body_len = strlen(pattern_body); + if (body_len > 0 && pattern_body[body_len - 1] == '$') { + body_len--; // Remove $ + } + + // Create new pattern: ^$ + char* new_pattern = + malloc(strlen(prefix) + body_len + 3); // ^ + $ + \0 + sprintf(new_pattern, "^%s%.*s$", prefix, (int)body_len, + pattern_body); + + regex_add_route(parent_data, new_pattern, + child->items[i].handler); + free(new_pattern); + } +} + +static void +regex_free(void* impl_data) +{ + RegexRouterData* data = (RegexRouterData*)impl_data; + for (size_t i = 0; i < data->len; i++) { + free(data->items[i].pattern); + regfree(&data->items[i].compiled_pattern); + } + free(data->items); + free(data); +} + +// initialize the regex dispatcher with member fields +static const Dispatcher regex_dispatcher = {.match = regex_match, + .add_route = regex_add_route, + .mount = regex_mount, + .free = regex_free}; + +Router* +router_create_regex(void) +{ + Router* router = calloc(1, sizeof(Router)); + RegexRouterData* data = calloc(1, sizeof(RegexRouterData)); + router->dispatcher = ®ex_dispatcher; + router->impl_data = data; + return router; +} diff --git a/src/lib/template/header.h b/src/lib/template/header.h deleted file mode 100644 index 762f203..0000000 --- a/src/lib/template/header.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef _TEMPLATE_HEADER_H -#define _TEMPLATE_HEADER_H - -#endif // _TEMPLATE_HEADER_H diff --git a/src/main.c b/src/main.c index 73a4aea..3814e6e 100644 --- a/src/main.c +++ b/src/main.c @@ -24,17 +24,28 @@ main() sprintf(hostname, "%s:%d", host_url, PORT); } - http.HandleFunc("^/$", Index); - http.HandleFunc("^/about$", About); - http.HandleFunc("^/404$", Error404); - http.HandleFunc("^/static/(.*)$", Static); + Router* router = router_create_regex(); + router_add(router, "^/$", Index); + router_add(router, "^/404$", Error404); + router_add(router, "^/static/(.*)$", Static); - http.ListenAndServe(hostname, NULL); - - // Router router = {{"/404", "/", "/about", NULL}, {Error404, Index, - // About, NULL}}; printf("Route: %s\n", router.patterns[1]); - // http.ListenAndServe(hostname, &router); + /* + * Used to check for memory leaks in allocation and deallocation of + * memory + */ + // router_free(router); + // printf("Freed test router\n"); printf("Server listening on %d\n", PORT); + http.ListenAndServe(hostname, router); + + /* + * TODO: This is never reached due to infinite listener that stops on + * CTRL + C + * - Need to add a way to handle graceful shut down + */ + printf("\n\n\t << Graceful Shutdown >>\n\n"); + router_free(router); + return 0; } diff --git a/src/tools/thc.c b/src/tools/thc.c deleted file mode 100644 index bf9be2f..0000000 --- a/src/tools/thc.c +++ /dev/null @@ -1,253 +0,0 @@ -#include -#include -#include -#include -#include - -#include "utils/header.h" - -int indentation_level = 1; -constexpr int indentation = 4; - -bool -print_escaped(char const* start, char const* end, struct StringBuilder* sb) -{ -#define output(...) \ - do { \ - bool ok = sb_appendf(sb, __VA_ARGS__); \ - if (!ok) return false; \ - } while (0) - - for (auto p = start; p < end; ++p) { - switch (*p) { - case '\"': - output("%s", "\\\""); - break; - case '\\': - output("%s", "\\\\"); - break; - case '\n': { - // combine newlines on single output line - for (; p < end && *p == '\n'; ++p) { - output("%s", "\\n"); - } - if (p >= end || *p == '\0') break; - --p; - - // split multi-line string - constexpr int quotes_index = sizeof("sb_appendf(sb,"); - output("\"\n%*s\"", - indentation_level * indentation + quotes_index, - ""); - } break; - case '\r': - output("%s", "\\r"); - break; - default: - output("%c", *p); - } - } - - return true; - -#undef output -} - -char* -trim(char* str) -{ - while (isspace(*str)) str++; - if (*str == 0) return str; - char* end = str + strlen(str) - 1; - while (end > str && isspace(*end)) end--; - end[1] = '\0'; - return str; -} - -int -translate(const char* const name, char const* const src) -{ - auto len = strlen(name); - char guard[len + 1]; - guard[len] = '\0'; - for (size_t i = 0; i < len; i++) { - guard[i] = toupper((unsigned char)name[i]); - if (guard[i] == '/') guard[i] = '_'; - } - - struct StringBuilder sb = {0}; - -#define outputf(...) \ - do { \ - bool ok = sb_appendf(&sb, __VA_ARGS__); \ - if (!ok) { \ - free(sb.ascii.data); \ - return EXIT_FAILURE; \ - } \ - } while (0) -#define output(...) outputf("%s", __VA_ARGS__) - -#define print_escaped(...) \ - do { \ - bool ok = print_escaped(__VA_ARGS__); \ - if (!ok) { \ - free(sb.ascii.data); \ - return EXIT_FAILURE; \ - } \ - } while (0) - - // Header Guard - outputf("#ifndef %s_H\n#define %s_H\n\n", guard, guard); - output( - "bool render_template(" - "struct Context* ctx, struct StringBuilder* sb);\n\n"); - output("#endif\n\n#ifdef IMPLEMENTATION\n\n"); - output("#define $ (*ctx)\n"); - output( - "#define sb_appendf(...) do { " - "bool res = sb_appendf(__VA_ARGS__); if (!res) return false; " - "} while(0)\n\n"); - output( - "bool\nrender_template(" - "struct Context* ctx, struct StringBuilder* sb)\n{\n"); - - char const* cursor = src; - while (*cursor) { - char* tag_open = strstr(cursor, "{{"); - - if (!tag_open) { - // Remainder of file - outputf("%*ssb_appendf(sb, \"", - indentation_level * indentation, ""); - print_escaped(cursor, cursor + strlen(cursor), &sb); - output("\");\n"); - break; - } - - // 1. Literal text before tag - struct String text_before_tag = {0}; - if (tag_open > cursor) { - text_before_tag.data = (char*)cursor; - text_before_tag.size = tag_open - cursor; - } - - // 2. Parse Tag - char* tag_contents = tag_open + 2; - char* tag_close = strstr(tag_contents, "}}"); - - if (!tag_close) { - int starting_line = 1; - for (auto it = src; it < tag_open; ++it) { - starting_line += *it == '\n'; - } - fprintf(stderr, - "ERROR: Template statement has unmatched " - "parentecies\n%s:%d:\n", - name, starting_line); - auto tail_len = strlen(tag_open); - if (tail_len > 40) tail_len = 40; - tag_open[tail_len] = '\0'; - fprintf(stderr, "%s", tag_open); - free(sb.ascii.data); - return EXIT_FAILURE; - } - - bool line_elimination = - (tag_close > tag_contents && *(tag_close - 1) == '-'); - - // Extract inner content - size_t content_len = tag_close - tag_contents; - if (line_elimination) content_len--; - - char* raw_content = strndup(tag_contents, content_len); - char* code = trim(raw_content); - - cursor = tag_close + 2; - - // 3. Handle Line Elimination - if (line_elimination) { - // Skip trailing white-spaces and exactly one newline - while (isspace(*cursor) && *cursor != '\n') { - cursor++; - } - if (*cursor == '\n') ++cursor; - - // Skip prepended white-spaces - while (text_before_tag.size) { - char last = text_before_tag - .data[text_before_tag.size - 1]; - if (isspace(last) && last != '\n') { - text_before_tag.size--; - } else { - break; - } - } - } - - // Output text before tag - if (text_before_tag.size) { - outputf("%*ssb_appendf(sb, \"", - indentation_level * indentation, ""); - print_escaped( - text_before_tag.data, - text_before_tag.data + text_before_tag.size, &sb); - output("\");\n"); - } - - // Output tag body - size_t const code_len = strlen(code); - if (strncmp(code, "$.", 2) == 0) { - // String Mode shortcut - outputf("%*ssb_appendf(sb, \"%%s\", %s);\n", - indentation_level * indentation, "", code); - } else if (code[0] == '\"') { - // Format Mode shortcut - outputf("%*ssb_appendf(sb, %s);\n", - indentation_level * indentation, "", code); - } else if (code_len) { - // Raw Mode (C code) - // count brackets balance for indentation - int brackets = 0; - for (size_t i = 0; i < code_len; ++i) { - brackets += code[i] == '{'; - brackets -= code[i] == '}'; - } - // decrease indentation before tag's body - if (brackets < 0) indentation_level += brackets; - outputf("%*s%s\n", indentation_level * indentation, "", - code); - // increase indentation after tag's body - if (brackets > 0) indentation_level += brackets; - } - free(raw_content); - } - - output(" return true;\n"); - output("}\n\n#undef sb_appendf\n#undef $\n#endif\n"); - - printf("%s", sb.ascii.data); - free(sb.ascii.data); - - return EXIT_SUCCESS; -} - -int -main(int argc, char** argv) -{ - if (argc < 2) return fprintf(stderr, "Usage: thc \n"), 1; - - auto content = read_entire_file(argv[1]); - if (!content.data) return perror("file"), 1; - - // Get filename without extension for the guard - char* filename = strdup(argv[1]); - char* ext = strrchr(filename, '.'); - if (ext) *ext = '\0'; - - auto exit_code = translate(filename, content.data); - - free(filename); - free(content.data); - - return exit_code; -} diff --git a/tests/app/index.hpp b/tests/app/index.hpp deleted file mode 100644 index 47323de..0000000 --- a/tests/app/index.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#include - -extern "C" { -#include "src/app/header.h" -} - -TEST(app_index, wrong_form_data) -{ - struct ResponseWriter rw = {}; - const char* body = ""; - struct Request r = {}; - r.body.size = strlen(body) + 1; - r.body.data = (char*)malloc(r.body.size); - strcpy(r.method, "POST"); - strcpy(r.body.data, body); - int result = Index(&rw, &r); - ASSERT_EQ(result, -1); - free(r.body.data); -} - -TEST(app_index, correct_form_data) -{ - struct ResponseWriter rw = {}; - InitResponseWriter(&rw); - const char* body = - "name=John Doe" - "&email=jd@example.com" - "&message=hello world"; - struct Request r = {}; - r.body.size = strlen(body) + 1; - r.body.data = (char*)malloc(r.body.size); - strcpy(r.method, "POST"); - strcpy(r.body.data, body); - int result = Index(&rw, &r); - ASSERT_EQ(result, 0); - free(r.body.data); -} diff --git a/tests/gtest.cpp b/tests/gtest.cpp index de5cd37..12f4a16 100644 --- a/tests/gtest.cpp +++ b/tests/gtest.cpp @@ -1,6 +1,5 @@ #include -#include "./app/index.hpp" #include "./lib/utils/string-builder/tests.hpp" int diff --git a/tests/integration-tests.hurl b/tests/integration-tests.hurl index 280d38d..056643e 100644 --- a/tests/integration-tests.hurl +++ b/tests/integration-tests.hurl @@ -4,10 +4,6 @@ GET http://localhost:{{PORT}}/ HTTP 200 -# Test: About page -GET http://localhost:{{PORT}}/about -HTTP 200 - # Test: Explicit 404 page GET http://localhost:{{PORT}}/404 HTTP 404