├── .github └── workflows │ └── build.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.yml ├── docs └── index.html ├── example ├── auth.c ├── chat.c ├── counter.c ├── cweb ├── deploy.bat ├── json.c ├── readme.md ├── routes.ini ├── static.c ├── todo.c ├── websocket.c ├── webui.c └── ws.html ├── include ├── container.h ├── crypto.h ├── cweb.h ├── db.h ├── defer.h ├── http.h ├── libevent.h ├── list.h ├── map.h ├── pool.h ├── queue.h ├── router.h └── scheduler.h ├── libs ├── .keep ├── libevent.c └── module.c ├── modules └── .keep ├── production.sh ├── src ├── container.c ├── crypto.c ├── db.c ├── engine.c ├── http.c ├── list.c ├── map.c ├── mngt.c ├── pool.c ├── queue.c ├── router.c ├── scheduler.c ├── server.c └── ws.c └── static ├── .keep ├── hello.txt └── index.html /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # Check out the repository code 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | # Set up a C/C++ build environment 19 | - name: Set up dependencies 20 | run: | 21 | sudo apt-get update && sudo apt-get install -y \ 22 | libssl-dev \ 23 | libsqlite3-dev \ 24 | libjansson-dev \ 25 | make \ 26 | gcc 27 | 28 | # Build the project 29 | - name: Build project 30 | run: make 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | # Cweb 55 | .vscode/ 56 | bin/ 57 | modules/routes.dat 58 | tmp/ 59 | db.sqlite3 60 | server.key 61 | server.crt -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Debian minimal image 2 | FROM debian:latest 3 | 4 | # Install necessary packages 5 | RUN apt-get update && apt-get install -y \ 6 | libssl-dev \ 7 | libsqlite3-dev \ 8 | libjansson-dev \ 9 | make \ 10 | gcc \ 11 | && apt-get clean \ 12 | && rm -rf /var/lib/apt/lists/* 13 | 14 | WORKDIR /app 15 | COPY . /app 16 | EXPOSE 8080 17 | 18 | # Define a build argument for production 19 | ARG PRODUCTION=0 20 | 21 | # Run make with the appropriate flags based on the PRODUCTION argument 22 | RUN if [ "$PRODUCTION" -eq 1 ]; then make clean && make PRODUCTION=1; else make clean && make; fi 23 | 24 | 25 | CMD ["./bin/cweb"] 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Compiler 2 | ifeq ($(OS), Windows_NT) 3 | $(error Windows is not supported) 4 | endif 5 | 6 | CC = gcc 7 | 8 | SRC_DIR = src 9 | BUILD_DIR = build 10 | BIN_DIR = bin 11 | 12 | LDFLAGS = -lssl -lcrypto -lsqlite3 -ljansson 13 | CFLAGS = -O2 -Wall -Werror -Wextra -I$(SRC_DIR) -I./include -pthread -g 14 | ifeq ($(shell uname), Darwin) 15 | HOMEBREW_PREFIX := $(shell brew --prefix openssl@3) 16 | JANSSON_PREFIX := $(shell brew --prefix jansson) 17 | ifeq ($(HOMEBREW_PREFIX),) 18 | $(error openssl@3 is not installed, make sure its installed with `brew install openssl@3`) 19 | endif 20 | ifeq ($(JANSSON_PREFIX),) 21 | $(error jansson is not installed, make sure its installed with `brew install jansson`) 22 | endif 23 | 24 | CFLAGS += -I/opt/homebrew/opt/openssl@3/include -I/opt/homebrew/opt/jansson/include -fsanitize=thread,undefined 25 | LDFLAGS += -L/opt/homebrew/opt/openssl@3/lib -L/opt/homebrew/opt/jansson/lib 26 | endif 27 | # Export dynamic symbols on Linux 28 | ifeq ($(shell uname), Linux) 29 | CFLAGS += -Wl,--export-dynamic -fsanitize=thread,undefined,bounds 30 | endif 31 | ifdef PRODUCTION 32 | CFLAGS += -DPRODUCTION 33 | endif 34 | 35 | SRCS = $(wildcard $(SRC_DIR)/*.c) 36 | OBJS = $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRCS)) 37 | 38 | TARGET = $(BIN_DIR)/cweb 39 | 40 | # Library 41 | LIB_DIR = libs 42 | LIB_SRCS = libs/module.c src/map.c 43 | LIB_OBJS = $(patsubst $(LIB_DIR)/%.c, $(BUILD_DIR)/%.o, $(LIB_SRCS)) 44 | LIB_TARGET = $(LIB_DIR)/libmodule.so 45 | 46 | all: $(LIB_TARGET) $(TARGET) 47 | 48 | $(TARGET): $(OBJS) ${LIB_DIR}/libevent.so 49 | @mkdir -p $(BIN_DIR) 50 | $(CC) $(LDFLAGS) $(CFLAGS) -o $@ $^ 51 | 52 | $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c 53 | @mkdir -p $(BUILD_DIR) 54 | $(CC) $(CFLAGS) -c -o $@ $< 55 | 56 | $(LIB_TARGET): $(LIB_OBJS) 57 | @mkdir -p $(BIN_DIR) 58 | $(CC) $(CFLAGS) -I./include -fPIC -shared -o $@ $^ 59 | 60 | $(BUILD_DIR)/%.o: $(LIB_DIR)/%.c 61 | @mkdir -p $(BUILD_DIR) 62 | $(CC) $(CFLAGS) -fPIC -c -o $@ $< 63 | 64 | ${LIB_DIR}/libevent.so: ${LIB_DIR}/libevent.c 65 | $(CC) $(CFLAGS) -fPIC -shared -o $@ $^ 66 | 67 | clean: 68 | rm -rf $(BUILD_DIR) $(BIN_DIR) 69 | rm -f $(LIB_TARGET) libs/libevent.so 70 | 71 | purge: 72 | rm -rf ./modules/*.so 73 | rm -rf ./modules/routes.dat 74 | 75 | run: all 76 | $(TARGET) 77 | 78 | 79 | .PHONY: all clean run -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # c-web-modules: Modules for the Web 2 | 3 | > **Note:** 4 | > This project is currently a **proof of concept** and represents the **bare minimum viable product (MVP)**. 5 | > It is not designed for production use, and there may be bugs, limitations, or incomplete features. 6 | > Use at your own discretion. 7 | 8 | Welcome to **c-web-modules**, a modular and efficient approach to web development in C. Inspired by kernel modules and AWS Lambda, this project allows you to upload C code directly to the server, which compiles and deploys it at runtime. No precompilation is necessary, and the server can easily be upgraded to include more features or external libraries. 9 | 10 | ## Documentation 11 | 12 | For more detailed information, please refer to the [official documentation](https://joexbayer.github.io/c-web-modules/). 13 | 14 | ## Addressing the Challenges of C for Web Development 15 | 16 | C isn’t typically the go-to for web development, and there are valid reasons why. Here’s how **c-web-modules** tackles some of the common concerns: 17 | 18 | 1. **Slow Build Cycles**: 19 | Instead of recompiling the entire application, **c-web-modules** allows you to upload raw C code. The server compiles it on-the-fly, enabling rapid iteration and eliminating the need for restarts. This is as close to "hot reloading" as you can get with C. 20 | 21 | 2. **Speed vs. Practicality**: 22 | While raw computation speed might not always be critical for web apps, **c-web-modules** shines in scenarios where performance matters, like handling heavy data processing or real-time applications. Modules let you inject optimized performance-critical code where it’s needed. 23 | 24 | 3. **Manpower and Time-to-Market**: 25 | By automating common server tasks (e.g., routing, module integration, and shared resources like SQLite3), **c-web-modules** reduces boilerplate and accelerates development compared to starting from scratch. It's not as fast as scripting languages, but it's far from the manual grind of traditional C projects. 26 | 27 | 4. **Memory Management and Crashes**: 28 | Modules are isolated and dynamically managed, reducing the risk of crashing the entire server. While C still requires careful memory management, the modular approach lets you focus on smaller, manageable pieces of functionality rather than tackling a monolithic application. 29 | 30 | 5. **Pre-Made Solutions**: 31 | By supporting external libraries like SQLite3, OpenSSL, and Jansson out of the box, **c-web-modules** leverages existing solutions, allowing developers to skip reinventing the wheel and focus on their application's unique needs. 32 | 33 | This isn’t a silver bullet—it’s a proof of concept. But **c-web-modules** aims to bring C’s raw power into the web world in a more developer-friendly way. 34 | 35 | --- 36 | 37 | # Getting started 38 | 39 | ## TLS 40 | 41 | The production environment uses TLS for encrypting, by default it expects there to be a server.crt and server.key which can be generated by using this command: 42 | 43 | ```bash 44 | openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt -days 365 -nodes 45 | ``` 46 | 47 | ## Example: Counter Module 48 | 49 | Here’s a simple example of a module that keeps track of a counter and returns its value every time you visit `/counter`. 50 | See more examples in the `example/` folder. 51 | 52 | [Websocket](example/websocket.c) 53 | 54 | [Chat application](example/chat.c) 55 | 56 | [JSON](example/json.c) 57 | 58 | [TODO App](example/todo.c) 59 | 60 | SQLite3 App (TODO) 61 | 62 | #### `counter.c` 63 | ```c 64 | #include 65 | #include 66 | 67 | static int counter = 0; 68 | static const char* template = 69 | "\n" 70 | " \n" 71 | "

Counter: %d

\n" 72 | " \n" 73 | "\n"; 74 | 75 | /* Route: /counter - Method GET */ 76 | static int index_route(struct http_request *req, struct http_response *res) { 77 | snprintf(res->body, HTTP_RESPONSE_SIZE, template, counter++); 78 | res->status = HTTP_200_OK; 79 | return 0; 80 | } 81 | 82 | /* Define the routes for the module */ 83 | export module_t config = { 84 | .name = "counter", 85 | .author = "cweb", 86 | .routes = { 87 | {"/counter", "GET", index_route, NONE}, 88 | }, 89 | .size = 1, 90 | }; 91 | ``` 92 | 93 | --- 94 | 95 | ## Why Use c-web-modules? 96 | 97 | 1. **Code Deployment**: Upload raw C code to the server for on-the-fly compilation and deployment. 98 | 2. **No Precompilation**: Simplify your workflow—focus on writing code, and let the server handle compilation. 99 | 3. **Dynamic Updates**: Add or replace functionality without downtime or recompiling the entire server. 100 | 4. **Performance**: Written in C, the server offers unmatched speed and efficiency. 101 | 5. **WebSocket Support**: Even when modules are updated, existing WebSocket connections remain alive. 102 | 6. **Built-In Features**: Includes a cross-module cache and scheduler for deferred tasks. 103 | 7. **Regex in Paths**: Define routes using regular expressions for more flexible and powerful URL matching. 104 | 105 | Currently supported external libraries: 106 | - **OpenSSL**: Currently only for hashing, but later for secure communication. 107 | - **SQLite3**: Shared by all modules for lightweight database needs. 108 | - **Jansson**: For easy JSON parsing and manipulation. 109 | 110 | --- 111 | 112 | # Deployment 113 | 114 | Deploying code to the server is simple and can be done in multiple ways, depending on your workflow. 115 | 116 | ### 1. Basic Deployment with `curl` 117 | 118 | At its core, deploying code to the server involves sending a POST request with the C file attached. Here’s an example using `curl`: 119 | 120 | `curl -X POST -F "code=@path/to/yourcode.c" http://localhost:8080/mgnt` 121 | 122 | ### 2. Using the cweb script and .ini config 123 | The script handles: 124 | - Sending the file to the server using `curl`. 125 | - Parsing responses for success or failure. 126 | - Providing helpful logs and error messages. 127 | 128 | #### Deploying Multiple Files with a Config File 129 | `./cweb deploy path/to/yourcode.c` 130 | 131 | You can deploy multiple modules in one go using a configuration file. By default, the script looks for a file named `routes.ini`. 132 | 133 | Example `routes.ini` file: 134 | ```ini 135 | server_url=http://localhost:8080/mgnt 136 | 137 | [modules] 138 | example1.c 139 | example2.c 140 | ``` 141 | 142 | When using the .ini files you run: `./cweb deploy` 143 | 144 | ## Windows 145 | 146 | For Windows there currently only is a very primitve deploy.bat script: 147 | 148 | ```bash 149 | .\deploy.bat file.c 150 | ``` 151 | 152 | The server needs to be specified inside the .bat file, default is: http://localhost:8080/mgnt 153 | 154 | ### Errors 155 | 156 | Error messages are forwarded back to you over http. 157 | 158 | --- 159 | 160 | # Build it yourself! 161 | 162 | > **Note:** 163 | > MacOS support is not guaranteed! 164 | 165 | The project depends on: 166 | 167 | ```bash 168 | # Debian 169 | sudo apt-get install libssl-dev 170 | sudo apt-get install libsqlite3-dev 171 | sudo apt-get install libjansson-dev 172 | 173 | # Arch 174 | sudo pacman -S openssl 175 | sudo pacman -S sqlite 176 | sudo pacman -S jansson 177 | 178 | # Bug with archlinux and fanitizser, ref: https://github.com/joexbayer/c-web-modules/issues/12 179 | sudo sysctl vm.mmap_rnd_bits=30 180 | 181 | 182 | # MacOS 183 | brew install openssl@3 184 | brew install sqlite 185 | brew install jansson 186 | ``` 187 | 188 | Run make to compile and make run to start the server. 189 | 190 | ```bash 191 | make 192 | make run 193 | ``` 194 | 195 | ## Docker 196 | 197 | ```bash 198 | docker-compose up --build 199 | ``` 200 | 201 | # How is c-web-modules different from Apache Modules and ISAPI Extensions? 202 | 203 | Unlike Apache modules and ISAPI extensions, which are tightly integrated into the server and require configuration changes followed by a server restart or reload, **c-web-modules** offers runtime flexibility. 204 | 205 | **Key Differences:** 206 | - **Dynamic Deployment**: 207 | c-web-modules allows you to upload raw C code directly to the server, which compiles and integrates it into the running application without restarts. Neither Apache modules nor ISAPI extensions support this level of runtime modification. 208 | 209 | - **Module Isolation**: 210 | Each module in c-web-modules is independently managed, minimizing the risk of crashing the entire server. 211 | 212 | - **WebSocket Updates**: 213 | WebSocket handlers can be updated at runtime without breaking existing connections, a feature not typically available in Apache modules or ISAPI. 214 | 215 | This makes **c-web-modules** suitable for rapid experimentation and modular design, especially in scenarios requiring frequent updates without disrupting service. 216 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | cweb: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | restart: always 9 | ports: 10 | - "8080:8080" # Expose port 8080 11 | volumes: 12 | - ./modules:/app/modules 13 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | c-web-modules Documentation 8 | 9 | 10 | 162 | 163 | 164 | 165 |
166 | 167 | 181 | 182 | 183 |
184 |

Welcome to c-web-modules Documentation

185 | 186 |

Getting Started

187 |

c-web-modules is a proof-of-concept modular framework for web development in C. Inspired by kernel modules, it 188 | enables dynamic runtime compilation and deployment of C code directly to the server. No precompilation is 189 | required.

190 | 191 |

Server

192 |

The c-web-modules server is a simple web server that listens on port 8080. It provides the foundation for 193 | loading and executing modules at runtime. The server relies on the following libraries:

194 |
    195 |
  • openssl: For secure connections (planned) and hashing
  • 196 |
  • sqlite3: For database management
  • 197 |
  • jansson: For JSON parsing and manipulation
  • 198 |
199 |

Check the Installation section for setup details. The easiest way to start the 200 | server is by using Docker.

201 | 202 |

Modules

203 |

Modules are dynamically loaded pieces of C code that define routes and WebSocket handlers. The most important 204 | part of the module is the module_t config.

205 |

For a module to be loaded into the server, the module needs to define a module_t struct with the name config. 206 | Preferbaly with the `export` macro. The module_t config struct defines the 207 | module name, author, routes, and WebSocket handlers. It also allows handlers to call when the module is loaded 208 | and unloaded.

209 |
/* Module information */
210 | typedef struct module {
211 |     char name[128];
212 |     char author[128];
213 |     route_info_t routes[10];
214 |     int size;
215 |     websocket_info_t websockets[10];
216 |     int ws_size;
217 |     
218 |     void (*onload)(void);
219 |     void (*unload)(void);
220 | } module_t;
221 |       
222 |

Check the Examples section for sample modules.

223 | 224 |

A route is defined by a path, method, function and flags. The path needs to be unique for the server, or else 225 | the module will not be loaded.

226 |

Websockets are defined by a path, on_open, on_message and on_close. Websocket connections will not be closed 227 | when a module is reloaded or changed. This needs to be done by the user.

228 | 229 | 230 |

Deployment

231 |

The most simple way to load a module is to use curl:

232 |
curl -X POST -d @path/to/module.c http://localhost:8080/mgnt
233 | 234 |

You can also deploy modules dynamically with configuration files:

235 |

236 |  ./cweb deploy path/to/module.c
237 |  # Using a config file (routes.ini)
238 |  server_url=http://localhost:8080/mgnt
239 |  
240 |  [modules]
241 |  example1.c
242 |  example2.c
243 |        
244 | 245 |

Examples

246 | 247 |
#include <stdio.h>
248 | #include <cweb.h>
249 | 
250 | static int counter = 0;
251 | static const char* template = 
252 |     "\n"
253 |     "  \n"
254 |     "    

Counter: %d

\n" 255 | " \n" 256 | "\n"; 257 | 258 | /* Route: /counter - Method GET */ 259 | static int index_route(struct http_request *req, struct http_response *res) { 260 | snprintf(res->body, HTTP_RESPONSE_SIZE, template, counter++); 261 | res->status = HTTP_200_OK; 262 | return 0; 263 | } 264 | 265 | /* Define the routes for the module */ 266 | export module_t config = { 267 | .name = "counter", 268 | .author = "cweb", 269 | .routes = { 270 | {"/counter", "GET", index_route, NONE}, 271 | }, 272 | .size = 1, 273 | };
274 | 275 |

For more examples, check out the following links:

276 | 279 | 280 |

Installation

281 |

Install the required dependencies for Linux or MacOS:

282 |

For Debian:

283 |
    284 |
  • sudo apt-get install libssl-dev
  • 285 |
  • sudo apt-get install libsqlite3-dev
  • 286 |
  • sudo apt-get install libjansson-dev
  • 287 |
288 |

For Arch Linux:

289 |
    290 |
  • sudo pacman -S openssl
  • 291 |
  • sudo pacman -S sqlite
  • 292 |
  • sudo pacman -S jansson
  • 293 |
294 |

For MacOS:

295 |
    296 |
  • brew install openssl
  • 297 |
  • brew install sqlite3
  • 298 |
  • brew install jansson
  • 299 |
300 | 301 |

Compile and run the server:

302 |
make
303 | make run
304 | 305 |

WebSockets

306 |

WebSockets enable real-time communication between clients and the server. Here's an example module with 307 | WebSocket handlers:

308 |

309 | #include 
310 | 
311 | void on_open(struct websocket *ws) {
312 |     printf("WebSocket connection opened\n");
313 | }
314 | 
315 | void on_message(struct websocket *ws, const char *message, size_t length) {
316 |     printf("Message received: %.*s\n", (int)length, message);
317 | }
318 | 
319 | void on_close(struct websocket *ws) {
320 |     printf("WebSocket connection closed\n");
321 | }
322 | 
323 | export module_t config = {
324 |     .name = "websocket_example",
325 |     .author = "cweb",
326 |     .websockets = {
327 |         {"/ws", on_open, on_message, on_close},
328 |     },
329 |     .ws_size = 1,
330 | };
331 |       
332 | 333 |

Environments

334 |

The c-web-modules framework supports multiple environments.

335 |

By default, the development environment is used, which uses HTTP. For production, the server uses HTTPS, which 336 | requires `server.crt` and `server.key` files. They can be generated using:

337 |
openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt -days 365 -nodes
338 |

Alternatively, you can use the `production.sh` script, which will also build the server with the `PRODUCTION` 339 | flag.

340 | 341 |

Production

342 |

To build the server for production, use the production.sh script:

343 |
./production.sh
344 |

To build a Docker image for production, use the --docker flag:

345 |
./production.sh --docker
346 |

This will create a cweb:production Docker image that you can run with:

347 |
docker run  -p 8080:8080 cweb:production
348 | 349 |

Key Structs

350 |

The following structs are central to the framework:

351 |

HTTP Request

352 |

353 | struct http_request {
354 |     http_method_t method; // HTTP method (e.g., GET, POST)
355 |     char *path;           // Request path
356 |     char *body;           // Request body
357 |     int content_length;   // Length of the body
358 |     struct map *headers;  // HTTP headers
359 |     int websocket;        // Is this a WebSocket request?
360 | };
361 |       
362 |

HTTP Response

363 |

364 | struct http_response {
365 |     http_error_t status;  // HTTP status code
366 |     struct map *headers;  // Response headers
367 |     char *body;           // Response body
368 | };
369 |       
370 |

WebSocket

371 |

372 | struct websocket {
373 |     int client_fd;  // Client file descriptor
374 |     int (*send)(struct websocket* ws, const char *message, size_t length);
375 |     int (*close)(struct websocket* ws);
376 | };
377 |       
378 | 379 |

Docker

380 |

Build and run the server using Docker:

381 |
docker build -t cweb .
382 | docker run -p 8080:8080 --mount type=bind,source=$(pwd)/modules,target=/app/modules cweb
383 |

Or use Docker Compose:

384 |
docker-compose up --build
385 |

For non-volatile modules during restarts, you need to mount the modules directory:

386 |
volumes:
387 |   - ./modules:/app/modules
388 | 389 |

FAQ

390 |

Frequently asked questions about c-web-modules.

391 |

