diff --git a/Makefile b/Makefile index ae789db..c8b7644 100644 --- a/Makefile +++ b/Makefile @@ -82,4 +82,8 @@ docs: example: $(MAKE) -C $@ -.PHONY: install uninstall docs format clean example +test: + make example + ./scripts/test.sh + +.PHONY: install uninstall docs format clean example test diff --git a/README.md b/README.md index bf0870c..527b63f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ ``` __ - _____/ /__________ ____ ___ - / ___/ __/ ___/ __ \/ __ `__ \ -/ /__/ /_/ / / /_/ / / / / / / -\___/\__/_/ \____/_/ /_/ /_/ 1.5 + _____/ /_____ _________ ___ + / ___/ __/ __ \/ ___/ __ `__ \ +/ /__/ /_/ /_/ / / / / / / / / +\___/\__/\____/_/ /_/ /_/ /_/ 1.6 ``` @@ -13,7 +13,7 @@ ![](https://img.shields.io/github/v/tag/ngn13/ctorm?label=version) ![](https://img.shields.io/github/license/ngn13/ctorm) -ctorm is a multi-threaded HTTP server for `HTTP/1.1` and `HTTP/1.0`. +ctorm is a multi-threaded, simple web server framework for `HTTP/1.1` and `HTTP/1.0`. It has an easy API for general web server applications. > [!WARNING] @@ -26,70 +26,93 @@ if you are interested. ### Features - Wildcard routes - Middleware support -- Form body parsing - URL queries (parameters) +- URL encoded body parsing - JSON support with [cJSON](https://github.com/DaveGamble/cJSON) - Handling 404 (all) routes - Sending files and static file serving ### Installation You will need the following software in order to build and install ctorm: +- GNU tar to extract the release archive (`tar`) - GCC and other general build tools (`build-essential`) +- If you want to build the man pages, [`doxygen`](https://www.doxygen.org/) - If you want JSON support, cJSON and it's headers (`cjson`, `libcjson-dev`) -- tar (to extract the release archive) -First [download the latest release](https://github.com/ngn13/ctorm/tags) archive, -**do not compile from the latest commit unless you are doing development**: +First [download the latest release archive](https://github.com/ngn13/ctorm/tags), +**do not compile from the latest commit or a branch unless you are doing development**: ```bash wget https://github.com/ngn13/ctorm/archive/refs/tags/1.5.tar.gz tar xf 1.5.tar.gz && cd ctorm-1.5 ``` - -Then use the `make` command to build and install: +Then use the `make` command to compile the library: +```bash +make +``` +**If you don't have cJSON installed**, you need to run this command with `CTORM_JSON_SUPPORT=0` +option to disable JSON support: +```bash +make CTORM_JSON_SUPPORT=0 +``` +**If you installed `doxygen`, and you want to build the man pages** run `make` with +the `docs` command: +```bash +make docs +``` +To install the library (and if you've built it, the documentation) run `make` with +the `install` command **as root**: ```bash -make && sudo make install +make install ``` ### Getting started #### Hello world application ```c -#include +#include -void hello_world(req_t *req, res_t *res) { +void GET_index(ctorm_req_t *req, ctorm_res_t *res) { // send the "Hello world!" message RES_SEND("Hello world!"); } int main() { // create the app with default configuration - app_t *app = app_new(NULL); + ctorm_app_t *app = ctorm_app_new(NULL); // setup the routes - GET(app, "/", hello_world); + GET(app, "/", GET_index); // run the app - if (!app_run(app, "0.0.0.0:8080")) - error("app failed: %s", app_geterror()); + if (!ctorm_app_run(app, "0.0.0.0:8080")) + ctorm_fail("failed to start the application: %s", ctorm_geterror()); // clean up - app_free(app); + ctorm_app_free(app); return 0; } ``` #### Other functions +Here are some nicely formatted markdown documents that explain all the functions you will +most likely gonna use: - [App](docs/app.md) +- [Error](docs/error.md) - [Logging](docs/log.md) - [Request](docs/req.md) - [Response](docs/res.md) +You can also checkout the man pages if you built and installed during the [installation](#installation). + #### Example applications -Repository also contains few example applications in the `example` folder, you can -build these by running `make example`. +Repository also contains few example applications in the `example` directory. You can +build these by running: +```bash +make example +``` #### Deploying your application -You can use the docker image (built with actions) to easily deploy your application, here is -an example: +You can use the docker image (built by github actions) to easily deploy your +application, here is an example: ```Dockerfile FROM ghcr.io/ngn13/ctorm:latest @@ -108,12 +131,13 @@ CMD ["/app/server"] ``` ### Development -For development, you can compile the library with debug mode: +For development, you can compile the library with the `CTORM_DEBUG=1` option to enable debug +messages: ```bash make CTORM_DEBUG=1 ``` -then you can use the example applications for testing: +Then you can use the example applications and the test scripts in the `scripts` directory +for testing: ```bash -make example -LD_LIBRARY_PATH=./dist ./dist/example_hello +make test ``` diff --git a/docs/app.md b/docs/app.md index 2eb7ed7..cff784b 100644 --- a/docs/app.md +++ b/docs/app.md @@ -3,14 +3,14 @@ Before creating an application, you can create a custom configuration: ```c -app_config_t config; +ctorm_config_t config; ``` But before using it you should initialize it: ```c -app_config_new(&config); +ctorm_config_new(&config); ``` This will set all the default values, you can modify these by -directly accessing them through the `app_config_t` structure, +directly accessing them through the `ctorm_config_t` structure, for example: ```c // disable request/response logging @@ -20,36 +20,36 @@ config.disable_logging = false; ### Managing the application To create an application: ```c -app_t *app = app_new(&config); +ctorm_app_t *app = ctorm_app_new(&config); ``` If you don't have a custom configuration and you want to use the default configuration: ```c -app_t *app = app_new(NULL); +ctorm_app_t *app = ctorm_app_new(NULL); ``` And to start the application: ```c -app_run(app, "0.0.0.0:8080") +ctorm_app_run(app, "0.0.0.0:8080") ``` This will start the application on port 8080, all interfaces, -after the app stops, you should clean up the `app_t` pointer +after the app stops, you should clean up the `ctorm_app_t` pointer to free all the resources, to do this: ```c -app_free(app); +ctorm_app_free(app); ``` ### Simple routing Handlers used for routing should follow this structure: ```c -void route(req_t*, res_t*); +void route(ctorm_req_t*, ctorm_res_t*); ``` -The `req_t` pointer points to the request object, and the `res_t` +The `ctorm_req_t` pointer points to the request object, and the `ctorm_res_t` pointer points to the response object. To learn what you can do with them, -check out the [request](req.md) and [response](res.md) documentation. +check out the [request](req.md) and [response documentation](res.md). To setup routes, you can use these simple macros: ```c - // get_index will handle any GET request for / +// get_index will handle any GET request for / GET(app, "/", get_index); // set_route will handle any PUT request for any routes @@ -67,6 +67,18 @@ DELETE(app, "/", delete_index); OPTIONS(app, "/", options_index); ``` +### URL parameters +You can use the `:` character to specify URL parameters in the routes: +```c +GET(app, "/blog/:lang/desc", get_blog_desc); +``` +For example this route will act the same as `/blog/*/desc` route. However +whatever fills the asterisk (wildcard) will be used as the value of the +`lang` URL parameter. + +Later during the handler call, URL parameters can be accessed using the request +pointer. See the [request documentation](req.md) for more information. + ### Middleware Middleware handlers have the exact same structure with the routes, however they have different macros: @@ -81,37 +93,17 @@ MIDDLEWARE_OPTIONS(app, "/", options_index_mid); ``` ### Set static directory -To setup a static route you can use the `app_static` function, +To setup a static route you can use the `ctorm_app_static` function, **please note that ctorm only supports a single static route.** ```c // static files be served at '/static' path, // from the './files' directory -app_static(app, "/static", "./files"); +ctorm_app_static(app, "/static", "./files"); ``` ### Setup 404 (all) route By default, routes that does not match with any other will be redirected to a 404 page, you set a custom route for this: ```c -app_all(app, all_route); -``` - -### Error handling -When a function fails, depending on the return value, you may receive a `false` -or a `NULL` return. To learn what went wrong and why the function failed, you can -use the `app_geterror` function, here is an example: -```c -if(!app_run(app, "127.0.0.1:8080")) - error("something went wrong: %s", app_geterror()); -``` -The error code also will be set the with the `errno` variable: -```c -#include -... -if(!app_run(argv[1])){ - if(errno == BadAddress) - error("you specified an invalid address"); - else - error("failed to start the app: %s", app_geterror()); -} +ctorm_app_all(app, all_route); ``` diff --git a/docs/error.md b/docs/error.md new file mode 100644 index 0000000..693a7e8 --- /dev/null +++ b/docs/error.md @@ -0,0 +1,30 @@ +# Error functions +### Checking the error code +When a function fails, depending on the return value, you may receive a `false` +or a `NULL` return. To learn what went wrong and why the function failed, you can +use check the `errno`: +```c +#include +... +if(!ctorm_app_run(app, argv[1])){ + if(errno == BadAddress) + ctorm_fail("you specified an invalid address"); + else + ctorm_fail("something else went wrong: %d", errno); + return EXIT_FAILURE; +} +``` +See [errors.h](../inc/errors.h) for the full list of error codes. + +### Getting the error description +To get the string description of an error, you can use `ctorm_geterror`: +```c +if(!ctorm_app_run(app, "0.0.0.0:8080")){ + ctorm_fail("something went wrong: %s", ctorm_geterror()); + return EXIT_FAILURE; +} +``` +Or you can get the description of a specific error code: +```c +ctorm_info("BadAddress: %s", ctorm_geterror_from_code(BadAddress)); +``` diff --git a/docs/log.md b/docs/log.md index 8918971..c7a70ff 100644 --- a/docs/log.md +++ b/docs/log.md @@ -1,14 +1,14 @@ -# Log Functions -ctorm provides a simple, colored logging system for your +# Log Functions +ctorm provides a simple, colored logging system for your general logging usage. -### General logging functions +### General logging functions ```c -info("some information"); -warn("there may be something wrong"); -error("PANIC!!"); +ctorm_info("some information"); +ctorm_warn("you better read this"); +ctorm_fail("PANIC!!"); ``` ### Enable/Disable request logging -By default ctorm will log information about every single request -on the console, you can disable/enabled this with the [app configuration](app.md). +By default ctorm will log information about every single request, +you can disable/enabled this with the [app configuration](app.md). diff --git a/docs/req.md b/docs/req.md index afa698f..3913d21 100644 --- a/docs/req.md +++ b/docs/req.md @@ -5,16 +5,16 @@ > [!WARNING] > Most of these functions are macros, and will only work if the -> `req_t` pointer is named `req`, otherwise you should directly +> `ctorm_req_t` pointer is named `req`, otherwise you should directly > use the original functions. ### Request path -There are two different sections of the `req_t` which you can +There are two different sections of the `ctorm_req_t` which you can use to access the request path, however you should not directly modify these sections: ```c -info("URL encoded full path with the queries: %s", req->encpath); -info("URL decoded full path without the queries: %s", req->path); +ctorm_info("URL encoded full path with the queries: %s", req->encpath); +ctorm_info("URL decoded full path without the queries: %s", req->path); ``` ### Request method @@ -48,11 +48,11 @@ you provide: ```c // get the body size uint64_t size = REQ_BODY_SIZE(); -// uint64_t size = req_body_size(req); +// uint64_t size = ctorm_req_body_size(req); // check if body size is valid if(size == 0){ - error("request does not contain a body"); + ctorm_fail("request does not contain a body"); return; } @@ -61,24 +61,24 @@ char *body = malloc(size); // read "size" bytes of body into the "buffer" REQ_BODY(body, size); -// req_body(req, body, size); +// ctorm_req_body(req, body, size); ``` ctrom also contains few helper functions to work with certain body formats: ```c // parse the form encoded body -enc_url_t *form = REQ_FORM(); -// enc_url_t *form = req_form(req); -char *username = enc_url_get(form, "username"); // do not free or directly modify -enc_url_free(form); // "username" now points to an invalid address +ctorm_url_t *form = REQ_FORM(); +// enc_url_t *form = ctorm_req_form(req); +char *username = ctorm_url_get(form, "username"); // do not free or directly modify +ctorm_url_free(form); // "username" now points to an invalid address // parse the JSON encoded body cJSON *json = REQ_JSON(); -// cJSON *json = req_json(req); +// cJSON *json = ctorm_req_json(req); cJSON *un_item = cJSON_GetObjectItem(json, "username"); char *un = cJSON_GetStringValue(un_item); // do not free or directly modify -enc_json_free(json); // "un" and "un_item" now points to an invalid address +ctorm_json_free(json); // "un" and "un_item" now points to an invalid address ``` ### Request queries @@ -86,30 +86,45 @@ To get URL decoded request queries, you can use the `REQ_QUERY` macro or the `req_query` function: ```c char *username = REQ_QUERY("username"); // do not free or directly modify -// char *username = req_query(req, "username"); +// char *username = ctorm_req_query(req, "username"); if(NULL == username){ - error("username query is not specified"); + ctorm_fail("username query is not specified"); return; } -info("username: %s", username); +ctorm_info("username: %s", username); +``` + +### Request parameters +If the route uses a URL parameter (see [app documentation](app.md) for more information) +then you can access this parameter by it's name: +```c +char *lang = REQ_PARAM("lang"); +// char *lang = ctorm_req_param(req, "lang"); + +if(NULL == lang){ + ctorm_warn("no language specified, using the default"); + lang = "en"; +} + +ctorm_info("language: %s", lang); ``` ### Request headers To get HTTP headers, you can use the `REQ_HEADER` macro or the -`req_header` function, please note that HTTP headers are case-insensitive. +`ctorm_req_header` function, please note that HTTP headers are case-insensitive. Also, if the client sent multiple headers with the same name, this macro/function will return the first one in the header list. ```c -char* agent = REQ_GET("User-Agent"); -// char *agent = req_get(req, "User-Agent"); +char* agent = REQ_GET("user-agent"); +// char *agent = ctorm_req_get(req, "user-agent"); if(NULL == agent){ - error("user-agent header is not set"); + ctorm_fail("user-agent header is not set"); return; } -info("user-agent is %s", agent); +ctorm_info("user-agent is %s", agent); ``` diff --git a/docs/res.md b/docs/res.md index ae054ee..2effd9f 100644 --- a/docs/res.md +++ b/docs/res.md @@ -1,11 +1,11 @@ -# Response functions -> [!IMPORTANT] +# Response functions +> [!IMPORTANT] > You should **NOT** `free()` any data returned by this functions > **UNLESS** it's explicitly told to do so. > [!WARNING] > Most of these functions are macros, and will only work if the -> `res_t` pointer is named `res`, otherwise you should directly +> `ctorm_res_t` pointer is named `res`, otherwise you should directly > use the original functions. ### Setting the response code @@ -19,7 +19,7 @@ Or you can directly modify the response code: res->code = 403; ``` -### Working with the response body +### Working with the response body There are few different ways to work with the response body: ```c // you can use local data, it will be copied to heap @@ -31,12 +31,12 @@ RES_SEND("hello world!"); // at the end char raw[128]; ... -res_send(res, data, sizeof(raw)); +ctorm_res_send(res, data, sizeof(raw)); // if the data is null terminated, you can set the size // to 0, and the size will be calculated with strlen() // internally -res_send(res, "hello world!", 0); +ctorm_res_send(res, "hello world!", 0); ``` If you have formatted text, you can use the formatted @@ -49,7 +49,7 @@ res_fmt(res, "username: %s", username); // this macro/function will append to the response body RES_ADD("age: %d", age); -res_add(res, "age: %d", age); +ctorm_res_add(res, "age: %d", age); ``` You can also send JSON data with response body using `cJSON` @@ -62,7 +62,7 @@ cJSON_AddStringToObject(json, "username", "John"); RES_JSON(json); // same thing without using the macro -res_json(res, json); +ctorm_res_json(res, json); ``` If for whatever reason, you want to completely clear the @@ -71,18 +71,20 @@ response body: RES_CLEAR(); // without using the macro -res_clear(res); +ctorm_res_clear(res); ``` ### Sending a file Using relative or absolute paths, you can send files with -the response using the `RES_SENDFILE` macro or the `res_sendfile` -function, `Content-Type` will be set for `html`, `css`, `js` and `json` -files based on the extension, for any other file type the `Content-Type` +the response using the `RES_SENDFILE` macro or the `ctorm_res_sendfile` +function. + +`Content-Type` header will be set for `html`, `css`, `js` and `json` +files based on the extension, for any other file type the `Content-Type` will be set to `text/plain` and you may need to manually set it. ```c RES_SENDFILE("files/index.html"); -res_sendfile(res, "files/index.html"); +ctorm_res_sendfile(res, "files/index.html"); ``` ### Working with headers @@ -91,14 +93,14 @@ function: ```c // just like the other body functions, you can use local data RES_SET("Cool", "yes"); -res_set(res, "Cool", "yes"); +ctorm_res_set(res, "Cool", "yes"); ``` To remove a header, you can use `RES_DEL` macro or the `res_del` function: ```c RES_DEL("Cool"); -res_del(res, "Cool"); +ctorm_res_del(res, "Cool"); ``` ### Redirecting @@ -106,5 +108,5 @@ To redirect the client to another page or a URL, you can use the `RES_REDIRECT` macro or the `res_redirect` function: ```c RES_REDIRECT("/login"); -res_redirect(res, "/login"); +ctorm_res_redirect(res, "/login"); ```