392 | Q: How do I restart the server?
393 | A: Use `make run` after making changes.
394 | 
395 | Q: How do I debug a failing module?
396 | A: Check logs in the console or the HTTP response from /mgnt.
397 |       
398 |
399 |
400 | 401 | 402 | -------------------------------------------------------------------------------- /example/auth.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | static const char* username = "admin"; 10 | static const char* password = "admin"; 11 | static const char* secret_key = "super_secret_key"; 12 | static char* hashed_password = NULL; 13 | 14 | static char* hash_password(const char* password) { 15 | unsigned char* hash = (unsigned char*)malloc(SHA256_DIGEST_LENGTH); 16 | SHA256_CTX sha256; 17 | SHA256_Init(&sha256); 18 | SHA256_Update(&sha256, password, strlen(password)); 19 | SHA256_Final(hash, &sha256); 20 | 21 | hashed_password = malloc(SHA256_DIGEST_LENGTH * 2 + 1); 22 | for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { 23 | sprintf(hashed_password + (i * 2), "%02x", hash[i]); 24 | } 25 | hashed_password[SHA256_DIGEST_LENGTH * 2] = '\0'; 26 | 27 | free(hash); 28 | return hashed_password; 29 | } 30 | 31 | static char* create_signed_cookie(const char* username) { 32 | unsigned char* hmac_result; 33 | unsigned int hmac_len; 34 | 35 | /* Create HMAC using the secret key */ 36 | hmac_result = HMAC(EVP_sha256(), secret_key, strlen(secret_key), 37 | (unsigned char*)username, strlen(username), 38 | NULL, &hmac_len); 39 | 40 | char* cookie = malloc(strlen(username) + hmac_len * 2 + 2); 41 | sprintf(cookie, "%s|", username); /* Add username to the cookie */ 42 | 43 | /* Append the HMAC signature */ 44 | for (unsigned int i = 0; i < hmac_len; i++) { 45 | sprintf(cookie + strlen(username) + 1 + i * 2, "%02x", hmac_result[i]); 46 | } 47 | 48 | return cookie; 49 | } 50 | 51 | static int verify_signed_cookie(const char* cookie) { 52 | char* separator = strchr(cookie, '|'); 53 | if (!separator) { 54 | return 0; /* Invalid cookie format */ 55 | } 56 | 57 | size_t username_len = separator - cookie; 58 | char* username = strndup(cookie, username_len); 59 | char* received_hmac = separator + 1; 60 | 61 | /* Recreate the HMAC for the username */ 62 | unsigned char* hmac_result; 63 | unsigned int hmac_len; 64 | hmac_result = HMAC(EVP_sha256(), secret_key, strlen(secret_key), 65 | (unsigned char*)username, username_len, 66 | NULL, &hmac_len); 67 | 68 | /* Convert HMAC to hex string */ 69 | char expected_hmac[hmac_len * 2 + 1]; 70 | for (unsigned int i = 0; i < hmac_len; i++) { 71 | sprintf(expected_hmac + i * 2, "%02x", hmac_result[i]); 72 | } 73 | 74 | free(username); 75 | 76 | /* Compare the received HMAC with the expected HMAC */ 77 | return strcmp(received_hmac, expected_hmac) == 0; 78 | } 79 | 80 | static int login_page(struct http_request *req, struct http_response *res) { 81 | const char* redirect_url = map_get(req->params, "redirect"); 82 | if (!redirect_url) { 83 | redirect_url = "/"; 84 | } 85 | 86 | char login_page[HTTP_RESPONSE_SIZE]; 87 | snprintf(login_page, sizeof(login_page), 88 | "\n" 89 | " \n" 90 | "
\n" 91 | " \n" 92 | "
\n" 93 | "
\n" 94 | "
\n" 95 | "

\n" 96 | " \n" 97 | "
\n" 98 | " \n" 99 | "\n", 100 | redirect_url); 101 | 102 | snprintf(res->body, HTTP_RESPONSE_SIZE, "%s", login_page); 103 | res->status = HTTP_200_OK; 104 | return 0; 105 | } 106 | 107 | static int secret(struct http_request *req, struct http_response *res) { 108 | if (req->data == NULL) { 109 | res->status = HTTP_400_BAD_REQUEST; 110 | return 0; 111 | } 112 | 113 | char* cookie = map_get(req->headers, "Cookie"); 114 | if (cookie != NULL) { 115 | char* cweb_auth_cookie = strstr(cookie, "cweb_auth="); 116 | if (cweb_auth_cookie != NULL) { 117 | cweb_auth_cookie += strlen("cweb_auth="); 118 | cookie = cweb_auth_cookie; 119 | } else { 120 | cookie = NULL; 121 | } 122 | } 123 | if (cookie == NULL || !verify_signed_cookie(cookie)) { 124 | res->status = HTTP_302_FOUND; 125 | char location[HTTP_RESPONSE_SIZE]; 126 | snprintf(location, sizeof(location), "/login?redirect=/secret"); 127 | map_insert(res->headers, "Location", location); 128 | return 0; 129 | } 130 | 131 | char* secret_page = 132 | "\n" 133 | " \n" 134 | "

Secret Page

\n" 135 | "

Welcome to the secret page!

\n" 136 | " \n" 137 | "\n"; 138 | 139 | snprintf(res->body, HTTP_RESPONSE_SIZE, "%s", secret_page); 140 | res->status = HTTP_200_OK; 141 | return 0; 142 | } 143 | 144 | static int authenticate(struct http_request *req, struct http_response *res) { 145 | if (req->data == NULL) { 146 | res->status = HTTP_400_BAD_REQUEST; 147 | return 0; 148 | } 149 | 150 | char* user = map_get(req->data, "username"); 151 | char* pass = map_get(req->data, "password"); 152 | char* redirect_url = map_get(req->data, "redirect"); 153 | if (!redirect_url) { 154 | redirect_url = "/"; 155 | } 156 | 157 | if (user == NULL || pass == NULL) { 158 | res->status = HTTP_400_BAD_REQUEST; 159 | return 0; 160 | } 161 | 162 | char* hashed = hash_password(pass); 163 | if (strcmp(user, username) != 0 || strcmp(hashed, hashed_password) != 0) { 164 | res->status = HTTP_401_UNAUTHORIZED; 165 | return 0; 166 | } 167 | 168 | const char* cookie_name = "cweb_auth="; 169 | 170 | /* Create a signed cookie */ 171 | char* cookie = create_signed_cookie(user); 172 | char* cookie_with_name = malloc(strlen(cookie_name) + strlen(cookie) + 1); 173 | sprintf(cookie_with_name, "%s%s", cookie_name, cookie); 174 | map_insert(res->headers, "Set-Cookie", cookie_with_name); 175 | free(cookie_with_name); 176 | free(cookie); 177 | 178 | res->status = HTTP_302_FOUND; 179 | map_insert(res->headers, "Location", redirect_url); 180 | 181 | return 1; 182 | } 183 | 184 | static void onload() { 185 | hashed_password = hash_password(password); 186 | } 187 | 188 | /* Define the routes for the module */ 189 | export module_t config = { 190 | .name = "auth", 191 | .author = "cweb", 192 | .routes = { 193 | {"/login", "GET", login_page, NONE}, 194 | {"/auth", "POST", authenticate, NONE}, 195 | {"/secret", "GET", secret, NONE}, 196 | }, 197 | .size = 3, 198 | }; -------------------------------------------------------------------------------- /example/chat.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define MAX_USERS 100 8 | 9 | /* User Management */ 10 | static struct websocket *users[MAX_USERS]; 11 | static int user_count = 0; 12 | static pthread_mutex_t user_mutex = PTHREAD_MUTEX_INITIALIZER; 13 | 14 | /* Add a WebSocket to the user list */ 15 | static void add_user(struct websocket *ws) { 16 | pthread_mutex_lock(&user_mutex); 17 | if (user_count < MAX_USERS) { 18 | users[user_count++] = ws; 19 | } else { 20 | printf("User limit reached. Cannot add more users.\n"); 21 | } 22 | pthread_mutex_unlock(&user_mutex); 23 | } 24 | 25 | /* Remove a WebSocket from the user list */ 26 | static void remove_user(struct websocket *ws) { 27 | pthread_mutex_lock(&user_mutex); 28 | for (int i = 0; i < user_count; i++) { 29 | if (users[i] == ws) { 30 | users[i] = users[--user_count]; 31 | break; 32 | } 33 | } 34 | pthread_mutex_unlock(&user_mutex); 35 | } 36 | 37 | /* Broadcast a message to all connected users */ 38 | static void broadcast_message(const char *message, size_t length) { 39 | pthread_mutex_lock(&user_mutex); 40 | for (int i = 0; i < user_count; i++) { 41 | users[i]->send(users[i], message, length); 42 | } 43 | pthread_mutex_unlock(&user_mutex); 44 | } 45 | 46 | /* WebSocket Handlers */ 47 | static void on_open(struct websocket *ws) { 48 | printf("WebSocket opened\n"); 49 | add_user(ws); 50 | const char *welcome = "A new user has joined the chat!"; 51 | broadcast_message(welcome, strlen(welcome)); 52 | } 53 | 54 | static void on_message(struct websocket *ws, const char *message, size_t length) { 55 | printf("Message received: %.*s\n", (int)length, message); 56 | 57 | /* Create a broadcast message */ 58 | char response[1024]; 59 | snprintf(response, sizeof(response), "User %d: %.*s", (int)(ws - users[0] + 1), (int)length, message); 60 | broadcast_message(response, strlen(response)); 61 | } 62 | 63 | static void on_close(struct websocket *ws) { 64 | printf("WebSocket closed\n"); 65 | remove_user(ws); 66 | const char *goodbye = "A user has left the chat."; 67 | broadcast_message(goodbye, strlen(goodbye)); 68 | } 69 | 70 | /* Serve the chat HTML page */ 71 | int chat_page(struct http_request *req, struct http_response *res) { 72 | const char *html = 73 | "" 74 | "" 75 | "Chat App" 76 | "" 77 | "

Simple Chat App

" 78 | "
" 79 | "" 80 | "" 81 | "" 92 | "" 93 | ""; 94 | 95 | snprintf(res->body, HTTP_RESPONSE_SIZE, "%s", html); 96 | map_insert(res->headers, "Content-Type", "text/html"); 97 | res->status = HTTP_200_OK; 98 | return 0; 99 | } 100 | 101 | void unload() { 102 | /* Close connections */ 103 | pthread_mutex_lock(&user_mutex); 104 | for (int i = 0; i < user_count; i++) { 105 | users[i]->close(users[i]); 106 | } 107 | pthread_mutex_unlock(&user_mutex); 108 | } 109 | 110 | /* Define the module */ 111 | export module_t config = { 112 | .name = "chat_app", 113 | .author = "cweb", 114 | .routes = { 115 | {"/chat", "GET", chat_page, NONE}, 116 | }, 117 | .size = 1, 118 | .websockets = { 119 | {"/chat/ws", on_open, on_message, on_close}, 120 | }, 121 | .ws_size = 1, 122 | }; 123 | -------------------------------------------------------------------------------- /example/counter.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static int counter = 0; 5 | static const char* template = 6 | "\n" 7 | " \n" 8 | "

Counter: %d

\n" 9 | " \n" 10 | "\n"; 11 | 12 | /* Route: /counter - Method GET */ 13 | static int index_route(struct http_request *req, struct http_response *res) { 14 | snprintf(res->body, HTTP_RESPONSE_SIZE, template, counter); 15 | res->status = HTTP_200_OK; 16 | 17 | defer {counter++;} 18 | 19 | return 0; 20 | } 21 | 22 | /* Define the routes for the module */ 23 | export module_t config = { 24 | .name = "counter", 25 | .author = "cweb", 26 | .routes = { 27 | {"/counter", "GET", index_route, NONE}, 28 | }, 29 | .size = 1, 30 | }; -------------------------------------------------------------------------------- /example/cweb: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DEFAULT_CONFIG_FILE="routes.ini" 3 | GREEN="\033[0;32m"; YELLOW="\033[1;33m"; RED="\033[0;31m"; BLUE="\033[1;34m"; NC="\033[0m" 4 | 5 | deploy_module() { 6 | local code="$1" server_url="$2" 7 | local response_body response_code 8 | 9 | echo -e "${BLUE}→ Deploying ${YELLOW}$code${NC} to ${YELLOW}$server_url${NC}..." 10 | 11 | # Capture both the HTTP response body and status code 12 | response_body=$(curl -s -w "%{http_code}" -o /tmp/response_body -X POST "$server_url" -F "code=@$code") 13 | response_code="${response_body:(-3)}" # Extract last 3 characters (HTTP status code) 14 | response_body=$(cat /tmp/response_body) # Read the response body from the file 15 | 16 | if [[ "$response_code" == "200" ]]; then 17 | echo -e "${GREEN}✔ Deployment succeeded${NC}\n" 18 | else 19 | echo -e "${RED}✖ Deployment failed (HTTP $response_code)${NC}\n" 20 | echo -e "${RED}Response: $response_body${NC}\n" 21 | fi 22 | } 23 | 24 | deploy_from_config() { 25 | local config_file="$1" server_url modules 26 | [[ ! -f "$config_file" ]] && echo -e "${RED}Error: Config file $config_file not found${NC}" && return 1 27 | 28 | server_url=$(awk -F= '/^server_url=/{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2; exit}' "$config_file") 29 | [[ -z "$server_url" ]] && echo -e "${RED}Error: server_url not in config${NC}" && return 1 30 | echo -e "${GREEN}🌐 Deploying to server: ${YELLOW}$server_url${NC}\n" 31 | modules=$(awk '/^\[modules\]/{flag=1; next} /^\[/{flag=0} flag && NF' "$config_file") 32 | for code in $modules; do 33 | [[ -f "$code" ]] && deploy_module "$code" "$server_url" || echo -e "${YELLOW}⚠ Skipping missing file: $code${NC}" 34 | done 35 | } 36 | 37 | main() { 38 | local command="$1" file="$2" server_url 39 | 40 | case "$command" in 41 | deploy) 42 | server_url=$(awk -F= '/^server_url=/{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2; exit}' "$DEFAULT_CONFIG_FILE") 43 | [[ -z "$server_url" ]] && echo -e "${RED}Error: server_url not in config${NC}" && return 1 44 | 45 | if [[ -n "$file" ]]; then 46 | [[ -f "$file" ]] && deploy_module "$file" "$server_url" || echo -e "${RED}Error: File $file not found${NC}" 47 | else 48 | echo -e "${GREEN}📄 Using config file: ${YELLOW}$DEFAULT_CONFIG_FILE${NC}\n" 49 | deploy_from_config "$DEFAULT_CONFIG_FILE" 50 | fi 51 | ;; 52 | *) echo -e "${YELLOW}Usage: $0 deploy [file.c]${NC}" ;; 53 | esac 54 | } 55 | 56 | main "$@" 57 | -------------------------------------------------------------------------------- /example/deploy.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | 4 | :deploy_module 5 | set "code=%~1" 6 | set "server_url=http://localhost:8080/mgnt" 7 | 8 | echo Deploying "%code%" to "%server_url%"... 9 | 10 | REM Send the file with curl and capture the response 11 | curl -X POST "%server_url%" -F "code=@%code%" -------------------------------------------------------------------------------- /example/json.c: -------------------------------------------------------------------------------- 1 | /** 2 | * @file json.c 3 | * @author Joe Bayer (joexbayer) 4 | * @brief Example program that uses jansson to serialize a list into JSON 5 | * @usage: curl http://localhost:8080/list 6 | * @usage: curl -X POST http://localhost:8080/json/add -d "item=Test Item" 7 | * @version 0.1 8 | * @date 2024-11-17 9 | * 10 | * @copyright Copyright (c) 2024 11 | * 12 | */ 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #define MAX_ITEMS 10 19 | 20 | static const char* list[MAX_ITEMS]; 21 | static int list_count = 0; 22 | 23 | /* Helper: Serialize the list into JSON */ 24 | static void serialize_list(char *buffer, size_t buffer_size) { 25 | json_t *json_arr = json_array(); 26 | for (int i = 0; i < list_count; i++) { 27 | json_array_append_new(json_arr, json_string(list[i])); 28 | } 29 | 30 | char *json_string_data = json_dumps(json_arr, JSON_COMPACT); 31 | snprintf(buffer, buffer_size, "%s", json_string_data); 32 | free(json_string_data); 33 | json_decref(json_arr); 34 | } 35 | 36 | /* Route: /list - Method GET */ 37 | int get_list_route(struct http_request *req, struct http_response *res) { 38 | char json_response[1024]; 39 | serialize_list(json_response, sizeof(json_response)); 40 | 41 | /* HTTP response */ 42 | snprintf(res->body, HTTP_RESPONSE_SIZE, "%s", json_response); 43 | map_insert(res->headers, "Content-Type", "application/json"); 44 | res->status = HTTP_200_OK; 45 | return 0; 46 | } 47 | 48 | /* Route: /json/add - Method POST */ 49 | int add_item_route(struct http_request *req, struct http_response *res) { 50 | if (list_count >= MAX_ITEMS) { 51 | json_t *error = json_pack("{s:s}", "error", "List is full"); 52 | 53 | char *error_json = json_dumps(error, JSON_COMPACT); 54 | snprintf(res->body, HTTP_RESPONSE_SIZE, "%s", error_json); 55 | map_insert(res->headers, "Content-Type", "application/json"); 56 | 57 | free(error_json); 58 | json_decref(error); 59 | res->status = HTTP_400_BAD_REQUEST; 60 | return 0; 61 | } 62 | 63 | const char *new_item = map_get(req->data, "item"); 64 | if (new_item && strlen(new_item) < 256) { 65 | list[list_count++] = strdup(new_item); /* uses malloc */ 66 | } 67 | 68 | json_t *message = json_pack("{s:s}", "message", "Item added"); 69 | char *message_json = json_dumps(message, JSON_COMPACT); 70 | snprintf(res->body, HTTP_RESPONSE_SIZE, "%s", message_json); 71 | map_insert(res->headers, "Content-Type", "application/json"); 72 | 73 | free(message_json); 74 | json_decref(message); 75 | res->status = HTTP_200_OK; 76 | return 0; 77 | } 78 | 79 | void unload() { 80 | printf("Unloading json_example_jansson %d\n", list_count); 81 | for (int i = 0; i < list_count; i++) { 82 | free((void*)list[i]); 83 | } 84 | } 85 | 86 | /* Export module */ 87 | export module_t config = { 88 | .name = "json_example_jansson", 89 | .author = "cweb", 90 | .size = 2, 91 | .routes = { 92 | {"/list", "GET", get_list_route, NONE}, 93 | {"/json/add", "POST", add_item_route, NONE}, 94 | }, 95 | .unload = unload, 96 | }; 97 | -------------------------------------------------------------------------------- /example/readme.md: -------------------------------------------------------------------------------- 1 | # c-web-modules Examples 2 | 3 | This directory contains example modules demonstrating various features and capabilities of **c-web-modules**, such as routing, WebSocket handling, and using external libraries like SQLite3 and Jansson. These examples are designed for experimentation and learning purposes. **None of them are production-ready**. 4 | 5 | --- 6 | 7 | ## Examples 8 | 9 | ### 1. **Chat Application (`chat.c`)** 10 | A simple WebSocket-based chat application that allows multiple users to connect and exchange messages in real time. 11 | - **Features**: 12 | - Broadcasts messages to all connected users. 13 | - Adds/removes users dynamically. 14 | - Provides a simple HTML chat interface. 15 | - **Endpoints**: 16 | - `GET /chat` – Serves the chat client page. 17 | - `WS /chat/ws` – Handles WebSocket connections. 18 | - **Key Concepts**: 19 | - WebSocket event handling (`on_open`, `on_message`, `on_close`). 20 | - Multi-user management with thread-safe operations. 21 | 22 | --- 23 | 24 | ### 2. **Counter Module (`counter.c`)** 25 | A minimal example that tracks a counter and displays it in an HTML page. 26 | - **Features**: 27 | - Increments a counter with every request. 28 | - **Endpoints**: 29 | - `GET /counter` – Displays the current counter value. 30 | - **Key Concepts**: 31 | - Simple routing and dynamic content generation. 32 | 33 | --- 34 | 35 | ### 3. **JSON Example (`json.c`)** 36 | An example showcasing how to use the Jansson library to handle JSON data. 37 | - **Features**: 38 | - Maintains a server-side list of items. 39 | - Allows adding new items via `POST` and retrieving the list as JSON. 40 | - **Endpoints**: 41 | - `GET /list` – Returns the list in JSON format. 42 | - `POST /json/add` – Adds a new item to the list. 43 | - **Key Concepts**: 44 | - Serializing and deserializing JSON. 45 | - Handling POST data. 46 | 47 | --- 48 | 49 | ### 4. **TODO List (`todo.c`)** 50 | A simple TODO list manager with an HTML front-end using Bootstrap. 51 | - **Features**: 52 | - Displays a list of tasks. 53 | - Allows adding tasks via a form. 54 | - **Endpoints**: 55 | - `GET /` – Serves the TODO list page. 56 | - `POST /add` – Adds a new task to the list. 57 | - **Key Concepts**: 58 | - HTML rendering with dynamic content. 59 | - Handling form submissions and redirects. 60 | 61 | --- 62 | 63 | ### 5. **WebSocket Echo (`websocket.c`)** 64 | A basic WebSocket module that echoes messages back to the sender. 65 | - **Features**: 66 | - Receives a message and responds with "You: [message]". 67 | - **Endpoints**: 68 | - `WS /websocket` – Handles WebSocket connections. 69 | - **Key Concepts**: 70 | - WebSocket communication basics. 71 | 72 | --- 73 | -------------------------------------------------------------------------------- /example/routes.ini: -------------------------------------------------------------------------------- 1 | # Example configuration file 2 | 3 | ## Server 4 | server_url=http://localhost:8080/mgnt 5 | 6 | [modules] 7 | websocket.c 8 | counter.c 9 | todo.c 10 | json.c 11 | chat.c 12 | static.c 13 | -------------------------------------------------------------------------------- /example/static.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static int security_check(const char *path) { 5 | if (strstr(path, "..") != NULL) { 6 | return 0; 7 | } 8 | return 1; 9 | } 10 | 11 | static int read_file(const char *path, char *body, int size) { 12 | FILE *fp = fopen(path, "rb"); 13 | if (fp == NULL) { 14 | return -1; 15 | } 16 | 17 | int ret = fread(body, 1, size, fp); 18 | if (ret < 0) { 19 | fclose(fp); 20 | return -1; 21 | } 22 | 23 | fclose(fp); 24 | return ret; 25 | } 26 | 27 | static void set_content_type(struct http_response *res, const char *path) { 28 | char *ext = strrchr(path, '.'); 29 | if (ext) { 30 | if (strcmp(ext, ".html") == 0) { 31 | map_insert(res->headers, "Content-Type", "text/html"); 32 | } else if (strcmp(ext, ".css") == 0) { 33 | map_insert(res->headers, "Content-Type", "text/css"); 34 | } else if (strcmp(ext, ".js") == 0) { 35 | map_insert(res->headers, "Content-Type", "application/javascript"); 36 | } else { 37 | map_insert(res->headers, "Content-Type", "text/plain"); 38 | } 39 | } else { 40 | map_insert(res->headers, "Content-Type", "text/plain"); 41 | } 42 | } 43 | 44 | static int download(struct http_request *req, struct http_response *res) { 45 | int ret; 46 | 47 | /* Copy path */ 48 | char path[256] = {0}; 49 | snprintf(path, sizeof(path), "%s", req->path+1); 50 | 51 | /* Security check on path */ 52 | if (!security_check(path)) { 53 | res->status = HTTP_403_FORBIDDEN; 54 | return 0; 55 | } 56 | 57 | /* Read file */ 58 | ret = read_file(path, res->body, HTTP_RESPONSE_SIZE); 59 | if (ret < 0) { 60 | printf("File not found\n"); 61 | res->status = HTTP_404_NOT_FOUND; 62 | return 0; 63 | } 64 | 65 | /* Set content options */ 66 | res->content_length = ret; 67 | set_content_type(res, req->path); 68 | 69 | res->status = HTTP_200_OK; 70 | return 0; 71 | } 72 | 73 | /* Define the routes for the module */ 74 | export module_t config = { 75 | .name = "static", 76 | .author = "cweb", 77 | .routes = { 78 | /* Allows regex in route paths */ 79 | {"/static/.*", "GET", download, NONE}, 80 | }, 81 | .size = 1, 82 | }; -------------------------------------------------------------------------------- /example/todo.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | #define MAX_ITEMS 10 8 | 9 | static const char* list[MAX_ITEMS]; 10 | static int list_count = 0; 11 | 12 | const char* head = 13 | "CWeb\n" 14 | "\n"; 15 | 16 | const char* todo_template = 17 | "
\n" 18 | "

My TODO List

\n" 19 | "
    \n" 20 | " %s\n" // Placeholder for list items 21 | "
\n" 22 | "
\n" 23 | "
\n" 24 | " \n" 25 | "
\n" 26 | " \n" 27 | "
\n" 28 | "
\n"; 29 | 30 | const char* home_template = 31 | "\n" 32 | " \n" 33 | " %s\n" // Placeholder for head 34 | " \n" 35 | " \n" 36 | " %s\n" // Placeholder for content 37 | " \n" 38 | "\n"; 39 | 40 | /* Helper */ 41 | static void render_todo_list(char *buffer, size_t buffer_size) { 42 | char items_buffer[1024] = ""; 43 | for (int i = 0; i < list_count; i++) { 44 | char item[256]; 45 | snprintf(item, sizeof(item), "
  • %s
  • \n", list[i]); 46 | strncat(items_buffer, item, sizeof(items_buffer) - strlen(items_buffer) - 1); 47 | } 48 | snprintf(buffer, buffer_size, todo_template, items_buffer); 49 | } 50 | 51 | /* Route: / - Method GET */ 52 | int index_route(struct http_request *req, struct http_response *res) { 53 | char content[2048]; 54 | char rendered_page[4096]; 55 | 56 | render_todo_list(content, sizeof(content)); 57 | snprintf(rendered_page, sizeof(rendered_page), home_template, head, content); 58 | snprintf(res->body, HTTP_RESPONSE_SIZE, "%s", rendered_page); 59 | 60 | map_insert(res->headers, "Content-Type", "text/html"); 61 | map_insert(res->headers, "x-custom-header", "Hello, World!"); 62 | 63 | res->status = HTTP_200_OK; 64 | return 0; 65 | } 66 | 67 | /* Route: /add - Method POST */ 68 | int add_todo_route(struct http_request *req, struct http_response *res) { 69 | if (list_count >= MAX_ITEMS) { 70 | snprintf(res->body, HTTP_RESPONSE_SIZE, "TODO list is full."); 71 | res->status = HTTP_400_BAD_REQUEST; 72 | return 0; 73 | } 74 | 75 | const char *new_item = map_get(req->data, "item"); 76 | if (new_item && strlen(new_item) < 256) { 77 | list[list_count++] = strdup(new_item); // Add to the TODO list 78 | } 79 | 80 | res->status = HTTP_302_FOUND; 81 | map_insert(res->headers, "Location", "/"); 82 | return 0; 83 | } 84 | 85 | void unload() { 86 | for (int i = 0; i < list_count; i++) { 87 | free((void*)list[i]); 88 | } 89 | } 90 | 91 | export module_t config = { 92 | .name = "todo", 93 | .author = "cweb", 94 | .size = 2, 95 | .routes = { 96 | {"/", "GET", index_route, NONE}, 97 | {"/add", "POST", add_todo_route, NONE}, 98 | }, 99 | .unload = unload, 100 | }; -------------------------------------------------------------------------------- /example/websocket.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static void on_open(struct websocket *ws) { 5 | printf("WebSocket opened\n"); 6 | } 7 | 8 | static void on_message(struct websocket *ws, const char *message, size_t length) { 9 | char response[1024]; 10 | snprintf(response, sizeof(response), "You: %s", message); 11 | ws->send(ws, response, strlen(response)); 12 | } 13 | 14 | static void on_close(struct websocket *ws) { 15 | printf("WebSocket closed\n"); 16 | } 17 | 18 | /* Define the routes for the module */ 19 | export module_t config = { 20 | .name = "websocket", 21 | .author = "cweb", 22 | .websockets = { 23 | {"/websocket", on_open, on_message, on_close}, 24 | }, 25 | .ws_size = 1, 26 | }; -------------------------------------------------------------------------------- /example/webui.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | struct html_builder { 9 | char *buffer; 10 | size_t size; 11 | size_t capacity; 12 | }; 13 | 14 | static void html_builder_init(struct html_builder *builder) { 15 | builder->buffer = malloc(1024); 16 | builder->size = 0; 17 | builder->capacity = 1024; 18 | } 19 | 20 | static void html_append(struct html_builder *builder, const char *str) { 21 | size_t len = strlen(str); 22 | if (builder->size + len >= builder->capacity) { 23 | builder->capacity *= 2; 24 | builder->buffer = realloc(builder->buffer, builder->capacity); 25 | } 26 | memcpy(builder->buffer + builder->size, str, len); 27 | builder->size += len; 28 | } 29 | 30 | static void html_builder_free(struct html_builder *builder) { 31 | free(builder->buffer); 32 | } 33 | 34 | static void html_append_format(struct html_builder *builder, const char *format, ...) { 35 | va_list args; 36 | va_start(args, format); 37 | 38 | size_t available = builder->capacity - builder->size; 39 | int needed = vsnprintf(builder->buffer + builder->size, available, format, args); 40 | 41 | if (needed >= available) { 42 | builder->capacity += needed + 1; 43 | builder->buffer = realloc(builder->buffer, builder->capacity); 44 | vsnprintf(builder->buffer + builder->size, needed + 1, format, args); 45 | } 46 | 47 | builder->size += needed; 48 | va_end(args); 49 | } 50 | 51 | static void html_tag(struct html_builder *builder, const char *tag, const char *format, ...) { 52 | va_list args; 53 | va_start(args, format); 54 | 55 | char content[1024]; 56 | vsnprintf(content, sizeof(content), format, args); 57 | 58 | html_append_format(builder, "<%s>%s", tag, content, tag); 59 | 60 | va_end(args); 61 | } 62 | 63 | static int index_route(struct http_request *req, struct http_response *res) { 64 | 65 | struct html_builder builder; 66 | html_builder_init(&builder); 67 | 68 | html_append(&builder, "

    Modules

    "); 69 | 70 | /* Get modules from database */ 71 | const char *sql = "SELECT * FROM module"; 72 | sqlite3_stmt *stmt; 73 | if (database->prepare(database->db, sql, -1, &stmt, NULL) != SQLITE_OK) { 74 | fprintf(stderr, "Failed to prepare statement\n"); 75 | return -1; 76 | } 77 | 78 | while (database->step(stmt) == SQLITE_ROW) { 79 | const char *author = database->column_text(stmt, 0); 80 | const char *name = database->column_text(stmt, 1); 81 | const char *code = database->column_text(stmt, 2); 82 | 83 | html_tag(&builder, "li", "%s - %s", author, name); 84 | html_tag(&builder, "pre", "%s", code); 85 | } 86 | 87 | database->finalize(stmt); 88 | 89 | html_append(&builder, 90 | "
    " 91 | "
    " 92 | "
    " 93 | "
    " 94 | "
    " 95 | "
    " 96 | "
    " 97 | "" 98 | "
    " 99 | ); 100 | 101 | html_append(&builder, 102 | "
    " 103 | "" 104 | "
    " 105 | ); 106 | 107 | snprintf(res->body, HTTP_RESPONSE_SIZE, "%.*s", (int)builder.size, builder.buffer); 108 | 109 | html_builder_free(&builder); 110 | 111 | res->status = HTTP_200_OK; 112 | return 0; 113 | } 114 | 115 | static int clear(struct http_request *req, struct http_response *res) { 116 | const char *sql = "DELETE FROM module"; 117 | if (database->exec(sql, NULL, NULL) != SQLITE_OK) { 118 | fprintf(stderr, "Failed to clear table\n"); 119 | return -1; 120 | } 121 | 122 | res->status = HTTP_302_FOUND; 123 | map_insert(res->headers, "Location", "/webui"); 124 | return 0; 125 | } 126 | 127 | static int add(struct http_request *req, struct http_response *res) { 128 | const char *author = map_get(req->data, "author"); 129 | const char *name = map_get(req->data, "name"); 130 | const char *code = map_get(req->data, "code"); 131 | 132 | printf("Adding module: %s - %s\n", author, name); 133 | printf("Code: %s\n", code); 134 | 135 | const char *sql = "INSERT INTO module (author, name, code) VALUES (?, ?, ?)"; 136 | sqlite3_stmt *stmt; 137 | if (database->prepare(database->db, sql, -1, &stmt, NULL) != SQLITE_OK) { 138 | fprintf(stderr, "Failed to prepare statement\n"); 139 | return -1; 140 | } 141 | 142 | if (database->bind_text(stmt, 1, author, -1, NULL) != SQLITE_OK || 143 | database->bind_text(stmt, 2, name, -1, NULL) != SQLITE_OK || 144 | database->bind_text(stmt, 3, code, -1, NULL) != SQLITE_OK) { 145 | fprintf(stderr, "Failed to bind parameters\n"); 146 | return -1; 147 | } 148 | 149 | if (database->step(stmt) != SQLITE_DONE) { 150 | fprintf(stderr, "Failed to execute statement\n"); 151 | return -1; 152 | } 153 | 154 | database->finalize(stmt); 155 | 156 | res->status = HTTP_302_FOUND; 157 | map_insert(res->headers, "Location", "/webui"); 158 | return 0; 159 | } 160 | 161 | static void onload(){ 162 | printf("[WEBUI] Loaded.\n"); 163 | 164 | const char *create_table_sql = "CREATE TABLE IF NOT EXISTS module (author TEXT, name TEXT, code TEXT);"; 165 | if (database->exec(create_table_sql, NULL, NULL) != SQLITE_OK) { 166 | fprintf(stderr, "Failed to create table\n"); 167 | return; 168 | } 169 | } 170 | 171 | static void unload(){ 172 | printf("[WEBUI] Unloaded.\n"); 173 | } 174 | 175 | export module_t config = { 176 | .name = "webui", 177 | .author = "joebayer", 178 | .routes = { 179 | {"/webui", "GET", index_route, NONE}, 180 | {"/webui", "POST", add, NONE}, 181 | {"/clear", "POST", clear, NONE}, 182 | }, 183 | .size = 3, 184 | .onload = onload, 185 | .unload = unload, 186 | }; 187 | -------------------------------------------------------------------------------- /example/ws.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WebSocket Test 8 | 9 | 10 | 11 |

    WebSocket Test Page

    12 | 13 | 14 | 15 |

    16 | 17 | 18 | 19 | 20 |

    21 | 22 |
    23 | Log: 24 |
    25 |
    26 |
    27 | 28 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /include/container.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTAINER_H 2 | #define CONTAINER_H 3 | 4 | #include 5 | #include 6 | 7 | /* Container key value structure */ 8 | struct container { 9 | int (*set)(const char *name, void* value); 10 | void* (*get)(const char *name); 11 | struct map *data; 12 | }; 13 | 14 | extern struct container* exposed_container; 15 | 16 | #endif // CONTAINER_H -------------------------------------------------------------------------------- /include/crypto.h: -------------------------------------------------------------------------------- 1 | #ifndef CRYPTO_H 2 | #define CRYPTO_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | extern SSL_CTX *ssl_ctx; /* Global SSL context */ 13 | 14 | typedef enum crypto_status { 15 | CRYPTO_FAILURE, 16 | CRYPTO_LOADED 17 | } crypto_status_t; 18 | 19 | /** 20 | * Main goal is to expose functions to allow signing and verifying tokens, 21 | * using the main server certificate. 22 | */ 23 | struct crypto_ops { 24 | /** 25 | * Sign a token using the server certificate. 26 | * @param token Token to sign 27 | * @param signed_token Buffer to store the signed token 28 | * @param signed_token_len Length of the signed token buffer 29 | * @return 0 on success, -1 on failure 30 | */ 31 | int (*sign_token)(const char *token, char *signed_token, unsigned int signed_token_len); 32 | 33 | /** 34 | * Verify a signed token using the server certificate. 35 | * @param signed_token Signed token to verify 36 | * @param token Buffer to store the token 37 | * @param token_len Length of the token buffer 38 | * @return 0 on success, -1 on failure 39 | */ 40 | int (*verify_token)(const char *signed_token, char *token, size_t token_len); 41 | }; 42 | 43 | struct crypto { 44 | SSL* ctx; 45 | crypto_status_t status; 46 | struct crypto_ops *ops; 47 | }; 48 | 49 | extern struct crypto* crypto_module; 50 | 51 | /** 52 | * Initialize the crypto module. 53 | * @return 0 on success, -1 on failure 54 | */ 55 | int crypto_init(); 56 | 57 | #endif // CRYPTO_H -------------------------------------------------------------------------------- /include/cweb.h: -------------------------------------------------------------------------------- 1 | #ifndef CWEB_H 2 | #define CWEB_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | /* Third party json parser */ 12 | #include 13 | 14 | /* Macro to export a module_t configuration */ 15 | #define export __attribute__((visibility("default"))) const 16 | 17 | typedef int (*entry_t)(struct http_request *, struct http_response *); 18 | typedef enum { 19 | NONE = 0, 20 | } cweb_feature_flag_t; 21 | 22 | /* Websocket information */ 23 | typedef struct ws_info { 24 | const char *path; 25 | void (*on_open)(struct websocket *); 26 | void (*on_message)(struct websocket *, const char *message, size_t length); 27 | void (*on_close)(struct websocket *); 28 | } websocket_info_t; 29 | 30 | /* Route information */ 31 | typedef struct route_info { 32 | const char *path; 33 | const char *method; 34 | entry_t handler; 35 | int flags; 36 | } route_info_t; 37 | 38 | /* Module information */ 39 | typedef struct module { 40 | char name[128]; 41 | char author[128]; 42 | route_info_t routes[10]; 43 | int size; 44 | websocket_info_t websockets[10]; 45 | int ws_size; 46 | 47 | void (*onload)(void); 48 | void (*unload)(void); 49 | } module_t; 50 | 51 | struct symbols { 52 | void* (*resolv)(const char* module, const char* symbol); 53 | }; 54 | 55 | /* Exposed primitives */ 56 | extern struct container* cache; 57 | extern struct scheduler* scheduler; 58 | extern struct sqldb* database; 59 | extern struct symbols* symbols; 60 | // KeyValue store 61 | // Queues 62 | // Authentication/Sessions. 63 | // Config 64 | // Logging 65 | 66 | #endif // CWEB_H -------------------------------------------------------------------------------- /include/db.h: -------------------------------------------------------------------------------- 1 | #ifndef DB_H 2 | #define DB_H 3 | 4 | #include 5 | 6 | struct sqldb { 7 | sqlite3 *db; 8 | int (*exec)(const char *, int (*)(void *, int, char **, char **), void *); 9 | int (*prepare)(sqlite3 *, const char *, int, sqlite3_stmt **, const char **); 10 | int (*step)(sqlite3_stmt *); 11 | int (*finalize)(sqlite3_stmt *); 12 | int (*bind_text)(sqlite3_stmt *, int, const char *, int, void (*)(void *)); 13 | int (*bind_int)(sqlite3_stmt *, int, int); 14 | const char* (*column_text)(sqlite3_stmt *, int); 15 | int (*column_int)(sqlite3_stmt *, int); 16 | int (*column_count)(sqlite3_stmt *); 17 | int (*reset)(sqlite3_stmt *); 18 | int (*changes)(sqlite3 *); 19 | int (*last_insert_rowid)(sqlite3 *); 20 | void (*free)(void *); 21 | 22 | }; 23 | extern struct sqldb *exposed_sqldb; 24 | 25 | #endif // DB_H -------------------------------------------------------------------------------- /include/defer.h: -------------------------------------------------------------------------------- 1 | #ifndef __DEFER_H 2 | #define __DEFER_H 3 | 4 | #define __DEFER__(F, V) \ 5 | auto inline __attribute__((always_inline)) void F(int*); \ 6 | __attribute__((cleanup(F))) int V; \ 7 | inline __attribute__((always_inline)) void F(int*) 8 | #define __DEFER_(N) __DEFER__(__DEFER_FUNCTION_ ## N, __DEFER_VARIABLE_ ## N) 9 | #define __DEFER(N) __DEFER_(N) 10 | #define defer __DEFER(__COUNTER__) 11 | 12 | #endif -------------------------------------------------------------------------------- /include/http.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTP_H 2 | #define HTTP_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define HTTP_VERSION "HTTP/1.1" 11 | #define HTTP_RESPONSE_SIZE 8*1024 /* 8KB */ 12 | 13 | typedef enum http_method { 14 | HTTP_ERR = -1, 15 | HTTP_GET, 16 | HTTP_POST, 17 | HTTP_PUT, 18 | HTTP_DELETE, 19 | } http_method_t; 20 | extern const char *http_methods[]; 21 | 22 | typedef enum http_error { 23 | HTTP_000_UNKNOWN, 24 | HTTP_101_SWITCHING_PROTOCOLS, 25 | HTTP_200_OK, 26 | HTTP_302_FOUND, 27 | HTTP_400_BAD_REQUEST, 28 | HTTP_401_UNAUTHORIZED, 29 | HTTP_403_FORBIDDEN, 30 | HTTP_404_NOT_FOUND, 31 | HTTP_405_METHOD_NOT_ALLOWED, 32 | HTTP_414_URI_TOO_LONG, 33 | HTTP_500_INTERNAL_SERVER_ERROR 34 | } http_error_t; 35 | extern const char *http_errors[]; 36 | 37 | struct http_request { 38 | http_method_t method; 39 | http_error_t status; 40 | char *path; 41 | char *body; 42 | int content_length; 43 | char keep_alive; 44 | char close; 45 | pthread_t tid; 46 | struct map *params; 47 | struct map *headers; 48 | struct map *data; 49 | 50 | int websocket; 51 | }; 52 | 53 | struct http_response { 54 | http_error_t status; 55 | struct map *headers; 56 | char *body; 57 | int content_length; 58 | }; 59 | 60 | struct websocket { 61 | char* session; 62 | int client_fd; 63 | int (*send)(struct websocket* ws, const char *message, size_t length); 64 | int (*close)(struct websocket* ws); 65 | }; 66 | 67 | int http_parse(const char *request, struct http_request *req); 68 | int http_parse_data(struct http_request *req); 69 | int http_is_websocket_upgrade(struct http_request *req); 70 | 71 | #endif // HTTP_H -------------------------------------------------------------------------------- /include/libevent.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBEVENT_H 2 | #define LIBEVENT_H 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include 9 | 10 | struct event; 11 | typedef void (*event_callback_t)(struct event *event, void *arg); 12 | 13 | struct event { 14 | int fd; 15 | short events; 16 | event_callback_t callback; 17 | void *arg; 18 | }; 19 | 20 | struct event *event_new(int fd, short events, event_callback_t callback, void *arg); 21 | void event_free(struct event *event); 22 | int event_add(struct event *event); 23 | int event_del(struct event *event); 24 | void event_dispatch(void); 25 | void event_dispatch_stop(void); 26 | 27 | #ifdef __cplusplus 28 | } 29 | #endif 30 | 31 | #endif // LIBEVENT_H -------------------------------------------------------------------------------- /include/list.h: -------------------------------------------------------------------------------- 1 | #ifndef LIST_H 2 | #define LIST_H 3 | 4 | #include 5 | 6 | /* Node structure */ 7 | struct list_node { 8 | void *data; 9 | struct list_node *next; 10 | }; 11 | 12 | /* List structure */ 13 | struct list { 14 | struct list_node *head; 15 | size_t size; 16 | }; 17 | 18 | #define LIST_FOREACH(list, node) \ 19 | for (struct list_node *node = list->head; node != NULL; node = node->next) 20 | 21 | #define LIST_FOREACH_SAFE(list, node, tmp) \ 22 | for (struct list_node *node = list->head, *tmp = NULL; node != NULL && (tmp = node->next); node = tmp) 23 | 24 | /* Initialize a new list */ 25 | struct list *list_create(void); 26 | void list_destroy(struct list *list); 27 | void list_add(struct list *list, void *data); 28 | int list_remove(struct list *list, void *data); 29 | void list_iterate(struct list *list, void (*func)(void *data)); 30 | 31 | #endif /* LIST_H */ -------------------------------------------------------------------------------- /include/map.h: -------------------------------------------------------------------------------- 1 | #ifndef MAP_H 2 | #define MAP_H 3 | 4 | #include 5 | #include 6 | 7 | typedef enum map_error { 8 | MAP_OK = 0, 9 | MAP_ERR = 1, 10 | MAP_FULL = 2, 11 | MAP_KEY_NOT_FOUND = 3, 12 | } map_error_t; 13 | 14 | struct map { 15 | struct map_entry { 16 | char *key; 17 | void *value; 18 | } *entries; 19 | size_t size; 20 | size_t capacity; 21 | }; 22 | 23 | struct map *map_create(size_t initial_capacity); 24 | void map_destroy(struct map *map); 25 | int map_insert(struct map *map, const char *key, void *value); 26 | void *map_get(const struct map *map, const char *key); 27 | int map_remove(struct map *map, const char *key); 28 | size_t map_size(const struct map *map); 29 | 30 | #endif // MAP_H -------------------------------------------------------------------------------- /include/pool.h: -------------------------------------------------------------------------------- 1 | #ifndef POOL_H 2 | #define POOL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | /* Task structure */ 12 | struct task { 13 | void (*function)(void *); 14 | void *arg; 15 | struct task *next; 16 | }; 17 | 18 | /* Thread pool structure */ 19 | struct thread_pool { 20 | pthread_mutex_t lock; 21 | pthread_cond_t cond; 22 | pthread_t *threads; 23 | int *thread_active; 24 | struct task *task_queue; 25 | atomic_int num_threads; 26 | int max_threads; 27 | int queue_length; 28 | volatile atomic_int stop; 29 | atomic_int active_threads; /* Number of threads actively processing */ 30 | }; 31 | 32 | struct thread_pool *thread_pool_init(int num_threads); 33 | void thread_pool_add_task(struct thread_pool *pool, void (*function)(void *), void *arg); 34 | void thread_pool_destroy(struct thread_pool *pool); 35 | int thread_pool_is_full(struct thread_pool *pool); 36 | 37 | #endif /* POOL_H */ 38 | -------------------------------------------------------------------------------- /include/queue.h: -------------------------------------------------------------------------------- 1 | #ifndef QUEUE_H 2 | #define QUEUE_H 3 | 4 | #include 5 | #include 6 | 7 | typedef enum queue_status { 8 | QUEUE_OK, 9 | QUEUE_FULL, 10 | QUEUE_EMPTY, 11 | QUEUE_ERROR 12 | } queue_status_t; 13 | 14 | struct queue { 15 | void **buffer; 16 | size_t head; 17 | size_t tail; 18 | size_t max_size; 19 | size_t current_size; 20 | pthread_mutex_t lock; 21 | pthread_cond_t not_full; 22 | pthread_cond_t not_empty; 23 | }; 24 | 25 | struct queue* queue_create(size_t max_size); 26 | void queue_destroy(struct queue* q); 27 | queue_status_t queue_enqueue(struct queue* q, void* item); 28 | queue_status_t queue_dequeue(struct queue* q, void** item); 29 | size_t queue_size(struct queue* q); 30 | 31 | 32 | 33 | #endif // QUEUE_H -------------------------------------------------------------------------------- /include/router.h: -------------------------------------------------------------------------------- 1 | #ifndef ROUTER_H 2 | #define ROUTER_H 3 | 4 | // Include necessary standard libraries 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define SO_PATH_MAX_LEN 256 11 | 12 | typedef int (*handler_t)(struct http_request *, struct http_response *); 13 | struct gateway_entry { 14 | void *handle; 15 | char so_path[SO_PATH_MAX_LEN]; 16 | struct module *module; 17 | pthread_rwlock_t rwlock; 18 | }; 19 | 20 | struct route { 21 | struct route_info *route; 22 | pthread_rwlock_t* rwlock; 23 | }; 24 | 25 | struct ws_route { 26 | struct ws_info *info; 27 | pthread_rwlock_t* rwlock; 28 | }; 29 | 30 | int route_register_module(char* so_path); 31 | struct route route_find(char *route, char *method); 32 | struct ws_route ws_route_find(char *route); 33 | int route_gateway_json(struct http_response* res); 34 | 35 | void* resolv(const char* module, const char* symbol); 36 | 37 | /* TODO: Move... */ 38 | int mgnt_parse_request(struct http_request *req, struct http_response *res); 39 | void safe_execute_handler(handler_t handler, struct http_request *req, struct http_response *res); 40 | 41 | #define dbgprint(fmt, ...) \ 42 | do { fprintf(stderr, fmt, __VA_ARGS__); } while (0) 43 | 44 | 45 | #endif // ROUTER_H -------------------------------------------------------------------------------- /include/scheduler.h: -------------------------------------------------------------------------------- 1 | #ifndef SCHEDULER_H 2 | #define SCHEDULER_H 3 | 4 | #include 5 | 6 | typedef void (*work_t)(void *); 7 | typedef enum { 8 | ASYNC, 9 | SYNC 10 | } worker_state_t; 11 | 12 | struct work { 13 | work_t work; 14 | void *data; 15 | struct work *next; 16 | }; 17 | 18 | struct scheduler { 19 | struct work *queue; 20 | int size; 21 | int capacity; 22 | 23 | pthread_mutex_t mutex; 24 | 25 | int (*add)(void (*work)(void *), void *data, worker_state_t state); 26 | }; 27 | extern struct scheduler *exposed_scheduler; 28 | 29 | #endif // SCHEDULER_H -------------------------------------------------------------------------------- /libs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joexbayer/c-web-modules/52ff9fa441b1691510b44e58462c804eec7d74c1/libs/.keep -------------------------------------------------------------------------------- /libs/libevent.c: -------------------------------------------------------------------------------- 1 | /** 2 | * @file libevent.c 3 | * @author Joe Bayer (joexbayer) 4 | * @brief A simple event library for Linux and MacOS 5 | * handling epoll and kqueue for network socket events. 6 | * @version 0.1 7 | * @date 2024-11-16 8 | * 9 | * @copyright Copyright (c) 2024 10 | * 11 | */ 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #ifdef __APPLE__ 23 | #include 24 | #include 25 | #elif __linux__ 26 | #include 27 | #include 28 | #endif 29 | 30 | #include "libevent.h" 31 | 32 | #define DEBUG_PRINT(fmt, args...) printf(fmt, ## args) 33 | #undef DEBUG_PRINT 34 | #define DEBUG_PRINT(fmt, args...) 35 | 36 | static int stop_flag = 0; 37 | static pthread_mutex_t stop_mutex = PTHREAD_MUTEX_INITIALIZER; 38 | static pthread_mutex_t event_mutex = PTHREAD_MUTEX_INITIALIZER; 39 | static void lock() { 40 | pthread_mutex_lock(&event_mutex); 41 | } 42 | static void unlock() { 43 | pthread_mutex_unlock(&event_mutex); 44 | } 45 | 46 | #ifdef __APPLE__ 47 | #define MAX_EVENTS 64 48 | static int kq = -1; 49 | #elif __linux__ 50 | #define MAX_EVENTS 64 51 | static int epoll_fd = -1; 52 | #endif 53 | 54 | /* Simple linked list to manage events */ 55 | struct event_list { 56 | struct event *ev; 57 | struct event_list *next; 58 | }; 59 | 60 | static struct event_list *events = NULL; 61 | 62 | #ifdef __linux__ 63 | static int notify_pipe[2] = {-1, -1}; 64 | static pthread_once_t epoll_init_once = PTHREAD_ONCE_INIT; 65 | 66 | void initialize_epoll(void) { 67 | epoll_fd = epoll_create1(0); 68 | if (epoll_fd == -1) { 69 | perror("epoll creation failed"); 70 | exit(EXIT_FAILURE); 71 | } 72 | 73 | /* Create a self pipe used to wakeup epoll on demand */ 74 | if (pipe(notify_pipe) == -1) { 75 | perror("pipe creation failed"); 76 | exit(EXIT_FAILURE); 77 | } 78 | 79 | struct epoll_event ep; 80 | ep.events = EPOLLIN; 81 | ep.data.fd = notify_pipe[0]; 82 | if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, notify_pipe[0], &ep) == -1) { 83 | perror("epoll_ctl failed for pipe"); 84 | exit(EXIT_FAILURE); 85 | } 86 | 87 | DEBUG_PRINT("epoll_fd initialized: %d\n", epoll_fd); 88 | } 89 | 90 | #endif 91 | 92 | #ifdef __APPLE__ 93 | static pthread_once_t kqueue_init_once = PTHREAD_ONCE_INIT; 94 | 95 | void initialize_kqueue(void) { 96 | kq = kqueue(); 97 | if (kq == -1) { 98 | perror("kqueue creation failed"); 99 | exit(EXIT_FAILURE); 100 | } 101 | 102 | DEBUG_PRINT("kqueue initialized: %d\n", kq); 103 | } 104 | #endif 105 | 106 | /* Create a new event */ 107 | struct event *event_new(int fd, short events, event_callback_t callback, void *arg) { 108 | lock(); 109 | struct event *ev = malloc(sizeof(struct event)); 110 | if (!ev) { 111 | perror("Failed to allocate memory for event"); 112 | unlock(); 113 | return NULL; 114 | } 115 | ev->fd = fd; 116 | ev->events = events; 117 | ev->callback = callback; 118 | ev->arg = arg; 119 | 120 | unlock(); 121 | return ev; 122 | } 123 | 124 | /* Free an event */ 125 | void event_free(struct event *event) { 126 | if (event) { 127 | free(event); 128 | } 129 | } 130 | 131 | /* Add an event to the event loop */ 132 | int event_add(struct event *ev) { 133 | if (!ev) return -1; 134 | 135 | lock(); 136 | 137 | #ifdef __APPLE__ 138 | pthread_once(&kqueue_init_once, initialize_kqueue); 139 | 140 | struct kevent ke; 141 | EV_SET(&ke, ev->fd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, ev); 142 | if (kevent(kq, &ke, 1, NULL, 0, NULL) == -1) { 143 | perror("kevent add failed"); 144 | unlock(); 145 | return -1; 146 | } 147 | #elif __linux__ 148 | pthread_once(&epoll_init_once, initialize_epoll); 149 | 150 | struct epoll_event ep; 151 | ep.events = EPOLLIN | EPOLLET; /* Edge-triggered */ 152 | ep.data.ptr = ev; 153 | if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, ev->fd, &ep) == -1) { 154 | perror("epoll_ctl add failed"); 155 | unlock(); 156 | return -1; 157 | } 158 | #endif 159 | 160 | /* Add to the internal list */ 161 | struct event_list *new_node = malloc(sizeof(struct event_list)); 162 | if (!new_node) { 163 | perror("Failed to allocate memory for event_list"); 164 | unlock(); 165 | return -1; 166 | } 167 | new_node->ev = ev; 168 | new_node->next = events; 169 | events = new_node; 170 | 171 | unlock(); /* Unlock after modification */ 172 | return 0; 173 | } 174 | 175 | /* Remove an event from the event loop */ 176 | int event_del(struct event *ev) { 177 | if (!ev) return -1; 178 | 179 | lock(); 180 | 181 | #ifdef __APPLE__ 182 | struct kevent ke; 183 | EV_SET(&ke, ev->fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); 184 | if (kevent(kq, &ke, 1, NULL, 0, NULL) == -1) { 185 | perror("kevent delete failed"); 186 | unlock(); 187 | return -1; 188 | } 189 | #elif __linux__ 190 | if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, ev->fd, NULL) == -1) { 191 | perror("epoll_ctl delete failed"); 192 | unlock(); 193 | return -1; 194 | } 195 | #endif 196 | 197 | /* Remove from the internal list */ 198 | struct event_list **current = &events; 199 | while (*current) { 200 | if ((*current)->ev == ev) { 201 | struct event_list *to_free = *current; 202 | *current = (*current)->next; 203 | free(to_free); 204 | unlock(); 205 | return 0; 206 | } 207 | current = &(*current)->next; 208 | } 209 | 210 | unlock(); 211 | return -1; 212 | } 213 | 214 | /* Stop the event dispatch loop */ 215 | void event_dispatch_stop(void) { 216 | pthread_mutex_lock(&stop_mutex); 217 | stop_flag = 1; 218 | pthread_mutex_unlock(&stop_mutex); 219 | 220 | #ifdef __linux__ 221 | /* Kind of a hack to alert epoll and let it shutdown gracefully... probably find a better way. */ 222 | if (notify_pipe[1] != -1) { 223 | if (write(notify_pipe[1], "1", 1) == -1) { 224 | perror("write to pipe failed"); 225 | } 226 | } 227 | DEBUG_PRINT("Stopping event dispatch\n"); 228 | #elif __APPLE__ 229 | struct kevent stop_event; 230 | EV_SET(&stop_event, -1, EVFILT_USER, EV_ADD | EV_ENABLE, NOTE_TRIGGER, 0, NULL); 231 | kevent(kq, &stop_event, 1, NULL, 0, NULL); 232 | #endif 233 | } 234 | 235 | /* Main loop which dispatch events */ 236 | void event_dispatch(void) { 237 | DEBUG_PRINT("Starting event dispatch\n"); 238 | 239 | #ifdef __linux__ 240 | struct epoll_event triggered_events[MAX_EVENTS]; 241 | pthread_once(&epoll_init_once, initialize_epoll); 242 | #elif __APPLE__ 243 | struct kevent triggered_events[MAX_EVENTS]; 244 | pthread_once(&kqueue_init_once, initialize_kqueue); 245 | if (kq == -1) { 246 | perror("kqueue not initialized"); 247 | return; 248 | } 249 | #endif 250 | 251 | while (1) { 252 | pthread_mutex_lock(&stop_mutex); 253 | if (stop_flag) { 254 | pthread_mutex_unlock(&stop_mutex); 255 | break; 256 | } 257 | pthread_mutex_unlock(&stop_mutex); 258 | 259 | #ifdef __linux__ 260 | int n = epoll_wait(epoll_fd, triggered_events, MAX_EVENTS, -1); 261 | if (n == -1) { 262 | perror("epoll_wait dispatch failed"); 263 | break; 264 | } 265 | 266 | #elif __APPLE__ 267 | int n = kevent(kq, NULL, 0, triggered_events, MAX_EVENTS, NULL); 268 | if (n == -1) { 269 | perror("kevent dispatch failed"); 270 | break; 271 | } 272 | #endif 273 | 274 | for (int i = 0; i < n; i++) { 275 | event_callback_t callback = NULL; 276 | void *arg = NULL; 277 | lock(); 278 | #ifdef __linux__ 279 | if (triggered_events[i].data.fd == notify_pipe[0]) { 280 | char buf[1]; 281 | int ret = read(notify_pipe[0], buf, 1); 282 | if(ret == -1) { 283 | perror("read from pipe failed"); 284 | } 285 | 286 | unlock(); 287 | continue; 288 | } 289 | 290 | struct event *ev = (struct event *)triggered_events[i].data.ptr; 291 | #elif __APPLE__ 292 | struct kevent *ke = &triggered_events[i]; 293 | if (ke->filter == EVFILT_USER) { 294 | unlock(); 295 | continue; 296 | } 297 | 298 | struct event *ev = (struct event *)ke->udata; 299 | #endif 300 | callback = ev->callback; 301 | arg = ev->arg; 302 | unlock(); 303 | 304 | if (callback) { 305 | callback(ev, arg); 306 | } 307 | 308 | } 309 | } 310 | 311 | printf("[EVENTLIB] Event dispatch stopped\n"); 312 | } -------------------------------------------------------------------------------- /libs/module.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | /* Macro loads a symbol from the "parent" and exposes it as given variable. */ 11 | #define LOAD_SYMBOL(handle, symbol, type, var) \ 12 | do { \ 13 | type** var##_ptr = (type**)dlsym(handle, symbol); \ 14 | if (!var##_ptr) { \ 15 | fprintf(stderr, "Error accessing " #var "_ptr: %s\n", dlerror()); \ 16 | return; \ 17 | } \ 18 | var = *var##_ptr; \ 19 | if (!var) { \ 20 | fprintf(stderr, "Error accessing " #var ": %s\n", dlerror()); \ 21 | return; \ 22 | } \ 23 | } while (0) 24 | 25 | struct scheduler* scheduler = NULL; 26 | struct sqldb *database = NULL; 27 | struct container* cache = NULL; 28 | struct crypto* crypto = NULL; 29 | /* Global handle to access server symbols */ 30 | static void *dlhandle = NULL; 31 | 32 | 33 | static void* cweb_mock_resolv( __attribute__((unused)) const char* module, __attribute__((unused)) const char* symbol) {return NULL;} 34 | struct symbols cweb_internal_symbols = { 35 | .resolv = cweb_mock_resolv, 36 | }; 37 | struct symbols* symbols = &cweb_internal_symbols; 38 | 39 | __attribute__((constructor)) void module_constructor() { 40 | dlhandle = dlopen(NULL, RTLD_GLOBAL | RTLD_LAZY); 41 | if (!dlhandle) { 42 | fprintf(stderr, "Error accessing server symbols: %s\n", dlerror()); 43 | return; 44 | } 45 | 46 | LOAD_SYMBOL(dlhandle, "exposed_container", struct container, cache); 47 | LOAD_SYMBOL(dlhandle, "exposed_scheduler", struct scheduler, scheduler); 48 | LOAD_SYMBOL(dlhandle, "exposed_sqldb", struct sqldb, database); 49 | LOAD_SYMBOL(dlhandle, "crypto_module", struct crypto, crypto); 50 | 51 | void* resolv = dlsym(dlhandle, "resolv"); 52 | if (!resolv) { 53 | fprintf(stderr, "Error accessing resolv: %s\n", dlerror()); 54 | return; 55 | } 56 | symbols->resolv = resolv; 57 | 58 | } -------------------------------------------------------------------------------- /modules/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joexbayer/c-web-modules/52ff9fa441b1691510b44e58462c804eec7d74c1/modules/.keep -------------------------------------------------------------------------------- /production.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CERT_FILE="server.crt" 4 | KEY_FILE="server.key" 5 | DOCKER_FLAG=false 6 | 7 | # Parse command line options 8 | while [[ "$#" -gt 0 ]]; do 9 | case $1 in 10 | --docker) 11 | DOCKER_FLAG=true 12 | ;; 13 | *) 14 | echo "Invalid option: $1" >&2 15 | exit 1 16 | ;; 17 | esac 18 | shift 19 | done 20 | 21 | # Check if server.crt and server.key exist 22 | if [[ ! -f "$CERT_FILE" || ! -f "$KEY_FILE" ]]; then 23 | echo "Certificate or key file not found." 24 | read -p "Do you want to generate them? (y/n): " generate 25 | 26 | if [[ "$generate" == "y" || "$generate" == "Y" ]]; then 27 | # Generate server.crt and server.key 28 | openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt -days 365 -nodes 29 | echo "Certificate and key generated." 30 | else 31 | echo "Certificate and key not generated. Exiting." 32 | exit 1 33 | fi 34 | fi 35 | 36 | # If docker flag is given 37 | if [ "$DOCKER_FLAG" = true ]; then 38 | docker build --build-arg PRODUCTION=1 -t cweb:production . 39 | else 40 | # Run make with -DPRODUCTION 41 | make clean 42 | make PRODUCTION=1 43 | fi -------------------------------------------------------------------------------- /src/container.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | /* Container functions */ 7 | static int container_set(const char *name, void* value); 8 | static void* container_get(const char *name); 9 | 10 | /* Main internal container */ 11 | static struct container internal_container = { 12 | .set = container_set, 13 | .get = container_get, 14 | .data = NULL, 15 | }; 16 | __attribute__((visibility("default"))) struct container* exposed_container = &internal_container; 17 | static pthread_mutex_t container_mutex = PTHREAD_MUTEX_INITIALIZER; 18 | 19 | static int container_set(const char *name, void* value) { 20 | if (name == NULL || value == NULL) return -1; 21 | pthread_mutex_lock(&container_mutex); 22 | int result = map_insert(internal_container.data, name, value); 23 | pthread_mutex_unlock(&container_mutex); 24 | return result; 25 | } 26 | 27 | static void* container_get(const char *name) { 28 | /* map_get handles input validation */ 29 | pthread_mutex_lock(&container_mutex); 30 | void* result = map_get(internal_container.data, name); 31 | pthread_mutex_unlock(&container_mutex); 32 | return result; 33 | } 34 | 35 | __attribute__((constructor)) static void container_init() { 36 | internal_container.data = map_create(32); 37 | printf("[STARTUP] Container initialized\n"); 38 | } 39 | 40 | __attribute__((destructor)) static void container_destroy() { 41 | map_destroy(internal_container.data); 42 | printf("[SHUTDOWN] Container destroyed\n"); 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/crypto.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static struct crypto_config { 5 | const char* certificate; 6 | const char* private_key; 7 | } config = { 8 | .certificate = "server.crt", 9 | .private_key = "server.key" 10 | }; 11 | 12 | __attribute__((used)) SSL_CTX *ssl_ctx; // Global SSL context 13 | 14 | static int sign_token(const char *token, char *signed_token, unsigned int signed_token_len); 15 | static int verify_token(const char *signed_token, char *token, size_t token_len); 16 | 17 | static struct crypto_ops crypto_ops = { 18 | .sign_token = sign_token, 19 | .verify_token = verify_token 20 | }; 21 | 22 | static struct crypto crypto = { 23 | .status = CRYPTO_FAILURE, 24 | .ops = &crypto_ops, 25 | .ctx = NULL 26 | }; 27 | struct crypto* crypto_module = &crypto; 28 | 29 | static char* base64_encode(const unsigned char *input, int length) { 30 | BIO *bmem, *b64; 31 | BUF_MEM *bptr; 32 | char *buff; 33 | 34 | b64 = BIO_new(BIO_f_base64()); 35 | bmem = BIO_new(BIO_s_mem()); 36 | b64 = BIO_push(b64, bmem); 37 | BIO_write(b64, input, length); 38 | BIO_flush(b64); 39 | BIO_get_mem_ptr(b64, &bptr); 40 | 41 | buff = (char *)malloc(bptr->length + 1); 42 | memcpy(buff, bptr->data, bptr->length); 43 | buff[bptr->length] = '\0'; 44 | 45 | BIO_free_all(b64); 46 | return buff; 47 | } 48 | 49 | static unsigned char* base64_decode(const char *input, int *length) { 50 | BIO *b64, *bmem; 51 | unsigned char *buffer = (unsigned char *)malloc(strlen(input)); 52 | memset(buffer, 0, strlen(input)); 53 | 54 | b64 = BIO_new(BIO_f_base64()); 55 | bmem = BIO_new_mem_buf(input, -1); 56 | bmem = BIO_push(b64, bmem); 57 | 58 | *length = BIO_read(bmem, buffer, strlen(input)); 59 | BIO_free_all(bmem); 60 | 61 | return buffer; 62 | } 63 | 64 | static int sign_token(const char *token, char *signed_token, unsigned int signed_token_len) { 65 | if (crypto.status != CRYPTO_LOADED) { 66 | fprintf(stderr, "[ERROR] Crypto module is not loaded\n"); 67 | return -1; 68 | } 69 | 70 | EVP_PKEY *pkey = NULL; 71 | EVP_MD_CTX *mdctx = NULL; 72 | FILE *key_file = NULL; 73 | unsigned char *signature = NULL; 74 | unsigned int actual_signed_len = 0; 75 | 76 | key_file = fopen(config.private_key, "r"); 77 | if (!key_file) { 78 | perror("[ERROR] Unable to open private key file"); 79 | return -1; 80 | } 81 | 82 | pkey = PEM_read_PrivateKey(key_file, NULL, NULL, NULL); 83 | fclose(key_file); 84 | if (!pkey) { 85 | fprintf(stderr, "[ERROR] Failed to read private key\n"); 86 | ERR_print_errors_fp(stderr); 87 | return -1; 88 | } 89 | 90 | mdctx = EVP_MD_CTX_new(); 91 | if (!mdctx) { 92 | fprintf(stderr, "[ERROR] Failed to create EVP_MD_CTX\n"); 93 | EVP_PKEY_free(pkey); 94 | return -1; 95 | } 96 | 97 | signature = (unsigned char *)malloc(EVP_PKEY_size(pkey)); 98 | if (!signature) { 99 | fprintf(stderr, "[ERROR] Failed to allocate signature buffer\n"); 100 | EVP_MD_CTX_free(mdctx); 101 | EVP_PKEY_free(pkey); 102 | return -1; 103 | } 104 | 105 | if (EVP_SignInit(mdctx, EVP_sha256()) != 1 || 106 | EVP_SignUpdate(mdctx, token, strlen(token)) != 1 || 107 | EVP_SignFinal(mdctx, signature, &actual_signed_len, pkey) != 1) { 108 | fprintf(stderr, "[ERROR] Failed to sign token\n"); 109 | ERR_print_errors_fp(stderr); 110 | free(signature); 111 | EVP_MD_CTX_free(mdctx); 112 | EVP_PKEY_free(pkey); 113 | return -1; 114 | } 115 | 116 | char *encoded_signature = base64_encode(signature, actual_signed_len); 117 | if (strlen(encoded_signature) >= signed_token_len) { 118 | fprintf(stderr, "[ERROR] Signed token buffer too small\n"); 119 | free(encoded_signature); 120 | free(signature); 121 | EVP_MD_CTX_free(mdctx); 122 | EVP_PKEY_free(pkey); 123 | return -1; 124 | } 125 | 126 | strncpy(signed_token, encoded_signature, signed_token_len); 127 | free(encoded_signature); 128 | free(signature); 129 | EVP_MD_CTX_free(mdctx); 130 | EVP_PKEY_free(pkey); 131 | 132 | return strlen(signed_token); // Return the length of the Base64 encoded signature 133 | } 134 | 135 | static int verify_token(const char *signed_token, char *token, size_t token_len) { 136 | if (crypto.status != CRYPTO_LOADED) { 137 | fprintf(stderr, "[ERROR] Crypto module is not loaded\n"); 138 | return -1; 139 | } 140 | 141 | EVP_PKEY *pkey = NULL; 142 | EVP_MD_CTX *mdctx = NULL; 143 | FILE *cert_file = NULL; 144 | int verify_result = 0; 145 | unsigned char *decoded_signature = NULL; 146 | int decoded_len = 0; 147 | 148 | cert_file = fopen(config.certificate, "r"); 149 | if (!cert_file) { 150 | perror("[ERROR] Unable to open certificate file"); 151 | return -1; 152 | } 153 | 154 | X509 *cert = PEM_read_X509(cert_file, NULL, NULL, NULL); 155 | fclose(cert_file); 156 | if (!cert) { 157 | fprintf(stderr, "[ERROR] Failed to read certificate\n"); 158 | ERR_print_errors_fp(stderr); 159 | return -1; 160 | } 161 | 162 | pkey = X509_get_pubkey(cert); 163 | X509_free(cert); 164 | if (!pkey) { 165 | fprintf(stderr, "[ERROR] Failed to extract public key from certificate\n"); 166 | ERR_print_errors_fp(stderr); 167 | return -1; 168 | } 169 | 170 | mdctx = EVP_MD_CTX_new(); 171 | if (!mdctx) { 172 | fprintf(stderr, "[ERROR] Failed to create EVP_MD_CTX\n"); 173 | EVP_PKEY_free(pkey); 174 | return -1; 175 | } 176 | 177 | decoded_signature = base64_decode(signed_token, &decoded_len); 178 | if (!decoded_signature) { 179 | fprintf(stderr, "[ERROR] Failed to decode signed token\n"); 180 | EVP_MD_CTX_free(mdctx); 181 | EVP_PKEY_free(pkey); 182 | return -1; 183 | } 184 | 185 | if (EVP_VerifyInit(mdctx, EVP_sha256()) != 1 || 186 | EVP_VerifyUpdate(mdctx, token, token_len) != 1) { 187 | fprintf(stderr, "[ERROR] Failed to initialize verification\n"); 188 | ERR_print_errors_fp(stderr); 189 | free(decoded_signature); 190 | EVP_MD_CTX_free(mdctx); 191 | EVP_PKEY_free(pkey); 192 | return -1; 193 | } 194 | 195 | verify_result = EVP_VerifyFinal(mdctx, decoded_signature, decoded_len, pkey); 196 | 197 | free(decoded_signature); 198 | EVP_MD_CTX_free(mdctx); 199 | EVP_PKEY_free(pkey); 200 | 201 | if (verify_result != 1) { 202 | fprintf(stderr, "[ERROR] Token verification failed\n"); 203 | ERR_print_errors_fp(stderr); 204 | return -1; 205 | } 206 | 207 | return 0; // Verification succeeded 208 | } 209 | 210 | static SSL_CTX* initialize_ssl_context() { 211 | const SSL_METHOD *method = TLS_server_method(); 212 | SSL_CTX *ctx = SSL_CTX_new(method); 213 | if (!ctx) { 214 | perror("[ERROR] Failed to initialize SSL context"); 215 | ERR_print_errors_fp(stderr); 216 | 217 | #ifdef PRODUCTION 218 | exit(EXIT_FAILURE); 219 | #else 220 | return NULL; 221 | #endif 222 | } 223 | 224 | if (SSL_CTX_use_certificate_file(ctx, config.certificate, SSL_FILETYPE_PEM) <= 0) { 225 | perror("[ERROR] Failed to load server certificate"); 226 | ERR_print_errors_fp(stderr); 227 | #ifdef PRODUCTION 228 | exit(EXIT_FAILURE); 229 | #else 230 | return NULL; 231 | #endif 232 | } 233 | 234 | if (SSL_CTX_use_PrivateKey_file(ctx, config.private_key , SSL_FILETYPE_PEM) <= 0) { 235 | perror("[ERROR] Failed to load server private key"); 236 | ERR_print_errors_fp(stderr); 237 | #ifdef PRODUCTION 238 | exit(EXIT_FAILURE); 239 | #else 240 | return NULL; 241 | #endif 242 | } 243 | 244 | if (!SSL_CTX_check_private_key(ctx)) { 245 | fprintf(stderr, "[ERROR] Private key does not match the certificate\n"); 246 | #ifdef PRODUCTION 247 | exit(EXIT_FAILURE); 248 | #else 249 | return NULL; 250 | #endif 251 | } 252 | 253 | return ctx; 254 | } 255 | 256 | __attribute__((constructor)) int crypto_init() { 257 | if (OPENSSL_init_crypto(OPENSSL_INIT_NO_ATEXIT, NULL) == 0) { 258 | fprintf(stderr, "[ERROR] Failed to initialize OpenSSL\n"); 259 | return -1; 260 | } 261 | 262 | SSL_library_init(); 263 | OpenSSL_add_all_algorithms(); 264 | SSL_load_error_strings(); 265 | 266 | ssl_ctx = initialize_ssl_context(); 267 | if (!ssl_ctx) { 268 | return -1; 269 | } 270 | crypto.status = CRYPTO_LOADED; 271 | 272 | char* test_token = "test_token"; 273 | char signed_token[512]; 274 | unsigned int signed_token_len = sizeof(signed_token); 275 | 276 | int sign_result = sign_token(test_token, signed_token, signed_token_len); 277 | if (sign_result <= 0) { 278 | fprintf(stderr, "[ERROR] Test signing failed\n"); 279 | return -1; 280 | } 281 | 282 | printf("[DEBUG] Signed token: %s\n", signed_token); 283 | 284 | int verify_result = verify_token(signed_token, test_token, strlen(test_token)); 285 | if (verify_result < 0) { 286 | fprintf(stderr, "[ERROR] Test verification failed\n"); 287 | return -1; 288 | } 289 | 290 | printf("[CRYPTO] Crypto module loaded\n"); 291 | return 0; 292 | } 293 | 294 | __attribute__((destructor)) void crypto_cleanup() { 295 | SSL_CTX_free(ssl_ctx); 296 | CRYPTO_cleanup_all_ex_data(); 297 | ERR_free_strings(); 298 | EVP_cleanup(); 299 | printf("[CRYPTO] Crypto module unloaded\n"); 300 | } -------------------------------------------------------------------------------- /src/db.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #define DB_FILE "db.sqlite3" 7 | 8 | static int db_exec(const char *sql, int (*callback)(void *, int, char **, char **), void *data); 9 | static int db_prepare(sqlite3 *db, const char *sql, int len, sqlite3_stmt **stmt, const char **tail); 10 | static int db_step(sqlite3_stmt *stmt); 11 | static int db_finalize(sqlite3_stmt *stmt); 12 | static int db_bind_text(sqlite3_stmt *stmt, int pos, const char *text, int len, void (*free)(void *)); 13 | static int db_bind_int(sqlite3_stmt *stmt, int pos, int value); 14 | static const char *db_column_text(sqlite3_stmt *stmt, int pos); 15 | static int db_column_int(sqlite3_stmt *stmt, int pos); 16 | static int db_column_count(sqlite3_stmt *stmt); 17 | static int db_reset(sqlite3_stmt *stmt); 18 | static int db_changes(sqlite3 *db); 19 | static int db_last_insert_rowid(sqlite3 *db); 20 | static void db_free(void *ptr); 21 | 22 | struct sqldb sql_db = { 23 | .exec = db_exec, 24 | .prepare = db_prepare, 25 | .step = db_step, 26 | .finalize = db_finalize, 27 | .bind_text = db_bind_text, 28 | .bind_int = db_bind_int, 29 | .column_text = db_column_text, 30 | .column_int = db_column_int, 31 | .column_count = db_column_count, 32 | .reset = db_reset, 33 | .changes = db_changes, 34 | .last_insert_rowid = db_last_insert_rowid, 35 | .free = db_free 36 | }; 37 | __attribute__((visibility("default"))) struct sqldb *exposed_sqldb = &sql_db; 38 | 39 | static int db_exec(const char *sql, int (*callback)(void *, int, char **, char **), void *data) { 40 | char *err_msg = 0; 41 | int rc = sqlite3_exec(sql_db.db, sql, callback, data, &err_msg); 42 | printf("[SQL] %s\n", sql); 43 | if (rc != SQLITE_OK) { 44 | fprintf(stderr, "SQL error: %s\n", err_msg); 45 | sqlite3_free(err_msg); 46 | } 47 | return rc; 48 | } 49 | 50 | static int db_prepare(sqlite3 *db, const char *sql, int len, sqlite3_stmt **stmt, const char **tail) { 51 | printf("[SQL] %s\n", sql); 52 | return sqlite3_prepare_v2(db, sql, len, stmt, tail); 53 | } 54 | 55 | static int db_step(sqlite3_stmt *stmt) { 56 | return sqlite3_step(stmt); 57 | } 58 | 59 | static int db_finalize(sqlite3_stmt *stmt) { 60 | return sqlite3_finalize(stmt); 61 | } 62 | 63 | static int db_bind_text(sqlite3_stmt *stmt, int pos, const char *text, int len, void (*free)(void *)) { 64 | return sqlite3_bind_text(stmt, pos, text, len, free); 65 | } 66 | 67 | static int db_bind_int(sqlite3_stmt *stmt, int pos, int value) { 68 | return sqlite3_bind_int(stmt, pos, value); 69 | } 70 | 71 | static const char *db_column_text(sqlite3_stmt *stmt, int pos) { 72 | return (const char *)sqlite3_column_text(stmt, pos); 73 | } 74 | 75 | static int db_column_int(sqlite3_stmt *stmt, int pos) { 76 | return sqlite3_column_int(stmt, pos); 77 | } 78 | 79 | static int db_column_count(sqlite3_stmt *stmt) { 80 | return sqlite3_column_count(stmt); 81 | } 82 | 83 | static int db_reset(sqlite3_stmt *stmt) { 84 | return sqlite3_reset(stmt); 85 | } 86 | 87 | static int db_changes(sqlite3 *db) { 88 | return sqlite3_changes(db); 89 | } 90 | 91 | static int db_last_insert_rowid(sqlite3 *db) { 92 | return sqlite3_last_insert_rowid(db); 93 | } 94 | 95 | static void db_free(void *ptr) { 96 | return sqlite3_free(ptr); 97 | } 98 | 99 | __attribute__((constructor)) void db_init() { 100 | sqlite3_config(SQLITE_CONFIG_SERIALIZED); 101 | 102 | int rc = sqlite3_open(DB_FILE, &sql_db.db); 103 | if (rc) { 104 | fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(sql_db.db)); 105 | exit(1); 106 | } 107 | } 108 | 109 | __attribute__((destructor)) void db_close() { 110 | sqlite3_close(sql_db.db); 111 | printf("[SHUTDOWN] Database closed\n"); 112 | } -------------------------------------------------------------------------------- /src/engine.c: -------------------------------------------------------------------------------- 1 | #include "http.h" 2 | #include "router.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | /* Thread-local storage for sigjmp_buf */ 14 | static __thread sigjmp_buf jump_buffer; 15 | 16 | /* Signal handler for fatal errors */ 17 | static void fault_handler(int sig, siginfo_t *info, void *ucontext) { 18 | (void)ucontext; 19 | const char *signal_name; 20 | 21 | switch (sig) { 22 | case SIGSEGV: signal_name = "Segmentation fault"; break; 23 | case SIGBUS: signal_name = "Bus error"; break; 24 | case SIGFPE: signal_name = "Floating-point exception"; break; 25 | case SIGILL: signal_name = "Illegal instruction"; break; 26 | default: signal_name = "Unknown signal"; break; 27 | } 28 | 29 | fprintf(stderr, "%s detected in handler execution. Signal %d received at address %p.\n", 30 | signal_name, sig, info->si_addr); 31 | siglongjmp(jump_buffer, 1); 32 | } 33 | 34 | /* Setup signal handlers for the process */ 35 | __attribute__((constructor)) void global_signal_setup() { 36 | struct sigaction sa; 37 | memset(&sa, 0, sizeof(sa)); 38 | sa.sa_sigaction = fault_handler; 39 | sigemptyset(&sa.sa_mask); 40 | sa.sa_flags = SA_SIGINFO; 41 | 42 | if (sigaction(SIGSEGV, &sa, NULL) == -1 || 43 | sigaction(SIGBUS, &sa, NULL) == -1 || 44 | sigaction(SIGFPE, &sa, NULL) == -1 || 45 | sigaction(SIGILL, &sa, NULL) == -1) { 46 | perror("[ERROR] Failed to set up global signal handlers"); 47 | exit(EXIT_FAILURE); 48 | } 49 | } 50 | 51 | /* Mask all fatal signals in a thread */ 52 | void block_signals_in_thread() { 53 | sigset_t mask; 54 | sigemptyset(&mask); 55 | sigaddset(&mask, SIGSEGV); 56 | sigaddset(&mask, SIGBUS); 57 | sigaddset(&mask, SIGFPE); 58 | sigaddset(&mask, SIGILL); 59 | 60 | if (pthread_sigmask(SIG_BLOCK, &mask, NULL) != 0) { 61 | perror("[ERROR] Failed to block signals in thread"); 62 | exit(EXIT_FAILURE); 63 | } 64 | } 65 | 66 | /* Unmask signals in the thread for safe execution */ 67 | void setup_thread_signals() { 68 | sigset_t mask; 69 | sigemptyset(&mask); 70 | sigaddset(&mask, SIGSEGV); 71 | sigaddset(&mask, SIGBUS); 72 | sigaddset(&mask, SIGFPE); 73 | sigaddset(&mask, SIGILL); 74 | 75 | if (pthread_sigmask(SIG_UNBLOCK, &mask, NULL) != 0) { 76 | perror("[ERROR] Failed to unblock signals for thread"); 77 | exit(EXIT_FAILURE); 78 | } 79 | } 80 | 81 | /* Safely execute a handler */ 82 | void safe_execute_handler(handler_t handler, struct http_request *req, struct http_response *res) { 83 | setup_thread_signals(); 84 | 85 | if (sigsetjmp(jump_buffer, 1) == 0) { 86 | handler(req, res); 87 | } else { 88 | snprintf(res->body, HTTP_RESPONSE_SIZE, "Handler execution failed: Fatal signal detected.\n"); 89 | res->status = HTTP_500_INTERNAL_SERVER_ERROR; 90 | } 91 | } -------------------------------------------------------------------------------- /src/http.c: -------------------------------------------------------------------------------- 1 | 2 | #include "http.h" 3 | #include "map.h" 4 | #include 5 | #include 6 | #include 7 | 8 | /* Hypertext Transfer Protocol -- HTTP/1.1 Spec: https://datatracker.ietf.org/doc/html/rfc2616 */ 9 | 10 | const char *http_methods[] = {"GET", "POST", "PUT", "DELETE"}; 11 | const char *http_errors[] = { 12 | "400 Bad Request", /* Unknown defaults to 400 */ 13 | "101 Switching Protocols", 14 | "200 OK", 15 | "302 Found", 16 | "400 Bad Request", "401 Unauthorized", "403 Forbidden", "404 Not Found", "405 Method Not Allowed", "414 URI Too Long", 17 | "500 Internal Server Error" 18 | }; 19 | 20 | /* Helper function to trim trailing whitespace */ 21 | static void trim_trailing_whitespace(char *str) { 22 | int len = strlen(str); 23 | while (len > 0 && (str[len - 1] == '\r' || str[len - 1] == '\n' || isspace((unsigned char)str[len - 1]))) { 24 | str[--len] = '\0'; 25 | } 26 | } 27 | 28 | /* Parse HTTP method */ 29 | static void http_parse_method(const char *method, struct http_request *req) { 30 | if (strncmp(method, "GET", 3) == 0) { 31 | req->method = HTTP_GET; 32 | } else if (strncmp(method, "POST", 4) == 0) { 33 | req->method = HTTP_POST; 34 | } else if (strncmp(method, "PUT", 3) == 0) { 35 | req->method = HTTP_PUT; 36 | } else if (strncmp(method, "DELETE", 6) == 0) { 37 | req->method = HTTP_DELETE; 38 | } else { 39 | req->method = -1; 40 | } 41 | } 42 | 43 | /* Parses HTTP headers and puts them into headers map */ 44 | static void http_parse_headers(const char *headers, struct http_request *req) { 45 | const char *line_start = headers; 46 | 47 | while (*line_start != '\0') { 48 | const char *line_end = strstr(line_start, "\r\n"); 49 | if (!line_end) { 50 | line_end = line_start + strlen(line_start); 51 | } 52 | 53 | /* Calculate line length and copy it into a temporary buffer */ 54 | size_t line_length = line_end - line_start; 55 | char *line = malloc(line_length + 1); 56 | if (!line) { 57 | perror("Failed to allocate memory for header line"); 58 | return; 59 | } 60 | strncpy(line, line_start, line_length); 61 | line[line_length] = '\0'; 62 | 63 | char *colon = strchr(line, ':'); 64 | if (colon) { 65 | *colon = '\0'; 66 | char *key = line; 67 | char *value = colon + 1; 68 | while (*value == ' ') { 69 | value++; 70 | } 71 | 72 | map_insert(req->headers, key, strdup(value)); 73 | } 74 | 75 | free(line); 76 | 77 | if (*line_end == '\0') { 78 | break; 79 | } 80 | line_start = line_end + 2; 81 | } 82 | } 83 | 84 | 85 | /* Parses query parameters and puts them into params map */ 86 | static void http_parse_params(const char *query, struct http_request *req) { 87 | const char *param_start = query; 88 | 89 | while (*param_start != '\0') { 90 | const char *param_end = strchr(param_start, '&'); 91 | const char *key_end = strchr(param_start, '='); 92 | 93 | if (key_end && (!param_end || key_end < param_end)) { 94 | size_t key_length = key_end - param_start; 95 | size_t value_length = param_end ? (size_t)(param_end - key_end - 1) : strlen(key_end + 1); 96 | 97 | char *key = malloc(key_length + 1); 98 | char *value = malloc(value_length + 1); 99 | if (!key || !value) { 100 | perror("Failed to allocate memory for query parameters"); 101 | free(key); 102 | free(value); 103 | return; 104 | } 105 | 106 | strncpy(key, param_start, key_length); 107 | key[key_length] = '\0'; 108 | 109 | strncpy(value, key_end + 1, value_length); 110 | value[value_length] = '\0'; 111 | 112 | map_insert(req->params, key, value); 113 | 114 | free(key); 115 | 116 | param_start = param_end ? param_end + 1 : ""; 117 | } else { 118 | break; 119 | } 120 | } 121 | } 122 | 123 | 124 | 125 | /* Allocates and copies request body */ 126 | static void http_parse_body(const char *request, struct http_request *req) { 127 | char *body = strstr(request, "\r\n\r\n"); 128 | if (body) { 129 | body += 4; /* Skip past the "\r\n\r\n" */ 130 | req->body = strdup(body); 131 | if (!req->body) { 132 | perror("Failed to allocate body"); 133 | } 134 | } else { 135 | req->body = NULL; 136 | } 137 | } 138 | 139 | /* Mainly parses the header of the HTTP request */ 140 | static void http_parse_request(const char *request, struct http_request *req) { 141 | char *request_copy = strdup(request); 142 | if (!request_copy) { 143 | perror("Failed to allocate request copy"); 144 | return; 145 | } 146 | 147 | char *cursor = request_copy; 148 | 149 | /* Parse method */ 150 | char *method_end = strchr(cursor, ' '); 151 | if (!method_end) { 152 | perror("Failed to parse method"); 153 | free(request_copy); 154 | req->method = -1; 155 | return; 156 | } 157 | *method_end = '\0'; 158 | http_parse_method(cursor, req); 159 | 160 | /** 161 | * Parse path 162 | * Note: Servers ought to be cautious about depending on URI lengths 163 | * above 255 bytes, because some older client or proxy 164 | * implementations might not properly support these lengths. 165 | */ 166 | cursor = method_end + 1; 167 | char *path_end = strchr(cursor, ' '); 168 | if (!path_end) { 169 | perror("Failed to parse path"); 170 | free(request_copy); 171 | req->method = -1; 172 | return; 173 | } 174 | *path_end = '\0'; 175 | if (strlen(cursor) > 255) { 176 | fprintf(stderr, "[ERROR] URI length exceeds 255 bytes\n"); 177 | /** 178 | * A server SHOULD return 414 (Request-URI Too Long) status if a URI is longer 179 | * than the server can handle. 180 | */ 181 | req->status = HTTP_414_URI_TOO_LONG; 182 | free(request_copy); 183 | req->method = -1; 184 | return; 185 | } 186 | req->path = strdup(cursor); 187 | if (!req->path) { 188 | perror("Failed to allocate memory for path"); 189 | free(request_copy); 190 | return; 191 | } 192 | 193 | /* Parse HTTP version */ 194 | cursor = path_end + 1; 195 | char *version_end = strstr(cursor, "\r\n"); 196 | if (!version_end) { 197 | perror("Failed to parse HTTP version"); 198 | free(request_copy); 199 | req->method = -1; 200 | return; 201 | } 202 | *version_end = '\0'; 203 | if (strncmp(cursor, HTTP_VERSION, strlen(HTTP_VERSION)) != 0) { 204 | printf("[ERROR] Invalid HTTP version %s. %s supported.\n", cursor, HTTP_VERSION); 205 | req->method = -1; 206 | } 207 | 208 | /* Move cursor to headers */ 209 | cursor = version_end + 2; 210 | 211 | /* Create maps for headers and params */ 212 | req->headers = map_create(32); 213 | req->params = map_create(10); 214 | if (!req->headers || !req->params) { 215 | perror("Failed to create map"); 216 | free(request_copy); 217 | return; 218 | } 219 | 220 | /* Parse headers */ 221 | char *headers_end = strstr(cursor, "\r\n\r\n"); 222 | if (headers_end) { 223 | size_t headers_length = headers_end - cursor; 224 | char *headers = malloc(headers_length + 1); 225 | if (!headers) { 226 | perror("Failed to allocate memory for headers"); 227 | free(request_copy); 228 | return; 229 | } 230 | strncpy(headers, cursor, headers_length); 231 | headers[headers_length] = '\0'; 232 | http_parse_headers(headers, req); 233 | free(headers); 234 | 235 | cursor = headers_end + 4; /* Move past "\r\n\r\n" */ 236 | } 237 | 238 | /* Parse query params */ 239 | char *query = strchr(req->path, '?'); 240 | if (query) { 241 | *query = '\0'; 242 | query++; 243 | http_parse_params(query, req); 244 | } 245 | 246 | /* Parse body */ 247 | http_parse_body(cursor, req); 248 | 249 | /* Parse Content-Length */ 250 | char *content_length_str = map_get(req->headers, "Content-Length"); 251 | if (content_length_str) { 252 | req->content_length = atoi(content_length_str); 253 | } else { 254 | req->content_length = 0; 255 | } 256 | 257 | /* Parse Connection */ 258 | const char *connection = map_get(req->headers, "Connection"); 259 | if (connection && strcasecmp(connection, "keep-alive") == 0) { 260 | req->keep_alive = 1; 261 | } else if (connection && strcasecmp(connection, "close") == 0) { 262 | req->close = 1; 263 | } 264 | 265 | free(request_copy); 266 | } 267 | 268 | 269 | /* Tries to get boundary if multipart data is present. */ 270 | static int http_parse_content_type(const struct http_request *req, char **boundary) { 271 | const char *content_type = map_get(req->headers, "Content-Type"); 272 | if (content_type == NULL) { 273 | fprintf(stderr, "[ERROR] Content-Type header not found\n"); 274 | return -1; 275 | } 276 | 277 | 278 | const char *boundary_prefix = "boundary="; 279 | *boundary = strstr(content_type, boundary_prefix); 280 | if (*boundary == NULL) { 281 | fprintf(stderr, "[ERROR] Boundary not found in Content-Type header\n"); 282 | return -1; 283 | } 284 | 285 | *boundary += strlen(boundary_prefix); 286 | if (**boundary == '\0') { 287 | fprintf(stderr, "[ERROR] Boundary value is empty\n"); 288 | return -1; 289 | } 290 | 291 | return 0; 292 | } 293 | 294 | /** 295 | * Extract form data from body 296 | * Multiple form fields are separated by boundary 297 | * @param body Request body 298 | * @param boundary Boundary string 299 | * @param form_data Map to store form data 300 | */ 301 | static int http_extract_multipart_form_data(const char *body, const char *boundary, struct map *form_data) { 302 | char *boundary_start = strstr(body, boundary); 303 | if (boundary_start == NULL) { 304 | fprintf(stderr, "[ERROR] Boundary not found in body\n"); 305 | return -1; 306 | } 307 | 308 | char *boundary_end = strstr(boundary_start, boundary); 309 | if (boundary_end == NULL) { 310 | fprintf(stderr, "[ERROR] Boundary end not found in body\n"); 311 | return -1; 312 | } 313 | 314 | while (boundary_start != NULL) { 315 | boundary_start += strlen(boundary); 316 | if (strncmp(boundary_start, "--", 2) == 0) break; 317 | 318 | /* Find Content-Disposition */ 319 | char *content_disposition = strstr(boundary_start, "Content-Disposition: form-data; name=\""); 320 | if (content_disposition == NULL) break; 321 | 322 | /* Extract field name */ 323 | content_disposition += strlen("Content-Disposition: form-data; name=\""); 324 | char field_name[50]; 325 | sscanf(content_disposition, "%49[^\"]", field_name); 326 | 327 | /* Extract value */ 328 | char *value_start = strstr(content_disposition, "\r\n\r\n"); 329 | if (value_start == NULL) break; 330 | value_start += 4; 331 | 332 | char *value_end = strstr(value_start, boundary); 333 | if (value_end == NULL) break; 334 | value_end -= 2; 335 | 336 | char *value = (char *)malloc(value_end - value_start + 1); 337 | if (value == NULL) { 338 | perror("Error allocating memory"); 339 | return -1; 340 | } 341 | 342 | strncpy(value, value_start, value_end - value_start); 343 | value[value_end - value_start] = '\0'; 344 | 345 | trim_trailing_whitespace(value); 346 | 347 | map_insert(form_data, field_name, value); 348 | 349 | boundary_start = strstr(value_end, boundary); 350 | } 351 | 352 | return 0; 353 | } 354 | 355 | /* Parses body data if its either multipart of x-www-form */ 356 | int http_parse_data(struct http_request *req) { 357 | req->data = map_create(32); 358 | if(!req->data) { 359 | perror("Failed to create map"); 360 | return -1; 361 | } 362 | 363 | char *content_type = map_get(req->headers, "Content-Type"); 364 | 365 | /* Handle multipart/form-data */ 366 | if (content_type && strstr(content_type, "multipart/form-data")) { 367 | char *boundary = NULL; 368 | if (http_parse_content_type(req, &boundary) == 0) { 369 | if (http_extract_multipart_form_data(req->body, boundary, req->data) != 0) { 370 | fprintf(stderr, "[ERROR] Failed to extract multipart form data\n"); 371 | return -1; 372 | } 373 | } 374 | } 375 | 376 | /* Handle application/x-www-form-urlencoded */ 377 | if (content_type && strstr(content_type, "application/x-www-form-urlencoded")) { 378 | const char *param_start = req->body; 379 | 380 | while (*param_start != '\0') { 381 | const char *param_end = strchr(param_start, '&'); 382 | const char *key_end = strchr(param_start, '='); 383 | 384 | if (key_end && (!param_end || key_end < param_end)) { 385 | size_t key_length = key_end - param_start; 386 | size_t value_length = param_end ? (size_t)(param_end - key_end - 1) : strlen(key_end + 1); 387 | 388 | char *key = malloc(key_length + 1); 389 | char *value = malloc(value_length + 1); 390 | if (!key || !value) { 391 | perror("Failed to allocate memory for form data"); 392 | free(key); 393 | free(value); 394 | return -1; 395 | } 396 | 397 | strncpy(key, param_start, key_length); 398 | key[key_length] = '\0'; 399 | 400 | strncpy(value, key_end + 1, value_length); 401 | value[value_length] = '\0'; 402 | 403 | map_insert(req->data, key, value); 404 | 405 | param_start = param_end ? param_end + 1 : ""; 406 | } else { 407 | break; 408 | } 409 | } 410 | } 411 | 412 | return 0; 413 | } 414 | 415 | int http_parse(const char *request, struct http_request *req) { 416 | http_parse_request(request, req); 417 | if (req->method == -1) { 418 | return -1; 419 | } 420 | 421 | /** 422 | * Requests MUST include the Host header field, and the server MUST reject requests without it (400 Bad Request). 423 | */ 424 | char* host = map_get(req->headers, "Host"); 425 | if (!host) { 426 | fprintf(stderr, "[ERROR] Host header not found\n"); 427 | req->method = -1; 428 | req->status = HTTP_400_BAD_REQUEST; 429 | return -1; 430 | } 431 | 432 | return 0; 433 | } -------------------------------------------------------------------------------- /src/list.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | /* Create a new list */ 6 | struct list *list_create(void) { 7 | struct list *list = malloc(sizeof(struct list)); 8 | if (!list) { 9 | perror("Failed to allocate memory for list"); 10 | return NULL; 11 | } 12 | list->head = NULL; 13 | list->size = 0; 14 | return list; 15 | } 16 | 17 | /* Destroy the list and free all nodes (data is not freed) */ 18 | void list_destroy(struct list *list) { 19 | if (!list) return; 20 | 21 | struct list_node *current = list->head; 22 | while (current) { 23 | struct list_node *next = current->next; 24 | free(current); 25 | current = next; 26 | } 27 | 28 | free(list); 29 | } 30 | 31 | /* Add an element to the front of the list */ 32 | void list_add(struct list *list, void *data) { 33 | if (!list) return; 34 | 35 | struct list_node *new_node = malloc(sizeof(struct list_node)); 36 | if (!new_node) { 37 | perror("Failed to allocate memory for list node"); 38 | return; 39 | } 40 | 41 | new_node->data = data; 42 | new_node->next = list->head; 43 | list->head = new_node; 44 | list->size++; 45 | } 46 | 47 | /* Remove the first occurrence of an element (uses pointer comparison) */ 48 | int list_remove(struct list *list, void *data) { 49 | if (!list || !list->head) return 0; 50 | 51 | struct list_node **current = &list->head; 52 | while (*current) { 53 | if ((*current)->data == data) { 54 | struct list_node *to_remove = *current; 55 | *current = to_remove->next; 56 | free(to_remove); 57 | list->size--; 58 | return 1; 59 | } 60 | current = &(*current)->next; 61 | } 62 | 63 | return 0; 64 | } 65 | 66 | /* Iterate over the list and apply a function to each element */ 67 | void list_iterate(struct list *list, void (*func)(void *data)) { 68 | if (!list || !func) return; 69 | 70 | struct list_node *current = list->head; 71 | while (current) { 72 | func(current->data); 73 | current = current->next; 74 | } 75 | } -------------------------------------------------------------------------------- /src/map.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "map.h" 6 | 7 | /* Create a new map with shared memory allocation */ 8 | struct map *map_create(size_t initial_capacity) { 9 | if (initial_capacity <= 0) 10 | return NULL; 11 | 12 | /* Allocate memory for the map structure */ 13 | struct map *m = malloc(sizeof(struct map)); 14 | if (!m) 15 | return NULL; 16 | 17 | /* Allocate memory for the entries array */ 18 | m->entries = malloc(initial_capacity * sizeof(struct map_entry)); 19 | if (!m->entries) { 20 | free(m); 21 | return NULL; 22 | } 23 | 24 | m->size = 0; 25 | m->capacity = initial_capacity; 26 | return m; 27 | } 28 | 29 | /* Destroy the map and free shared memory */ 30 | void map_destroy(struct map *map) { 31 | if (!map) 32 | return; 33 | 34 | /* Free each key's shared memory */ 35 | for (size_t i = 0; i < map->size; ++i) { 36 | if (map->entries[i].key) { 37 | free(map->entries[i].key); 38 | } 39 | } 40 | 41 | /* Free the entries array and the map structure */ 42 | free(map->entries); 43 | free(map); 44 | } 45 | 46 | /* Insert a key-value pair into the map using shared memory for the key */ 47 | int map_insert(struct map *map, const char *key, void *value) { 48 | if (!map) 49 | return -MAP_ERR; 50 | 51 | /* Check if capacity is exceeded */ 52 | if (map->size >= map->capacity) { 53 | return -MAP_ERR; 54 | } 55 | 56 | /* Check for existing key to avoid duplicates */ 57 | for (size_t i = 0; i < map->size; ++i) { 58 | if (strcmp(map->entries[i].key, key) == 0) { 59 | return 0; // Key already exists, no insertion 60 | } 61 | } 62 | 63 | /* Allocate shared memory for the key string */ 64 | size_t key_len = strlen(key) + 1; 65 | map->entries[map->size].key = malloc(key_len); 66 | if (!map->entries[map->size].key) { 67 | return -MAP_ERR; 68 | } 69 | 70 | /* Copy the key into shared memory */ 71 | strncpy(map->entries[map->size].key, key, key_len); 72 | 73 | /* Assign the value and increment the size */ 74 | map->entries[map->size].value = value; 75 | map->size++; 76 | return 0; 77 | } 78 | 79 | /* Retrieve a value from the map by key */ 80 | void *map_get(const struct map *map, const char *key) { 81 | if (!map) return NULL; 82 | 83 | for (size_t i = 0; i < map->size; ++i) { 84 | if (strcmp(map->entries[i].key, key) == 0) { 85 | return map->entries[i].value; 86 | } 87 | } 88 | return NULL; 89 | } 90 | 91 | /* Remove a key-value pair from the map */ 92 | int map_remove(struct map *map, const char *key) { 93 | for (size_t i = 0; i < map->size; ++i) { 94 | if (strcmp(map->entries[i].key, key) == 0) { 95 | /* Free the shared memory for the key */ 96 | free(map->entries[i].key); 97 | 98 | /* Move the last entry to the current position to fill the gap */ 99 | map->entries[i] = map->entries[map->size - 1]; 100 | map->size--; 101 | return 0; 102 | } 103 | } 104 | return -MAP_KEY_NOT_FOUND; 105 | } 106 | 107 | /* Get the number of entries in the map */ 108 | size_t map_size(const struct map *map) { 109 | return map->size; 110 | } -------------------------------------------------------------------------------- /src/mngt.c: -------------------------------------------------------------------------------- 1 | #include "http.h" 2 | #include "router.h" 3 | #include "cweb.h" 4 | #include "map.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #define TMP_DIR "modules" 16 | #ifdef __APPLE__ 17 | #define CFLAGS "-fPIC -shared -I./include -I/opt/homebrew/opt/jansson/include -I/opt/homebrew/opt/openssl@3/include" 18 | #define LIBS "-L./libs -lmodule -L/opt/homebrew/opt/jansson/lib -ljansson -lsqlite3 -L/opt/homebrew/opt/openssl@3/lib -lssl -lcrypto" 19 | #elif __linux__ 20 | #define CFLAGS "-fPIC -shared -I./include -Wl,-rpath=./libs" /* -Wl,-rpath needed for archlinux. */ 21 | #define LIBS "-L./libs -lmodule -ljansson -lsqlite3" 22 | #else 23 | #error "Unsupported platform" 24 | #endif 25 | 26 | /** 27 | * Write code to a temporary file and compile it to .so 28 | * @param filename Name of the file 29 | * @param code Code to write to the file 30 | */ 31 | int write_and_compile(const char *filename, const char *code, char *error_buffer, size_t buffer_size) { 32 | char source_path[SO_PATH_MAX_LEN], so_path[SO_PATH_MAX_LEN]; 33 | if (snprintf(source_path, sizeof(source_path), "%s/%s.c", TMP_DIR, filename) >= (int)sizeof(source_path) || 34 | snprintf(so_path, sizeof(so_path), "%s/%s.so", TMP_DIR, filename) >= (int)sizeof(so_path)) { 35 | fprintf(stderr, "[ERROR] Path buffer overflow.\n"); 36 | return -1; 37 | } 38 | 39 | /* Save code to file for compilation. */ 40 | FILE *fp = fopen(source_path, "w"); 41 | if (fp == NULL) { 42 | perror("Error creating C file"); 43 | return -1; 44 | } 45 | if (fprintf(fp, "%s", code) < 0) { 46 | perror("Error writing to C file"); 47 | fclose(fp); 48 | return -1; 49 | } 50 | fclose(fp); 51 | 52 | char command[SO_PATH_MAX_LEN * 2 + 200]; 53 | if (snprintf(command, sizeof(command), "gcc "LIBS" "CFLAGS" -o %s %s 2>&1", so_path, source_path) >= (int)sizeof(command)) { 54 | fprintf(stderr, "[ERROR] Command buffer overflow.\n"); 55 | unlink(source_path); 56 | return -1; 57 | } 58 | 59 | FILE *gcc_output = popen(command, "r"); 60 | if (gcc_output == NULL) { 61 | perror("Error running gcc"); 62 | unlink(source_path); 63 | return -1; 64 | } 65 | 66 | /* Read gcc output into the error buffer */ 67 | size_t bytes_read = 0; 68 | while (fgets(error_buffer + bytes_read, buffer_size - bytes_read, gcc_output) != NULL) { 69 | bytes_read = strlen(error_buffer); 70 | if (bytes_read >= buffer_size - 1) { 71 | break; 72 | } 73 | } 74 | 75 | int exit_code = pclose(gcc_output); 76 | if (exit_code != 0) { 77 | fprintf(stderr, "Compilation failed for %s -> %s\n", source_path, so_path); 78 | unlink(source_path); 79 | return -1; 80 | } 81 | 82 | unlink(source_path); 83 | return 0; 84 | } 85 | 86 | /** 87 | * Hash code to a SHA256 hash 88 | * Used to create a unique hash for each code snippet 89 | * @param code Code to hash 90 | * @return Hashed code 91 | */ 92 | static char* hash_code(char* code) { 93 | // Disable deprecation warning for SHA256, probably should fix this... 94 | #pragma GCC diagnostic push 95 | #pragma GCC diagnostic ignored "-Wdeprecated-declarations" 96 | char* hash_str = (char*)calloc(65, sizeof(char)); 97 | if (hash_str == NULL) { 98 | fprintf(stderr, "[ERROR] Memory allocation failed for hash_str.\n"); 99 | return NULL; 100 | } 101 | unsigned char hash[SHA256_DIGEST_LENGTH]; 102 | SHA256_CTX sha256; 103 | SHA256_Init(&sha256); 104 | SHA256_Update(&sha256, code, strlen(code)); 105 | SHA256_Final(hash, &sha256); 106 | 107 | for(int i = 0; i < SHA256_DIGEST_LENGTH; i++) { 108 | sprintf(hash_str + (i * 2), "%02x", hash[i]); 109 | } 110 | hash_str[64] = 0; 111 | 112 | #pragma GCC diagnostic pop 113 | 114 | return hash_str; 115 | } 116 | 117 | /** 118 | * Register a route with the given code 119 | * Creates a hash of the code and compiles it to a .so file 120 | * @param route Route to register 121 | * @param code Code to register 122 | * @param func_name Function name to call 123 | * @param method HTTP method 124 | * @return 0 on success, -1 on failure 125 | */ 126 | static int mgnt_register_module(struct http_response *req, char* code) { 127 | if(code == NULL ) { 128 | fprintf(stderr, "[ERROR] Code is not provided.\n"); 129 | return -1; 130 | } 131 | 132 | char* hash = hash_code(code); 133 | if(hash == NULL) { 134 | fprintf(stderr, "[ERROR] Failed to hash code\n"); 135 | return -1; 136 | } 137 | 138 | char filename[256]; 139 | snprintf(filename, sizeof(filename), "%s", hash); 140 | 141 | if (write_and_compile(filename, code, req->body, HTTP_RESPONSE_SIZE) == -1) { 142 | fprintf(stderr, "[ERROR] Failed to register '%s' due to compilation error.\n", filename); 143 | return -1; 144 | } 145 | 146 | char so_path[SO_PATH_MAX_LEN + 12]; 147 | snprintf(so_path, sizeof(so_path), "%s/%s.so", TMP_DIR, filename); 148 | 149 | free(hash); 150 | 151 | return route_register_module(so_path); 152 | } 153 | 154 | int mgnt_parse_request(struct http_request *req, struct http_response *res) { 155 | if (req->method == -1) { 156 | return -1; 157 | } 158 | 159 | return mgnt_register_module(res, map_get(req->data, "code"));; 160 | } 161 | -------------------------------------------------------------------------------- /src/pool.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "pool.h" 7 | #include 8 | 9 | /* Prototypes */ 10 | static void *thread_pool_worker(void *arg); 11 | 12 | /* Initialize the thread pool */ 13 | struct thread_pool *thread_pool_init(int num_threads) { 14 | struct thread_pool *pool = malloc(sizeof(struct thread_pool)); 15 | if (pool == NULL) { 16 | fprintf(stderr, "[ERROR] Failed to allocate memory for thread pool\n"); 17 | return NULL; 18 | } 19 | 20 | pool->num_threads = num_threads; 21 | pool->max_threads = num_threads; 22 | pool->task_queue = NULL; 23 | atomic_init(&pool->stop, 0); 24 | pool->queue_length = 0; 25 | 26 | pthread_mutex_init(&pool->lock, NULL); 27 | pthread_cond_init(&pool->cond, NULL); 28 | 29 | pool->threads = malloc(num_threads * sizeof(pthread_t)); 30 | if (pool->threads == NULL) { 31 | fprintf(stderr, "[ERROR] Failed to allocate memory for threads\n"); 32 | free(pool); 33 | return NULL; 34 | } 35 | 36 | for (int i = 0; i < num_threads; i++) { 37 | if (pthread_create(&pool->threads[i], NULL, thread_pool_worker, pool) != 0) { 38 | fprintf(stderr, "[ERROR] Failed to create thread %d\n", i); 39 | free(pool->threads); 40 | free(pool); 41 | return NULL; 42 | } 43 | } 44 | 45 | printf("[INFO] Initialized thread pool with %d threads\n", num_threads); 46 | 47 | return pool; 48 | } 49 | 50 | static void cleanup_unlock(void *arg) { 51 | pthread_mutex_unlock((pthread_mutex_t *)arg); 52 | } 53 | 54 | /* Worker thread function */ 55 | static void *thread_pool_worker(void *arg) { 56 | struct thread_pool *pool = (struct thread_pool *)arg; 57 | 58 | /* Ensure mutex is unlocked if the thread is cancelled. */ 59 | pthread_cleanup_push(cleanup_unlock, &pool->lock); 60 | 61 | while (1) { 62 | pthread_mutex_lock(&pool->lock); 63 | 64 | while (pool->task_queue == NULL && !atomic_load(&pool->stop)) { 65 | pthread_cond_wait(&pool->cond, &pool->lock); 66 | } 67 | 68 | if (atomic_load(&pool->stop) && pool->task_queue == NULL) { // Ensure no task remains when stopping. 69 | pthread_mutex_unlock(&pool->lock); 70 | break; 71 | } 72 | 73 | struct task *task = pool->task_queue; 74 | if (task != NULL) { 75 | pool->task_queue = task->next; 76 | pool->queue_length--; 77 | atomic_fetch_add(&pool->active_threads, 1); 78 | } 79 | 80 | pthread_mutex_unlock(&pool->lock); 81 | 82 | if (task != NULL && !atomic_load(&pool->stop)) { 83 | task->function(task->arg); 84 | atomic_fetch_sub(&pool->active_threads, 1); 85 | free(task); 86 | } 87 | } 88 | 89 | /* Unlocks mutex if thread is cancelled while holding the lock. */ 90 | pthread_cleanup_pop(1); 91 | return NULL; 92 | } 93 | 94 | int thread_pool_is_full(struct thread_pool *pool) { 95 | return atomic_load(&pool->active_threads) == pool->num_threads; 96 | } 97 | 98 | /* Add a task to the thread pool */ 99 | void thread_pool_add_task(struct thread_pool *pool, void (*function)(void *), void *arg) { 100 | struct task *task = malloc(sizeof(struct task)); 101 | if (task == NULL) { 102 | fprintf(stderr, "[ERROR] Failed to allocate memory for task\n"); 103 | return; 104 | } 105 | 106 | task->function = function; 107 | task->arg = arg; 108 | task->next = NULL; 109 | 110 | pthread_mutex_lock(&pool->lock); 111 | 112 | /* Add the task to the queue */ 113 | if (pool->task_queue == NULL) { 114 | pool->task_queue = task; 115 | } else { 116 | struct task *tmp = pool->task_queue; 117 | while (tmp->next != NULL) { 118 | tmp = tmp->next; 119 | } 120 | tmp->next = task; 121 | } 122 | 123 | pool->queue_length++; 124 | pthread_cond_signal(&pool->cond); 125 | pthread_mutex_unlock(&pool->lock); 126 | } 127 | 128 | 129 | /* Destroy the thread pool */ 130 | void thread_pool_destroy(struct thread_pool *pool) { 131 | printf("[INFO] Destroying thread pool\n"); 132 | 133 | pthread_mutex_lock(&pool->lock); 134 | atomic_store(&pool->stop, 1); 135 | 136 | printf("[INFO] Stopping all threads\n"); 137 | 138 | pthread_cond_broadcast(&pool->cond); 139 | pthread_mutex_unlock(&pool->lock); 140 | 141 | printf("[INFO] Joining threads\n"); 142 | 143 | for (int i = 0; i < pool->num_threads; i++) { 144 | int ret = pthread_join(pool->threads[i], NULL); 145 | if (ret != 0) { 146 | fprintf(stderr, "[ERROR] Failed to join thread %d: %s\n", i, strerror(ret)); 147 | } 148 | 149 | printf("[INFO] Thread %d joined\n", i); 150 | } 151 | 152 | printf("[INFO] All threads stopped\n"); 153 | 154 | free(pool->threads); 155 | pthread_mutex_destroy(&pool->lock); 156 | pthread_cond_destroy(&pool->cond); 157 | 158 | while (pool->task_queue != NULL) { 159 | struct task *tmp = pool->task_queue; 160 | pool->task_queue = tmp->next; 161 | free(tmp); 162 | } 163 | 164 | free(pool); 165 | printf("[INFO] Thread pool destroyed\n"); 166 | } -------------------------------------------------------------------------------- /src/queue.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | struct queue* queue_create(size_t max_size) { 6 | struct queue* q = (struct queue*)malloc(sizeof(struct queue)); 7 | if (q == NULL) { 8 | return NULL; 9 | } 10 | 11 | q->buffer = (void**)malloc(max_size * sizeof(void*)); 12 | if (q->buffer == NULL) { 13 | free(q); 14 | return NULL; 15 | } 16 | 17 | /* Initialize queue */ 18 | q->head = 0; 19 | q->tail = 0; 20 | q->max_size = max_size; 21 | q->current_size = 0; 22 | 23 | /* Initialize mutex and condition variables */ 24 | pthread_mutex_init(&q->lock, NULL); 25 | pthread_cond_init(&q->not_full, NULL); 26 | pthread_cond_init(&q->not_empty, NULL); 27 | return q; 28 | } 29 | 30 | void queue_destroy(struct queue* q) { 31 | pthread_mutex_destroy(&q->lock); 32 | pthread_cond_destroy(&q->not_full); 33 | pthread_cond_destroy(&q->not_empty); 34 | free(q->buffer); 35 | free(q); 36 | } 37 | 38 | queue_status_t queue_enqueue(struct queue* q, void* item) { 39 | pthread_mutex_lock(&q->lock); 40 | 41 | /* Wait until queue is not full */ 42 | while (q->current_size == q->max_size) { 43 | pthread_cond_wait(&q->not_full, &q->lock); 44 | } 45 | 46 | /* Enqueue item at tail and move pointer */ 47 | q->buffer[q->tail] = item; 48 | q->tail = (q->tail + 1) % q->max_size; 49 | q->current_size++; 50 | 51 | pthread_cond_signal(&q->not_empty); 52 | pthread_mutex_unlock(&q->lock); 53 | return QUEUE_OK; 54 | } 55 | 56 | queue_status_t queue_dequeue(struct queue* q, void** item) { 57 | pthread_mutex_lock(&q->lock); 58 | while (q->current_size == 0) { 59 | pthread_cond_wait(&q->not_empty, &q->lock); 60 | } 61 | 62 | /* Dequeue item at head and move pointer */ 63 | *item = q->buffer[q->head]; 64 | q->head = (q->head + 1) % q->max_size; 65 | q->current_size--; 66 | 67 | pthread_cond_signal(&q->not_full); 68 | pthread_mutex_unlock(&q->lock); 69 | return QUEUE_OK; 70 | } 71 | 72 | size_t queue_size(struct queue* q) { 73 | pthread_mutex_lock(&q->lock); 74 | size_t size = q->current_size; 75 | pthread_mutex_unlock(&q->lock); 76 | return size; 77 | } -------------------------------------------------------------------------------- /src/router.c: -------------------------------------------------------------------------------- 1 | #include "http.h" 2 | #include "cweb.h" 3 | #include "router.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #define MODULE_TAG "config" 12 | #define ROUTE_FILE "modules/routes.dat" 13 | 14 | /* Routes depends on this function from ws */ 15 | void ws_force_close(struct ws_info *info); 16 | int ws_update_container(const char* path, struct ws_info *info); 17 | 18 | struct route_disk_header { 19 | char magic[5]; 20 | int count; 21 | }; 22 | pthread_mutex_t save_mutex = PTHREAD_MUTEX_INITIALIZER; 23 | 24 | struct gateway { 25 | struct gateway_entry entries[100]; 26 | pthread_rwlock_t rwlock; 27 | int count; 28 | } gateway = { 29 | .rwlock = PTHREAD_RWLOCK_INITIALIZER, 30 | .count = 0 31 | }; 32 | 33 | static int route_save_to_disk(char* filename); 34 | static int route_load_from_disk(char* filename); 35 | 36 | static struct gateway_entry* find_gateway_entry(const char* module) { 37 | for (int i = 0; i < gateway.count; i++) { 38 | if (strcmp(gateway.entries[i].module->name, module) == 0) { 39 | return &gateway.entries[i]; 40 | } 41 | } 42 | return NULL; 43 | } 44 | 45 | /* Resolve symbol from shared object */ 46 | void* resolv(const char* module, const char* symbol) { 47 | /* Find module */ 48 | struct gateway_entry* entry = find_gateway_entry(module); 49 | if (!entry) { 50 | return NULL; 51 | } 52 | 53 | /* Find symbol */ 54 | void* sym = dlsym(entry->handle, symbol); 55 | if (!sym) { 56 | fprintf(stderr, "Error resolving symbol: %s\n", dlerror()); 57 | return NULL; 58 | } 59 | 60 | return sym; 61 | } 62 | 63 | int route_gateway_json(struct http_response* res){ 64 | json_t *root = json_object(); 65 | json_t *modules = json_array(); 66 | 67 | for (int i = 0; i < gateway.count; i++) { 68 | json_t *module = json_object(); 69 | json_object_set_new(module, "name", json_string(gateway.entries[i].module->name)); 70 | json_object_set_new(module, "author", json_string(gateway.entries[i].module->author)); 71 | json_object_set_new(module, "path", json_string(gateway.entries[i].so_path)); 72 | 73 | json_t *routes = json_array(); 74 | for (int j = 0; j < gateway.entries[i].module->size; j++) { 75 | json_t *route = json_object(); 76 | json_object_set_new(route, "method", json_string(gateway.entries[i].module->routes[j].method)); 77 | json_object_set_new(route, "path", json_string(gateway.entries[i].module->routes[j].path)); 78 | json_array_append_new(routes, route); 79 | } 80 | json_object_set_new(module, "routes", routes); 81 | 82 | 83 | json_t *websockets = json_array(); 84 | for (int j = 0; j < gateway.entries[i].module->ws_size; j++) { 85 | json_t *ws_route = json_object(); 86 | json_object_set_new(ws_route, "path", json_string(gateway.entries[i].module->websockets[j].path)); 87 | json_array_append_new(websockets, ws_route); 88 | } 89 | 90 | json_object_set_new(module, "websockets", websockets); 91 | 92 | json_array_append_new(modules, module); 93 | } 94 | 95 | json_object_set_new(root, "modules", modules); 96 | char *json_str = json_dumps(root, JSON_INDENT(2)); 97 | json_decref(root); 98 | 99 | map_insert(res->headers, "Content-Type", "application/json"); 100 | map_insert(res->headers, "Access-Control-Allow-Origin", "*"); 101 | res->body = json_str; 102 | res->content_length = strlen(json_str); 103 | res->status = HTTP_200_OK; 104 | 105 | return 0; 106 | } 107 | 108 | /* Find route with regex pattern matching included. */ 109 | struct route route_find(char *route, char *method) { 110 | pthread_rwlock_rdlock(&gateway.rwlock); 111 | for (int i = 0; i < gateway.count; i++) { 112 | pthread_rwlock_rdlock(&gateway.entries[i].rwlock); 113 | for (int j = 0; j < gateway.entries[i].module->size; j++) { 114 | struct route_info *entry = &gateway.entries[i].module->routes[j]; 115 | if (entry->path == NULL || entry->method == NULL) { 116 | continue; 117 | } 118 | 119 | /* Check if method and path matches */ 120 | if (strcmp(method, entry->method) == 0) { 121 | regex_t regex; 122 | char anchored_pattern[1024]; 123 | 124 | /* Create an anchored regex pattern, else partial paths will be matched... */ 125 | snprintf(anchored_pattern, sizeof(anchored_pattern), "^%s$", entry->path); 126 | if (regcomp(®ex, anchored_pattern, REG_EXTENDED | REG_NOSUB) != 0) { 127 | fprintf(stderr, "Invalid regex pattern: %s\n", anchored_pattern); 128 | continue; 129 | } 130 | 131 | int match = regexec(®ex, route, 0, NULL, 0); 132 | regfree(®ex); 133 | if (match == 0) { 134 | pthread_rwlock_unlock(&gateway.rwlock); 135 | /* Caller is responsible for unlocking the route read lock! */ 136 | return (struct route){ 137 | .route = entry, 138 | .rwlock = &gateway.entries[i].rwlock 139 | }; 140 | } 141 | } 142 | } 143 | pthread_rwlock_unlock(&gateway.entries[i].rwlock); 144 | } 145 | pthread_rwlock_unlock(&gateway.rwlock); 146 | return (struct route){0}; 147 | } 148 | 149 | struct ws_route ws_route_find(char *route) { 150 | pthread_rwlock_rdlock(&gateway.rwlock); 151 | for (int i = 0; i < gateway.count; i++) { 152 | pthread_rwlock_rdlock(&gateway.entries[i].rwlock); 153 | for (int j = 0; j < gateway.entries[i].module->ws_size; j++) { 154 | if (strcmp(gateway.entries[i].module->websockets[j].path, route) == 0) { 155 | /* Caller is responsible for unlocking the read lock! */ 156 | return (struct ws_route){ 157 | .info = &gateway.entries[i].module->websockets[j], 158 | .rwlock = &gateway.entries[i].rwlock 159 | }; 160 | } 161 | } 162 | pthread_rwlock_unlock(&gateway.entries[i].rwlock); 163 | } 164 | pthread_rwlock_unlock(&gateway.rwlock); 165 | return (struct ws_route){0}; 166 | } 167 | 168 | static int update_gateway_entry(int index, char* so_path, struct module* routes, void* handle) { 169 | pthread_rwlock_wrlock(&gateway.entries[index].rwlock); 170 | 171 | void* old_handle = gateway.entries[index].handle; 172 | 173 | /* Unload old module */ 174 | if (gateway.entries[index].module && gateway.entries[index].module->unload) { 175 | gateway.entries[index].module->unload(); 176 | } 177 | 178 | /* Update entry */ 179 | gateway.entries[index].handle = handle; 180 | gateway.entries[index].module = routes; 181 | 182 | /* Update all websocket connections */ 183 | for(int i = 0; gateway.entries[index].module && i < gateway.entries[index].module->ws_size; i++) { 184 | /** 185 | * TODO: What if we do want to close the websocket connections? 186 | * Currently the module has to do that itself on unload. 187 | */ 188 | //ws_force_close(&gateway.entries[index].module->websockets[i]); 189 | ws_update_container(gateway.entries[index].module->websockets[i].path, &gateway.entries[index].module->websockets[i]); 190 | } 191 | 192 | /* Close old handle */ 193 | if (old_handle){ 194 | unlink(gateway.entries[index].so_path); 195 | dlclose(old_handle); 196 | } 197 | 198 | memset(gateway.entries[index].so_path, 0, SO_PATH_MAX_LEN); 199 | strncpy(gateway.entries[index].so_path, so_path, SO_PATH_MAX_LEN); 200 | 201 | /* Load new module */ 202 | if (gateway.entries[index].module->onload) { 203 | gateway.entries[index].module->onload(); 204 | } 205 | 206 | pthread_rwlock_unlock(&gateway.entries[index].rwlock); 207 | printf("[INFO ] Module %s is updated.\n", routes->name); 208 | return 0; 209 | } 210 | 211 | static void* load_shared_object(char* so_path){ 212 | void* handle = dlopen(so_path, RTLD_LAZY); 213 | if (!handle) { 214 | fprintf(stderr, "[ERROR] Error loading shared object: %s\n", dlerror()); 215 | return NULL; 216 | } 217 | return handle; 218 | } 219 | 220 | static int load_from_shared_object(char* so_path){ 221 | void* handle = load_shared_object(so_path); 222 | if (!handle) { 223 | return -1; 224 | } 225 | 226 | struct module* module = dlsym(handle, MODULE_TAG); 227 | if (!module || strnlen(module->name, 128) == 0) { 228 | fprintf(stderr, "[ERROR] Error finding module definition: %s\n", dlerror()); 229 | dlclose(handle); 230 | return -1; 231 | } 232 | 233 | /* Check if gateway is full */ 234 | if (gateway.count >= 100) { 235 | fprintf(stderr, "[ERROR] Gateway is full\n"); 236 | dlclose(handle); 237 | return -1; 238 | } 239 | 240 | pthread_rwlock_rdlock(&gateway.rwlock); 241 | 242 | /* Check if module already exists */ 243 | for (int i = 0; i < gateway.count; i++) { 244 | if (strcmp(gateway.entries[i].module->name, module->name) == 0) { 245 | /* Update existing entry, if so_path differ */ 246 | if(strcmp(gateway.entries[i].so_path, so_path) == 0) { 247 | return 0; 248 | } 249 | int ret = update_gateway_entry(i, so_path, module, handle); 250 | 251 | pthread_rwlock_unlock(&gateway.rwlock); 252 | return ret; 253 | } 254 | } 255 | 256 | printf("[INFO ] Module %s is loaded.\n", module->name); 257 | 258 | /* Only handle route conflicts on new modules */ 259 | for (int i = 0; i < module->size; i++) { 260 | struct route route = route_find((char*)module->routes[i].path, (char*)module->routes[i].method); 261 | if (route.route) { 262 | pthread_rwlock_unlock(route.rwlock); 263 | pthread_rwlock_unlock(&gateway.rwlock); 264 | fprintf(stderr, "[ERROR] Route conflict: %s %s - \n", module->routes[i].method, module->routes[i].path); 265 | dlclose(handle); 266 | return -1; 267 | } 268 | } 269 | 270 | pthread_rwlock_init(&gateway.entries[gateway.count].rwlock, NULL); 271 | update_gateway_entry(gateway.count, so_path, module, handle); 272 | gateway.count++; 273 | 274 | pthread_rwlock_unlock(&gateway.rwlock); 275 | 276 | return 0; 277 | } 278 | 279 | int route_register_module(char* so_path) { 280 | return load_from_shared_object(so_path); 281 | } 282 | 283 | void route_cleanup() { 284 | for (int i = 0; i < gateway.count; i++) { 285 | pthread_rwlock_wrlock(&gateway.entries[i].rwlock); 286 | dlclose(gateway.entries[i].handle); 287 | pthread_rwlock_unlock(&gateway.entries[i].rwlock); 288 | pthread_rwlock_destroy(&gateway.entries[i].rwlock); 289 | } 290 | 291 | } 292 | 293 | static int route_save_to_disk(char* filename) { 294 | int ret; 295 | pthread_mutex_lock(&save_mutex); 296 | 297 | FILE *fp = fopen(filename, "wb"); 298 | if (fp == NULL) { 299 | perror("Error creating route file"); 300 | pthread_mutex_unlock(&save_mutex); 301 | return -1; 302 | } 303 | 304 | struct route_disk_header header = { 305 | .magic = "CWEB", 306 | .count = gateway.count 307 | }; 308 | ret = fwrite(&header, sizeof(struct route_disk_header), 1, fp); 309 | if(ret != 1) { 310 | fprintf(stderr, "Error writing route file header\n"); 311 | fclose(fp); 312 | pthread_mutex_unlock(&save_mutex); 313 | return -1; 314 | } 315 | 316 | for (int i = 0; i < gateway.count; i++) { 317 | ret = fwrite(gateway.entries[i].so_path, SO_PATH_MAX_LEN, 1, fp); 318 | if(ret != 1) { 319 | fprintf(stderr, "Error writing route file entry\n"); 320 | fclose(fp); 321 | pthread_mutex_unlock(&save_mutex); 322 | return -1; 323 | } 324 | } 325 | 326 | fclose(fp); 327 | pthread_mutex_unlock(&save_mutex); 328 | return 0; 329 | } 330 | 331 | static int route_load_from_disk(char* filename) { 332 | int ret; 333 | pthread_mutex_lock(&save_mutex); 334 | FILE *fp = fopen(filename, "rb"); 335 | if (fp == NULL) { 336 | perror("Error opening route file"); 337 | pthread_mutex_unlock(&save_mutex); 338 | return -1; 339 | } 340 | 341 | struct route_disk_header header; 342 | ret = fread(&header, sizeof(struct route_disk_header), 1, fp); 343 | if(ret != 1) { 344 | fprintf(stderr, "Error reading route file header\n"); 345 | fclose(fp); 346 | pthread_mutex_unlock(&save_mutex); 347 | return -1; 348 | } 349 | 350 | if(strcmp(header.magic, "CWEB") != 0) { 351 | fprintf(stderr, "Invalid route file\n"); 352 | fclose(fp); 353 | return -1; 354 | } 355 | 356 | for (int i = 0; i < header.count; i++) { 357 | char so_path[SO_PATH_MAX_LEN]; 358 | ret = fread(so_path, SO_PATH_MAX_LEN, 1, fp); 359 | if(ret != 1) { 360 | fprintf(stderr, "Error reading route file entry\n"); 361 | fclose(fp); 362 | pthread_mutex_unlock(&save_mutex); 363 | return -1; 364 | } 365 | 366 | route_register_module(so_path); 367 | } 368 | 369 | fclose(fp); 370 | pthread_mutex_unlock(&save_mutex); 371 | return 0; 372 | } 373 | 374 | __attribute__((constructor)) void route_init() { 375 | /* Load libmap.so dependency - Only needed for Linux, perhaps wrap in #ifdef */ 376 | void *map_handle = dlopen("./libs/libmodule.so", RTLD_GLOBAL | RTLD_LAZY); 377 | if (!map_handle) { 378 | fprintf(stderr, "Error loading dependency libmap: %s\n", dlerror()); 379 | return; 380 | } 381 | 382 | route_load_from_disk(ROUTE_FILE); 383 | 384 | printf("[STARTUP] Router initialized\n"); 385 | } 386 | 387 | __attribute__((destructor)) void route_close() { 388 | route_save_to_disk(ROUTE_FILE); 389 | route_cleanup(); 390 | printf("[SHUTDOWN] Router closed\n"); 391 | } -------------------------------------------------------------------------------- /src/scheduler.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | static int add_work(work_t work, void *data, worker_state_t state); 8 | 9 | struct scheduler internal_scheduler = { 10 | .queue = NULL, 11 | .size = 0, 12 | .capacity = 10, 13 | .mutex = PTHREAD_MUTEX_INITIALIZER, 14 | .add = add_work, 15 | }; 16 | struct scheduler *exposed_scheduler = &internal_scheduler; 17 | 18 | static pthread_t scheduler_thread; 19 | static pthread_cond_t work_available = PTHREAD_COND_INITIALIZER; 20 | static volatile atomic_int running = 1; 21 | 22 | static int add_work(work_t work, void *data, worker_state_t state) { 23 | struct work *new_work = (struct work *)malloc(sizeof(struct work)); 24 | if (new_work == NULL) { 25 | perror("Error allocating memory for work"); 26 | return -1; 27 | } 28 | 29 | /* TODO: Implement the choice of state... */ 30 | (void)state; 31 | 32 | new_work->work = work; 33 | new_work->data = data; 34 | new_work->next = NULL; 35 | 36 | pthread_mutex_lock(&internal_scheduler.mutex); 37 | if (internal_scheduler.size == 0) { 38 | internal_scheduler.queue = new_work; 39 | } else { 40 | struct work *current = internal_scheduler.queue; 41 | while (current->next != NULL) { 42 | current = current->next; 43 | } 44 | current->next = new_work; 45 | } 46 | internal_scheduler.size++; 47 | pthread_mutex_unlock(&internal_scheduler.mutex); 48 | 49 | /* use this to signal the thread that new work is available */ 50 | pthread_cond_signal(&work_available); 51 | 52 | return 0; 53 | } 54 | 55 | static void* scheduler_thread_function(void *arg) { 56 | (void)arg; 57 | 58 | while (running) { 59 | pthread_mutex_lock(&internal_scheduler.mutex); 60 | 61 | /* Wait until the add_work functions signals that new work is avaiable. */ 62 | while (internal_scheduler.size == 0 && running) { 63 | /* Avoid constantly running */ 64 | pthread_cond_wait(&work_available, &internal_scheduler.mutex); 65 | } 66 | 67 | if (!running) { 68 | pthread_mutex_unlock(&internal_scheduler.mutex); 69 | break; 70 | } 71 | 72 | struct work *current = internal_scheduler.queue; 73 | internal_scheduler.queue = current->next; 74 | internal_scheduler.size--; 75 | 76 | pthread_mutex_unlock(&internal_scheduler.mutex); 77 | 78 | current->work(current->data); 79 | free(current); 80 | } 81 | 82 | return NULL; 83 | } 84 | 85 | __attribute__((constructor)) void scheduler_init() { 86 | if (pthread_create(&scheduler_thread, NULL, scheduler_thread_function, NULL) != 0) { 87 | perror("Error creating scheduler thread"); 88 | exit(EXIT_FAILURE); 89 | } 90 | /* We dont want to pthread_detach(scheduler_thread); as the pthread_join in the destructor.*/ 91 | } 92 | 93 | __attribute__((destructor)) void scheduler_destroy() { 94 | running = 0; 95 | pthread_cond_signal(&work_available); 96 | pthread_join(scheduler_thread, NULL); 97 | printf("[SHUTDOWN] Scheduler destroyed\n"); 98 | } -------------------------------------------------------------------------------- /src/server.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include "http.h" 21 | #include "router.h" 22 | #include "cweb.h" 23 | #include "map.h" 24 | #include "db.h" 25 | #include "scheduler.h" 26 | #include "pool.h" 27 | #include "crypto.h" 28 | 29 | #define DEFAULT_PORT 8080 30 | #define ACCEPT_BACKLOG 128 31 | #define MODULE_URL "/mgnt" 32 | 33 | /* Feature for later... */ 34 | static const char* allowed_management_commands[] = { 35 | "reload", 36 | "shutdown", 37 | "status", 38 | "routes", 39 | "modules", 40 | "help" 41 | }; 42 | 43 | static const char* allowed_ip_prefixes[] = { 44 | "192.168.", 45 | "10.0.", 46 | "172.16." 47 | }; 48 | 49 | struct cidr_prefix { 50 | uint32_t prefix; 51 | uint8_t prefix_len; 52 | }; 53 | 54 | struct connection { 55 | int sockfd; 56 | struct sockaddr_in address; 57 | SSL *ssl; 58 | }; 59 | 60 | static struct thread_pool *pool; 61 | 62 | typedef enum env { 63 | DEV, 64 | PROD 65 | } env_t; 66 | 67 | static struct server_config { 68 | uint16_t port; 69 | int thread_pool_size; 70 | char silent_mode; 71 | env_t environment; 72 | } config = { 73 | .port = DEFAULT_PORT, 74 | .thread_pool_size = 0, 75 | .silent_mode = 0, 76 | #ifdef PRODUCTION 77 | .environment = PROD 78 | #else 79 | .environment = DEV 80 | #endif 81 | }; 82 | 83 | // static int parse_cidr(const char *cidr_str, struct cidr_prefix *result) { 84 | // char ip[INET_ADDRSTRLEN]; 85 | // int prefix_len; 86 | 87 | // if (sscanf(cidr_str, "%15[^/]/%d", ip, &prefix_len) != 2) { 88 | // fprintf(stderr, "Invalid CIDR format: %s\n", cidr_str); 89 | // return -1; 90 | // } 91 | 92 | // if (prefix_len < 0 || prefix_len > 32) { 93 | // fprintf(stderr, "Invalid prefix length: %d\n", prefix_len); 94 | // return -1; 95 | // } 96 | 97 | // struct in_addr addr; 98 | // if (inet_pton(AF_INET, ip, &addr) != 1) { 99 | // fprintf(stderr, "Invalid IP address: %s\n", ip); 100 | // return -1; 101 | // } 102 | 103 | // result->prefix = ntohl(addr.s_addr); // Convert to host byte order 104 | // result->prefix_len = (uint8_t)prefix_len; 105 | 106 | // return 0; 107 | // } 108 | 109 | /* TODO: Ugly fix to allow server access to these.. */ 110 | void ws_handle_client(int sd, struct http_request *req, struct http_response *res, struct ws_info *ws_module_info); 111 | int ws_confirm_open(int sd); 112 | 113 | #ifdef PRODUCTION 114 | #endif 115 | 116 | static struct connection server_init(uint16_t port) { 117 | struct connection s; 118 | s.sockfd = socket(AF_INET, SOCK_STREAM, 0); 119 | if (s.sockfd == 0) { 120 | perror("Socket failed"); 121 | exit(EXIT_FAILURE); 122 | } 123 | 124 | int opt = 1; 125 | if (setsockopt(s.sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { 126 | perror("setsockopt failed"); 127 | close(s.sockfd); 128 | exit(EXIT_FAILURE); 129 | } 130 | 131 | s.address.sin_family = AF_INET; 132 | s.address.sin_addr.s_addr = INADDR_ANY; 133 | s.address.sin_port = htons(port); 134 | 135 | if (bind(s.sockfd, (struct sockaddr *)&s.address, sizeof(s.address)) < 0) { 136 | perror("Bind failed"); 137 | close(s.sockfd); 138 | exit(EXIT_FAILURE); 139 | } 140 | 141 | if (listen(s.sockfd, ACCEPT_BACKLOG) < 0) { 142 | perror("Listen failed"); 143 | close(s.sockfd); 144 | exit(EXIT_FAILURE); 145 | } 146 | 147 | return s; 148 | } 149 | 150 | static struct connection* server_accept(struct connection s) { 151 | struct connection *c = (struct connection *)malloc(sizeof(struct connection)); 152 | memset(c, 0, sizeof(struct connection)); 153 | if (c == NULL) { 154 | perror("Error allocating memory for client"); 155 | close(s.sockfd); 156 | exit(EXIT_FAILURE); 157 | } 158 | 159 | int addrlen = sizeof(s.address); 160 | c->sockfd = accept(s.sockfd, (struct sockaddr *)&s.address, (socklen_t *)&addrlen); 161 | if (c->sockfd < 0) { 162 | perror("Accept failed"); 163 | free(c); 164 | return NULL; 165 | } 166 | 167 | #ifdef PRODUCTION 168 | c->ssl = SSL_new(ssl_ctx); 169 | if (!c->ssl) { 170 | perror("[ERROR] SSL initialization failed"); 171 | free(c); 172 | return NULL; 173 | } 174 | 175 | SSL_set_fd(c->ssl, c->sockfd); 176 | if (SSL_accept(c->ssl) <= 0) { 177 | perror("[ERROR] SSL handshake failed"); 178 | SSL_free(c->ssl); 179 | close(c->sockfd); 180 | free(c); 181 | return NULL; 182 | } 183 | #endif 184 | 185 | return c; 186 | } 187 | 188 | static int gateway(int fd, struct http_request *req, struct http_response *res) { 189 | if (strncmp(req->path, "/favicon.ico", 12) == 0) { 190 | res->status = HTTP_404_NOT_FOUND; 191 | snprintf(res->body, HTTP_RESPONSE_SIZE, "404 Not Found\n"); 192 | return 0; 193 | } 194 | 195 | if(strncmp(req->path, MODULE_URL, 6) == 0) { 196 | if (req->method == HTTP_GET) { 197 | route_gateway_json(res); 198 | } else if (req->method == HTTP_POST) { 199 | if (mgnt_parse_request(req, res) >= 0) { 200 | res->status = HTTP_200_OK; 201 | } else { 202 | res->status = HTTP_500_INTERNAL_SERVER_ERROR; 203 | } 204 | } else { 205 | res->status = HTTP_405_METHOD_NOT_ALLOWED; 206 | } 207 | return 0; 208 | } 209 | 210 | if(http_is_websocket_upgrade(req)) { 211 | struct ws_route ws = ws_route_find(req->path); 212 | if (ws.info == NULL) { 213 | res->status = HTTP_404_NOT_FOUND; 214 | snprintf(res->body, HTTP_RESPONSE_SIZE, "404 Not Found\n"); 215 | return 0; 216 | } 217 | 218 | /* Upgrade to websocket */ 219 | ws_handle_client(fd, req, res, ws.info); 220 | 221 | pthread_rwlock_unlock(ws.rwlock); 222 | 223 | return 0; 224 | } 225 | 226 | struct route r = route_find(req->path, (char*)http_methods[req->method]); 227 | if (r.route == NULL) { 228 | res->status = HTTP_404_NOT_FOUND; 229 | snprintf(res->body, HTTP_RESPONSE_SIZE, "404 Not Found\n"); 230 | return 0; 231 | } 232 | 233 | safe_execute_handler(r.route->handler, req, res); 234 | 235 | /* Release the read lock after handler execution */ 236 | pthread_rwlock_unlock(r.rwlock); 237 | return 0; 238 | } 239 | 240 | /* Build headers for response */ 241 | static void build_headers(struct http_response *res, char *headers, int headers_size) { 242 | struct map *headers_map = res->headers; 243 | int headers_len = 0; 244 | for (size_t i = 0; i < map_size(headers_map); i++) { 245 | int written = snprintf(headers + headers_len, headers_size - headers_len, "%s: %s\r\n", headers_map->entries[i].key, (char*)headers_map->entries[i].value); 246 | if (written < 0 || written >= headers_size - headers_len) { 247 | fprintf(stderr, "[ERROR] Header buffer overflow\n"); 248 | break; 249 | } 250 | headers_len += written; 251 | } 252 | } 253 | 254 | static void measure_time(struct timespec *start, struct timespec *end, double *time_taken) { 255 | clock_gettime(CLOCK_MONOTONIC, end); 256 | *time_taken = (end->tv_sec - start->tv_sec) * 1e9; 257 | *time_taken = (*time_taken + (end->tv_nsec - start->tv_nsec)) * 1e-9; 258 | } 259 | 260 | static void thread_clean_up_request(struct http_request *req) { 261 | if (req->body) free(req->body); 262 | if (req->path) free(req->path); 263 | 264 | if(req->params != NULL){ 265 | for (size_t i = 0; i < map_size(req->params); i++) { 266 | free(req->params->entries[i].value); 267 | } 268 | map_destroy(req->params); 269 | } 270 | 271 | if(req->headers != NULL){ 272 | for (size_t i = 0; i < map_size(req->headers); i++) { 273 | free(req->headers->entries[i].value); 274 | } 275 | map_destroy(req->headers); 276 | } 277 | 278 | if(req->data != NULL){ 279 | for (size_t i = 0; i < map_size(req->data); i++) { 280 | free(req->data->entries[i].value); 281 | } 282 | map_destroy(req->data); 283 | } 284 | } 285 | 286 | static void thread_clean_up(struct http_request *req, struct http_response *res) { 287 | thread_clean_up_request(req); 288 | map_destroy(res->headers); 289 | free(res->body); 290 | } 291 | 292 | static void thread_set_timeout(int sockfd, int seconds) { 293 | struct timeval tv; 294 | tv.tv_sec = seconds; 295 | tv.tv_usec = 0; 296 | setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv); 297 | } 298 | 299 | static void thread_clear_timeout(int sockfd) { 300 | struct timeval tv; 301 | tv.tv_sec = 0; 302 | tv.tv_usec = 0; 303 | setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv); 304 | } 305 | 306 | static void thread_handle_client(void *arg) { 307 | int ret; 308 | struct connection *c = (struct connection *)arg; 309 | thread_set_timeout(c->sockfd, 2); 310 | 311 | while(1){ 312 | int close_connection = 0; 313 | 314 | /* Read initial request */ 315 | char buffer[8*1024] = {0}; 316 | #ifdef PRODUCTION 317 | int read_size = SSL_read(c->ssl, buffer, sizeof(buffer) - 1); 318 | if (read_size <= 0) { 319 | if (SSL_get_error(c->ssl, read_size) == SSL_ERROR_ZERO_RETURN) { 320 | break; // Client closed the connection 321 | } 322 | perror("[ERROR] SSL read failed"); 323 | break; 324 | } 325 | #else 326 | int read_size = read(c->sockfd, buffer, sizeof(buffer) - 1); 327 | if (read_size <= 0) { 328 | perror("[ERROR] Read failed"); 329 | break; 330 | } 331 | #endif 332 | 333 | buffer[read_size] = '\0'; 334 | 335 | struct timespec start, end; 336 | clock_gettime(CLOCK_MONOTONIC, &start); 337 | 338 | struct http_request req; 339 | req.tid = pthread_self(); 340 | 341 | http_parse(buffer, &req); 342 | if(req.method == HTTP_ERR) { 343 | dprintf(c->sockfd, "HTTP/1.1 %s\r\nContent-Length: 0\r\n\r\n", http_errors[req.status]); 344 | thread_clean_up_request(&req); 345 | goto thread_handle_client_exit; 346 | } 347 | 348 | /* Read the rest of the request */ 349 | while (req.content_length > read_size) { 350 | #ifdef PRODUCTION 351 | ret = SSL_read(c->ssl, buffer + read_size, sizeof(buffer) - read_size - 1); 352 | #else 353 | ret = read(c->sockfd, buffer + read_size, sizeof(buffer) - read_size - 1); 354 | #endif 355 | if (ret <= 0) { 356 | break; 357 | } 358 | 359 | read_size += ret; 360 | buffer[read_size] = '\0'; 361 | } 362 | char* body_ptr = strstr(buffer, "\r\n\r\n"); 363 | if (body_ptr) { 364 | req.body = strdup(body_ptr + 4); 365 | } else { 366 | req.body = strdup(""); 367 | } 368 | 369 | http_parse_data(&req); 370 | close_connection = req.close; 371 | 372 | struct http_response res; 373 | res.headers = map_create(32); 374 | if (res.headers == NULL) { 375 | perror("[ERROR] Error creating map"); 376 | goto thread_handle_client_exit; 377 | } 378 | 379 | res.body = (char *)malloc(HTTP_RESPONSE_SIZE); 380 | if (res.body == NULL) { 381 | perror("[ERROR] Error allocating memory for response body"); 382 | goto thread_handle_client_exit; 383 | } 384 | 385 | gateway(c->sockfd, &req, &res); 386 | 387 | /* If all threads are in use, send close */ 388 | if(thread_pool_is_full(pool)) { 389 | map_insert(res.headers, "Connection", "close"); 390 | close_connection = 1; 391 | } 392 | 393 | /* Servers MUST include a valid Date header in HTTP responses. */ 394 | time_t now = time(NULL); 395 | struct tm tm; 396 | gmtime_r(&now, &tm); 397 | char date[128]; 398 | strftime(date, sizeof(date), "%a, %d %b %Y %H:%M:%S GMT", &tm); 399 | map_insert(res.headers, "Date", strdup(date)); 400 | 401 | char headers[4*1024] = {0}; 402 | char response[8*1024] = {0}; 403 | build_headers(&res, headers, sizeof(headers)); 404 | snprintf(response, sizeof(response), HTTP_VERSION" %s\r\n%sContent-Length: %lu\r\n\r\n%s", http_errors[res.status], headers, strlen(res.body), res.body); 405 | 406 | #ifdef PRODUCTION 407 | if (SSL_write(c->ssl, response, strlen(response)) <= 0) { 408 | perror("[ERROR] SSL write failed"); 409 | break; 410 | } 411 | #else 412 | if (write(c->sockfd, response, strlen(response)) <= 0) { 413 | perror("[ERROR] Write failed"); 414 | break; 415 | } 416 | #endif 417 | 418 | double time_taken; 419 | measure_time(&start, &end, &time_taken); 420 | 421 | if (!config.silent_mode) 422 | printf("[%ld] %s - Request %s %s took %f seconds.\n", (long)req.tid, http_errors[res.status], http_methods[req.method], req.path, time_taken); 423 | 424 | /* Ugly hacks */ 425 | char* ac = map_get(res.headers, "Sec-WebSocket-Accept"); 426 | if (ac) free(ac); 427 | char* dt = map_get(res.headers, "Date"); 428 | if (dt) free(dt); 429 | 430 | thread_clean_up(&req, &res); 431 | if (req.websocket) { 432 | thread_clear_timeout(c->sockfd); 433 | ws_confirm_open(c->sockfd); 434 | return; /* Websocket connection is handled by the websocket thread */ 435 | } 436 | 437 | /** 438 | * HTTP/1.1 connections MUST be persistent by default unless a Connection: close header is explicitly included. 439 | */ 440 | if (close_connection) { 441 | goto thread_handle_client_exit; 442 | } 443 | } 444 | thread_handle_client_exit: 445 | 446 | #ifdef PRODUCTION 447 | SSL_shutdown(c->ssl); 448 | SSL_free(c->ssl); 449 | #endif 450 | close(c->sockfd); 451 | free(c); 452 | return; 453 | } 454 | 455 | #define INIT_OPTIONS (OPENSSL_INIT_NO_ATEXIT) 456 | static void openssl_init_wrapper(void) { 457 | if (OPENSSL_init_crypto(INIT_OPTIONS, NULL) == 0) { 458 | fprintf(stderr, "[ERROR] Failed to initialize OpenSSL\n"); 459 | exit(EXIT_FAILURE); 460 | } 461 | } 462 | 463 | /* Signal handler */ 464 | static volatile sig_atomic_t stop = 0; 465 | static void server_signal_handler(int sig) { 466 | (void)sig; 467 | stop = 1; 468 | } 469 | 470 | #include 471 | 472 | int main(int argc, char *argv[]) { 473 | (void)allowed_management_commands; 474 | (void)allowed_ip_prefixes; 475 | 476 | int opt; 477 | while ((opt = getopt(argc, argv, "p:t:s")) != -1) { 478 | switch (opt) { 479 | case 'p': 480 | config.port = atoi(optarg); 481 | break; 482 | case 't': 483 | config.thread_pool_size = atoi(optarg); 484 | break; 485 | case 's': 486 | config.silent_mode = 1; 487 | break; 488 | default: 489 | fprintf(stderr, "Usage: %s [-p port] [-t thread_pool_size] [-s (silent mode)]\n", argv[0]); 490 | } 491 | } 492 | 493 | /* Moved to crypto.c */ 494 | // SSL_library_init(); 495 | // OpenSSL_add_all_algorithms(); 496 | // SSL_load_error_strings(); 497 | 498 | #ifdef PRODUCTION 499 | if(ssl_ctx == NULL) { 500 | fprintf(stderr, "[ERROR] SSL context is not initilized\n"); 501 | exit(EXIT_FAILURE); 502 | } 503 | printf("[SERVER] SSL context initialized\n"); 504 | #endif 505 | 506 | int num_cores = sysconf(_SC_NPROCESSORS_ONLN); 507 | printf("[SERVER] Detected %d cores\n", num_cores); 508 | 509 | CRYPTO_ONCE openssl_once = CRYPTO_ONCE_STATIC_INIT; 510 | if (!CRYPTO_THREAD_run_once(&openssl_once, openssl_init_wrapper)) { 511 | fprintf(stderr, "[ERROR] Failed to run OpenSSL initialization\n"); 512 | exit(EXIT_FAILURE); 513 | } 514 | 515 | struct sigaction sa; 516 | sa.sa_handler = server_signal_handler; 517 | sigemptyset(&sa.sa_mask); 518 | sa.sa_flags = 0; 519 | sigaction(SIGINT, &sa, NULL); 520 | sigaction(SIGTERM, &sa, NULL); 521 | 522 | struct connection s = server_init(config.port); 523 | 524 | /* Initialize thread pool, 2 times numbers of cores */ 525 | #ifdef PRODUCTION 526 | pool = thread_pool_init(config.thread_pool_size ? config.thread_pool_size : num_cores*2); 527 | #else 528 | pool = thread_pool_init(2); 529 | #endif 530 | if (pool == NULL) { 531 | fprintf(stderr, "[ERROR] Failed to initialize thread pool\n"); 532 | return 1; 533 | } 534 | 535 | /* Main server loop */ 536 | printf("\n Version: %s\n", "0.0.1"); 537 | printf(" Thread Pool Size: %d\n", pool->max_threads); 538 | printf(" Environment: %s\n", config.environment == PROD ? "Production" : "Development"); 539 | printf(" PID: %d\n", getpid()); 540 | printf(" Listening on %s://%s:%d\n",config.environment == PROD ? "https" : "http", inet_ntoa(s.address.sin_addr), ntohs(s.address.sin_port)); 541 | while (!stop) { 542 | struct connection *client = server_accept(s); 543 | if (client == NULL) { 544 | if (stop) { 545 | break; 546 | } 547 | perror("Error accepting client"); 548 | continue; 549 | } 550 | printf("[SERVER] Accepted connection from %s:%d\n", inet_ntoa(client->address.sin_addr), ntohs(client->address.sin_port)); 551 | 552 | /* Add client handling task to the thread pool */ 553 | thread_pool_add_task(pool, thread_handle_client, client); 554 | } 555 | 556 | /* Clean up */ 557 | thread_pool_destroy(pool); 558 | close(s.sockfd); 559 | printf("[SERVER] Server shutting down gracefully.\n"); 560 | 561 | return 0; 562 | } -------------------------------------------------------------------------------- /src/ws.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #define WS_MAX_FRAME_SIZE 2048 16 | 17 | typedef enum { 18 | WS_OPCODE_CONTINUATION = 0x0, 19 | WS_OPCODE_TEXT = 0x1, 20 | WS_OPCODE_BINARY = 0x2, 21 | WS_OPCODE_CLOSE = 0x8, 22 | WS_OPCODE_PING = 0x9, 23 | WS_OPCODE_PONG = 0xA 24 | } ws_opcode_t; 25 | 26 | typedef enum { 27 | WS_FRAME_INCOMPLETE, 28 | WS_FRAME_COMPLETE, 29 | WS_FRAME_ERROR 30 | } ws_frame_status_t; 31 | 32 | struct websocket_frame { 33 | uint8_t fin; 34 | uint8_t opcode; 35 | uint8_t masked; 36 | uint64_t payload_len; 37 | uint8_t mask[4]; 38 | unsigned char *payload; 39 | }; 40 | 41 | struct ws_container { 42 | char path[128]; 43 | struct event *ev; 44 | struct websocket *ws; 45 | struct ws_info *info; 46 | pthread_mutex_t mutex; 47 | }; 48 | 49 | /* WebSocket container functions prototypes */ 50 | static int ws_send(struct websocket *ws, const char *message, size_t length); 51 | static int ws_close(struct websocket *ws); 52 | 53 | /* Internal function prototypes */ 54 | static void ws_handle_event_callback(struct event *ev, void *arg); 55 | static void ws_handle_frames(struct ws_container container[static 1]); 56 | static int ws_send_frame(int client_fd, const char *message, int length, uint8_t opcode); 57 | 58 | /* Global variables */ 59 | static struct list *ws_container_list = NULL; 60 | static pthread_mutex_t ws_container_list_mutex = PTHREAD_MUTEX_INITIALIZER; 61 | 62 | __attribute__((constructor)) void ws_init() { 63 | ws_container_list = list_create(); 64 | pthread_mutex_init(&ws_container_list_mutex, NULL); 65 | } 66 | 67 | __attribute__((destructor)) void ws_destroy() { 68 | printf("[INFO ] Destroying WebSocket module\n"); 69 | pthread_mutex_lock(&ws_container_list_mutex); 70 | 71 | LIST_FOREACH(ws_container_list, node) { 72 | struct ws_container *container = node->data; 73 | pthread_mutex_lock(&container->mutex); 74 | 75 | //printf("Destroying WebSocket container %d: %s\n", container->ws->client_fd, container->path); 76 | 77 | event_del(container->ev); 78 | close(container->ws->client_fd); 79 | free(container->ws); 80 | pthread_mutex_unlock(&container->mutex); 81 | pthread_mutex_destroy(&container->mutex); 82 | free(container); 83 | 84 | list_remove(ws_container_list, node); 85 | } 86 | 87 | list_destroy(ws_container_list); 88 | 89 | pthread_mutex_unlock(&ws_container_list_mutex); 90 | pthread_mutex_destroy(&ws_container_list_mutex); 91 | 92 | printf("[INFO ] WebSocket module destroyed\n"); 93 | } 94 | 95 | /* helper for websockets base64 */ 96 | static const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 97 | static void base64_encode(const unsigned char *input, size_t input_len, char *output) { 98 | size_t j = 0; 99 | unsigned char buffer[3]; 100 | while (input_len > 0) { 101 | size_t chunk_len = input_len >= 3 ? 3 : input_len; 102 | memset(buffer, 0, 3); 103 | memcpy(buffer, input, chunk_len); 104 | 105 | output[j++] = base64_chars[(buffer[0] & 0xFC) >> 2]; 106 | output[j++] = base64_chars[((buffer[0] & 0x03) << 4) | ((buffer[1] & 0xF0) >> 4)]; 107 | output[j++] = chunk_len > 1 ? base64_chars[((buffer[1] & 0x0F) << 2) | ((buffer[2] & 0xC0) >> 6)] : '='; 108 | output[j++] = chunk_len > 2 ? base64_chars[buffer[2] & 0x3F] : '='; 109 | 110 | input += chunk_len; 111 | input_len -= chunk_len; 112 | } 113 | output[j] = '\0'; 114 | } 115 | 116 | /* helper for websockets sha1, accept key is the result of combining the client key and the websocket guid */ 117 | static void ws_compute_accept_key(const char *client_key, char *accept_key) { 118 | const char *websocket_guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 119 | size_t client_key_len = strlen(client_key); 120 | size_t guid_len = strlen(websocket_guid); 121 | char combined[256]; 122 | 123 | if (client_key_len + guid_len >= sizeof(combined)) { 124 | fprintf(stderr, "[ERROR] Client key and GUID combination too long\n"); 125 | return; 126 | } 127 | 128 | unsigned char sha1_result[SHA_DIGEST_LENGTH]; 129 | snprintf(combined, sizeof(combined), "%s%s", client_key, websocket_guid); 130 | 131 | SHA1((unsigned char *)combined, strlen(combined), sha1_result); 132 | base64_encode(sha1_result, SHA_DIGEST_LENGTH, accept_key); 133 | } 134 | 135 | static struct ws_container *ws_container_create(int client_fd, struct ws_info *info, char *path) { 136 | struct websocket *ws = malloc(sizeof(struct websocket)); 137 | if (!ws) { 138 | perror("Failed to allocate memory for websocket"); 139 | return NULL; 140 | } 141 | 142 | ws->client_fd = client_fd; 143 | ws->session = NULL; 144 | ws->send = ws_send; 145 | ws->close = ws_close; 146 | 147 | struct ws_container *container = malloc(sizeof(struct ws_container)); 148 | if (!container) { 149 | perror("Failed to allocate memory for ws_container"); 150 | free(ws); 151 | return NULL; 152 | } 153 | 154 | container->ws = ws; 155 | container->info = info; 156 | snprintf(container->path, sizeof(container->path), "%s", path); 157 | container->mutex = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER; 158 | container->ev = event_new(client_fd, 0, ws_handle_event_callback, container); 159 | if (!container->ev) { 160 | perror("Failed to create event"); 161 | free(ws); 162 | free(container); 163 | return NULL; 164 | } 165 | 166 | pthread_mutex_lock(&ws_container_list_mutex); 167 | list_add(ws_container_list, container); 168 | pthread_mutex_unlock(&ws_container_list_mutex); 169 | 170 | return container; 171 | } 172 | 173 | static int ws_container_destroy(struct ws_container *container) { 174 | if (!container) return -1; 175 | 176 | /* Remove from list */ 177 | pthread_mutex_lock(&ws_container_list_mutex); 178 | list_remove(ws_container_list, container); 179 | pthread_mutex_unlock(&ws_container_list_mutex); 180 | 181 | /* Free resources */ 182 | pthread_mutex_lock(&container->mutex); 183 | 184 | event_del(container->ev); 185 | close(container->ws->client_fd); 186 | free(container->ws); 187 | 188 | pthread_mutex_unlock(&container->mutex); 189 | pthread_mutex_destroy(&container->mutex); 190 | 191 | free(container); 192 | 193 | return 0; 194 | } 195 | 196 | static struct ws_container *ws_container_find(int client_fd) { 197 | struct ws_container *container = NULL; 198 | pthread_mutex_lock(&ws_container_list_mutex); 199 | 200 | LIST_FOREACH(ws_container_list, node) { 201 | struct ws_container *current = node->data; 202 | if (current->ws->client_fd == client_fd) { 203 | container = current; 204 | break; 205 | } 206 | } 207 | 208 | pthread_mutex_unlock(&ws_container_list_mutex); 209 | return container; 210 | } 211 | 212 | static void ws_handle_event_callback(struct event *ev, void *arg) { 213 | struct ws_container *container = (struct ws_container *)arg; 214 | (void)ev; 215 | 216 | pthread_mutex_lock(&container->mutex); 217 | if (!container || !container->ws || !container->info) { 218 | fprintf(stderr, "Invalid container or resources\n"); 219 | pthread_mutex_unlock(&container->mutex); 220 | return; 221 | } 222 | pthread_mutex_unlock(&container->mutex); 223 | 224 | ws_handle_frames(container); 225 | } 226 | 227 | int ws_update_container(const char* path, struct ws_info *info) { 228 | if (!info || !ws_container_list) return -1; 229 | 230 | pthread_mutex_lock(&ws_container_list_mutex); 231 | LIST_FOREACH(ws_container_list, node) { 232 | struct ws_container *container = node->data; 233 | pthread_mutex_lock(&container->mutex); 234 | if (strcmp(container->path, path) == 0) { 235 | //printf("Updating WebSocket container %d: %s\n", container->ws->client_fd, container->path); 236 | container->info = info; 237 | } 238 | pthread_mutex_unlock(&container->mutex); 239 | 240 | } 241 | pthread_mutex_unlock(&ws_container_list_mutex); 242 | 243 | return 0; 244 | } 245 | 246 | static ws_frame_status_t ws_decode_frame(const unsigned char *data, size_t data_len, struct websocket_frame *frame) { 247 | if (data_len < 2) return WS_FRAME_INCOMPLETE; 248 | 249 | frame->fin = (data[0] & 0x80) != 0; 250 | frame->opcode = data[0] & 0x0F; 251 | frame->masked = (data[1] & 0x80) != 0; 252 | frame->payload_len = data[1] & 0x7F; 253 | 254 | size_t offset = 2; 255 | if (frame->payload_len == 126) { 256 | if (data_len < 4) return WS_FRAME_INCOMPLETE; 257 | frame->payload_len = (data[2] << 8) | data[3]; 258 | offset += 2; 259 | } else if (frame->payload_len == 127) { 260 | if (data_len < 10) return WS_FRAME_INCOMPLETE; 261 | frame->payload_len = 0; 262 | offset += 8; 263 | } 264 | 265 | if (frame->masked) { 266 | if (data_len < offset + 4) return WS_FRAME_INCOMPLETE; 267 | memcpy(frame->mask, &data[offset], 4); 268 | offset += 4; 269 | } 270 | 271 | if (data_len < offset + frame->payload_len) return WS_FRAME_INCOMPLETE; 272 | 273 | frame->payload = malloc(frame->payload_len); 274 | if (!frame->payload) return WS_FRAME_ERROR; 275 | memcpy(frame->payload, &data[offset], frame->payload_len); 276 | 277 | if (frame->masked) { 278 | for (size_t i = 0; i < frame->payload_len; i++) { 279 | frame->payload[i] ^= frame->mask[i % 4]; 280 | } 281 | } 282 | 283 | return WS_FRAME_COMPLETE; 284 | } 285 | 286 | static int ws_send_frame(int client_fd, const char *message, int length, uint8_t opcode) { 287 | unsigned char frame[WS_MAX_FRAME_SIZE]; 288 | size_t message_len = length; 289 | size_t offset = 0; 290 | 291 | frame[offset++] = 0x80 | opcode; 292 | if (message_len <= 125) { 293 | frame[offset++] = message_len; 294 | } else if (message_len <= 65535) { 295 | frame[offset++] = 126; 296 | frame[offset++] = (message_len >> 8) & 0xFF; 297 | frame[offset++] = message_len & 0xFF; 298 | } else { 299 | fprintf(stderr, "[ERROR] Message too large\n"); 300 | return -1; 301 | } 302 | 303 | memcpy(&frame[offset], message, message_len); 304 | offset += message_len; 305 | 306 | send(client_fd, frame, offset, 0); 307 | 308 | return 0; 309 | } 310 | 311 | static void ws_free_frame(struct websocket_frame *frame){ 312 | if (frame->payload) free(frame->payload); 313 | frame->payload = NULL; 314 | } 315 | 316 | static void ws_handle_frames(struct ws_container container[static 1]) { 317 | 318 | struct websocket *ws = container->ws; 319 | unsigned char buffer[WS_MAX_FRAME_SIZE]; 320 | ssize_t received = recv(ws->client_fd, buffer, sizeof(buffer), 0); 321 | 322 | /* Lock incase modules is about to get updated. */ 323 | pthread_mutex_lock(&container->mutex); 324 | struct ws_info *info = container->info; 325 | 326 | if (received <= 0) { 327 | perror("Connection closed or error"); 328 | if (info->on_close) info->on_close(ws); 329 | pthread_mutex_unlock(&container->mutex); 330 | ws_container_destroy(container); 331 | return; 332 | } 333 | 334 | struct websocket_frame frame; 335 | ws_frame_status_t status = ws_decode_frame(buffer, received, &frame); 336 | if (status == WS_FRAME_COMPLETE) { 337 | if (frame.opcode == WS_OPCODE_CLOSE) { 338 | if (info->on_close) info->on_close(ws); 339 | pthread_mutex_unlock(&container->mutex); 340 | ws_free_frame(&frame); 341 | ws_container_destroy(container); 342 | return; 343 | } else if (frame.opcode == WS_OPCODE_TEXT) { 344 | if (info->on_message){ 345 | 346 | /* Copy payload to null-terminated string to avoid race conditon on payload */ 347 | char *payload_copy = malloc(frame.payload_len + 1); 348 | if (payload_copy) { 349 | memcpy(payload_copy, frame.payload, frame.payload_len); 350 | payload_copy[frame.payload_len] = '\0'; 351 | info->on_message(ws, payload_copy, frame.payload_len); 352 | free(payload_copy); 353 | } 354 | } 355 | } else if (frame.opcode == WS_OPCODE_PING) { 356 | ws_send_frame(ws->client_fd, (const char *)frame.payload, frame.payload_len, WS_OPCODE_PONG); 357 | } 358 | ws_free_frame(&frame); 359 | } else if (status == WS_FRAME_ERROR) { 360 | fprintf(stderr, "[ERROR] Error decoding WebSocket frame\n"); 361 | ws_free_frame(&frame); 362 | } else { 363 | fprintf(stderr, "[ERROR] Incomplete WebSocket frame\n"); 364 | ws_free_frame(&frame); 365 | } 366 | pthread_mutex_unlock(&container->mutex); 367 | } 368 | 369 | void ws_force_close(struct ws_info *info) { 370 | if (!info) return; 371 | 372 | pthread_mutex_lock(&ws_container_list_mutex); 373 | LIST_FOREACH(ws_container_list, node) { 374 | struct ws_container *container = node->data; 375 | if (container->info == info) { 376 | event_del(container->ev); 377 | close(container->ws->client_fd); 378 | free(container->ws); 379 | free(container); 380 | } 381 | list_remove(ws_container_list, node); 382 | } 383 | pthread_mutex_unlock(&ws_container_list_mutex); 384 | } 385 | 386 | static int ws_send(struct websocket *ws, const char *message, size_t length) { 387 | return ws_send_frame(ws->client_fd, message, length, WS_OPCODE_TEXT); 388 | } 389 | 390 | static int ws_close(struct websocket *ws) { 391 | return ws_send_frame(ws->client_fd, NULL, 0, WS_OPCODE_CLOSE); 392 | } 393 | 394 | static void* ws_event_thread(void* args) { 395 | (void)args; 396 | event_dispatch(); 397 | return NULL; 398 | } 399 | 400 | int http_is_websocket_upgrade(struct http_request *req) { 401 | const char *connection = map_get(req->headers, "Connection"); 402 | const char *upgrade = map_get(req->headers, "Upgrade"); 403 | 404 | if (connection && upgrade && strstr(connection, "Upgrade") && strcmp(upgrade, "websocket") == 0) { 405 | return 1; 406 | } 407 | 408 | return 0; 409 | } 410 | 411 | int ws_confirm_open(int sd){ 412 | struct ws_container *container = ws_container_find(sd); 413 | if (!container) return -1; 414 | 415 | pthread_mutex_lock(&container->mutex); 416 | if (container->info->on_open) container->info->on_open(container->ws); 417 | pthread_mutex_unlock(&container->mutex); 418 | 419 | return 0; 420 | } 421 | 422 | void ws_handle_client(int sd, struct http_request *req, struct http_response *res, struct ws_info *info) { 423 | printf("[WS] Upgrading connection to WebSocket %d\n", sd); 424 | 425 | const char *client_key = map_get(req->headers, "Sec-WebSocket-Key"); 426 | if (!client_key) { 427 | fprintf(stderr, "[ERROR] Missing Sec-WebSocket-Key header\n"); 428 | res->status = HTTP_400_BAD_REQUEST; 429 | return; 430 | } 431 | 432 | struct ws_container *container = ws_container_create(sd, info, req->path); 433 | if (!container) { 434 | fprintf(stderr, "[ERROR] Failed to create WebSocket container\n"); 435 | res->status = HTTP_500_INTERNAL_SERVER_ERROR; 436 | return; 437 | } 438 | 439 | if(event_add(container->ev) < 0) { 440 | fprintf(stderr, "[ERROR] Failed to add event to event loop\n"); 441 | ws_container_destroy(container); 442 | res->status = HTTP_500_INTERNAL_SERVER_ERROR; 443 | return; 444 | } 445 | 446 | /* Freed by main server */ 447 | char* accept_key = malloc(128); 448 | if(!accept_key) { 449 | perror("[ERROR] Failed to allocate memory for accept key"); 450 | res->status = HTTP_500_INTERNAL_SERVER_ERROR; 451 | return; 452 | } 453 | ws_compute_accept_key(client_key, accept_key); 454 | 455 | req->websocket = 1; 456 | res->status = HTTP_101_SWITCHING_PROTOCOLS; 457 | map_insert(res->headers, "Sec-WebSocket-Accept", accept_key); 458 | map_insert(res->headers, "Upgrade", "websocket"); 459 | map_insert(res->headers, "Connection", "Upgrade"); 460 | res->body[0] = '\0'; 461 | } 462 | 463 | static pthread_t ws_thread; 464 | __attribute__((constructor)) void ws_constructor() { 465 | pthread_create(&ws_thread, NULL, ws_event_thread, NULL); 466 | printf("[WS] WebSocket thread started\n"); 467 | } 468 | 469 | __attribute__((destructor)) void ws_destructor() { 470 | printf("[WS] Shutting down WebSocket thread\n"); 471 | 472 | event_dispatch_stop(); 473 | pthread_join(ws_thread, NULL); 474 | } 475 | -------------------------------------------------------------------------------- /static/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joexbayer/c-web-modules/52ff9fa441b1691510b44e58462c804eec7d74c1/static/.keep -------------------------------------------------------------------------------- /static/hello.txt: -------------------------------------------------------------------------------- 1 | Hello World! -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Hello World 8 | 9 | 10 | 11 |

    Hello, World!

    12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------