diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index a00c3fb..b9430ac 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -15,5 +15,5 @@ jobs: - name: Run clang-format style check for C/C++/Protobuf programs. uses: jidicula/clang-format-action@f62da5e3d3a2d88ff364771d9d938773a618ab5e with: - check-path: 'src' + check-path: 'src/nginx_module' clang-format-version: '16' diff --git a/.gitignore b/.gitignore index 434a471..3d16ad9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ nginx-* vcpkg .vscode **/.DS_Store +src/validator/firetail-validator.so +src/validator/firetail-validator.h +*.swo +*.swp diff --git a/README.md b/README.md index 5c21cf2..ffe9c62 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,62 @@ The Firetail NGINX Module will be receiving 401 responses as you have not yet co +### Request Validation + +To demonstrate request validation a `POST /proxy/profile/{username}/comment` operation is defined in the provided [appspec.yml](./dev/appspec.yml), with one profile defined in the provided [nginx.conf](./dev/nginx.conf): `POST /proxy/profile/alice/comment`. A `proxy_pass` directive is used to forward requests to `/profile/alice/comment` as the request body validation will not occur on locations where the `return` directive is used. + +Making a curl request to `POST /profile/alice/comment` should yield the following result, which validates successfully against the provided appspec: + +```bash +curl localhost:8080/proxy/profile/alice/comment -X POST -H "Content-Type: application/json" -d '{"comment":"Hello world!"} +``` + +```json +{"message":"Success!"} +``` + +If you alter the request body in any way that deviates from the appspec you will receive an error response from the Firetail nginx module instead: + +```bash +curl localhost:8080/proxy/profile/alice/comment -X POST -H "Content-Type: application/json" -d '{"comment":12345}' +``` + +```json +{"code":400,"title":"something's wrong with your request body","detail":"the request's body did not match your appspec: request body has an error: doesn't match the schema: Error at \"/comment\": field must be set to string or not be present\nSchema:\n {\n \"type\": \"string\"\n }\n\nValue:\n \"number, integer\"\n"} +``` + + + +### Response Validation + +To demonstrate response validation a `GET /profile/{username}` operation is defined in the provided [appspec.yml](./dev/appspec.yml), and two profiles are defined in the provided [nginx.conf](./dev/nginx.conf): `GET /profile/alice` and `GET /profile/bob`. + +Making a curl request to `GET /profile/alice` should yield the following result, which validates successfully against the provided appspec, as it returns only Alice's username and friend count: + +```bash +curl localhost:8080/profile/alice +``` + +```json +{"username":"alice", "friends": 123456789} +``` + +Making a curl request to `GET /profile/bob` will yield a different result, as the response body defined in our nginx.conf erroneously includes Bob's address. This does not validate against our appspec, so the response body is overwritten by the Firetail middleware, which can protect Bob from having his personally identifiable information disclosed by our faulty application. For the purposes of this demo, the Firetail library's debugging responses are enabled so we get a verbose explanation of the problem: + +```bash +curl localhost:8080/profile/bob +``` + +```json +{ + "code": 500, + "title": "internal server error", + "detail": "the response's body did not match your appspec: response body doesn't match the schema: property \"address\" is unsupported\nSchema:\n {\n \"additionalProperties\": false,\n \"properties\": {\n \"friends\": {\n \"minimum\": 0,\n \"type\": \"integer\"\n },\n \"username\": {\n \"type\": \"string\"\n }\n },\n \"type\": \"object\"\n }\n\nValue:\n {\n \"address\": \"Oh dear, this shouldn't be public!\",\n \"friends\": 123456789,\n \"username\": \"bob\"\n }\n" +} +``` + + + ### VSCode For local development with VSCode you'll probably want to download the nginx tarball matching the version you're developing for, and configure it: @@ -76,7 +132,7 @@ You can then use the `configure` command to generate a `makefile` to build the d ```bash cd nginx-1.24.0 -./configure --with-compat --add-dynamic-module=../src +./configure --with-compat --add-dynamic-module=../src/nginx_module make modules ``` @@ -86,6 +142,10 @@ You will then need to install the Firetail NGINX Module's dependencies, [curl](h make modules ``` +The Firetail NGINX module is also dependent upon a validator module, written in Go. + +// TODO: docs for building the Golang validator + ## Configuration diff --git a/dev/Dockerfile b/dev/Dockerfile index 966d480..75f9ccf 100644 --- a/dev/Dockerfile +++ b/dev/Dockerfile @@ -1,4 +1,18 @@ -FROM debian:bullseye-slim AS build +FROM golang:1.21.0-bullseye AS build-golang + +# Make a /src and /dist directory, with our workdir set to /src +WORKDIR /src +RUN mkdir /dist + +# Copy in our go source for the validator +COPY src/validator /src + +# Build the go source as a c-shared lib, outputting to /dist as /dist/validator.so +# NOTE: this will also create a /dist/validator.h which is not needed, so it's discarded it afterwards +RUN CGO_ENABLED=1 go build -buildmode c-shared -o /dist/firetail-validator.so . +RUN rm /dist/firetail-validator.h + +FROM debian:bullseye-slim AS build-c ENV NGINX_VERSION 1.24.0 @@ -12,7 +26,7 @@ RUN curl http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz -o /tmp/nginx-$ RUN apt install -y build-essential libpcre++-dev zlib1g-dev libcurl4-openssl-dev libjson-c-dev # Build our dynamic module -COPY src /tmp/ngx-firetail-module +COPY src/nginx_module /tmp/ngx-firetail-module RUN cd /tmp/nginx-${NGINX_VERSION} && \ ./configure --with-compat --add-dynamic-module=/tmp/ngx-firetail-module && \ make modules @@ -20,10 +34,12 @@ RUN cd /tmp/nginx-${NGINX_VERSION} && \ # Copy our dynamic module & its dependencies into the image for the nginx version we want to use FROM nginx:1.24.0 AS firetail-nginx RUN apt-get update && apt-get install -y libjson-c-dev -COPY --from=build /tmp/nginx-${NGINX_VERSION}/objs/* /etc/nginx/modules/ +COPY --from=build-golang /dist/* /etc/nginx/modules/ +COPY --from=build-c /tmp/nginx-${NGINX_VERSION}/objs/* /etc/nginx/modules/ # An image for local dev with a custom nginx.conf and index.html FROM firetail-nginx as firetail-nginx-dev +COPY dev/appspec.yml /etc/nginx/appspec.yml COPY dev/nginx.conf /etc/nginx/nginx.conf COPY dev/index.html /usr/share/nginx/html/ CMD ["nginx-debug", "-g", "daemon off;"] diff --git a/dev/appspec.yml b/dev/appspec.yml new file mode 100644 index 0000000..bd09d30 --- /dev/null +++ b/dev/appspec.yml @@ -0,0 +1,181 @@ +openapi: 3.0.1 +info: + title: Firetail Nginx Module Example + version: "0.1" +paths: + /: + get: + summary: Returns an index.html + responses: + "200": + description: An index.html file + content: + text/plain: {} + /health: + get: + summary: Returns the status of the server + responses: + "200": + $ref: "#/components/responses/Healthy" + /notfound: + get: + summary: Returns a mock 404 response + responses: + "404": + $ref: "#/components/responses/NotFound" + /unhealthy: + get: + summary: Returns a mock 400 response + responses: + "500": + $ref: "#/components/responses/Unhealthy" + /proxy/health: + get: + summary: Returns the status of the server + responses: + "200": + $ref: "#/components/responses/Healthy" + /proxy/notfound: + get: + summary: Returns a mock 404 response + responses: + "404": + $ref: "#/components/responses/NotFound" + /proxy/unhealthy: + get: + summary: Returns a mock 400 response + responses: + "500": + $ref: "#/components/responses/Unhealthy" + /profile/{username}: + get: + summary: Returns a user's profile + parameters: + - in: path + name: username + required: true + schema: + description: The username of the user whose profile should be returned + type: string + minLength: 3 + maxLength: 20 + responses: + "200": + description: A user's profile + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + username: + type: string + friends: + type: integer + minimum: 0 + /profile/{username}/comment: + post: + summary: Post a comment on a user's profile + requestBody: + description: The comment to be added to the user's profile + required: true + content: + application/json: + schema: + type: object + required: ["comment"] + additionalProperties: false + properties: + comment: + type: string + parameters: + - in: path + name: username + required: true + schema: + description: The username of the user whose profile should be returned + type: string + minLength: 3 + maxLength: 20 + responses: + "201": + description: Comment created + content: + application/json: + schema: + type: object + additionalProperties: False + properties: + message: + type: string + /proxy/profile/{username}/comment: + post: + required: true + summary: Post a comment on a user's profile + requestBody: + description: The comment to be added to the user's profile + required: true + content: + application/json: + schema: + type: object + required: ["comment"] + additionalProperties: false + properties: + comment: + type: string + parameters: + - in: path + name: username + required: true + schema: + description: The username of the user whose profile should be returned + type: string + minLength: 3 + maxLength: 20 + responses: + "201": + description: Comment created + content: + application/json: + schema: + type: object + additionalProperties: False + properties: + message: + type: string +components: + responses: + Healthy: + description: A mocked response from a /health endpoint on a healthy service + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + message: + type: string + enum: ["I'm healthy! 💖"] + Unhealthy: + description: A mocked response from a /health endpoint on an unhealthy service + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + message: + type: string + enum: ["I'm unhealthy! 🤒"] + NotFound: + description: A mocked 404 response + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + message: + type: string + enum: ["Not Found 🤷"] diff --git a/dev/nginx.conf b/dev/nginx.conf index c95e641..1be63e1 100644 --- a/dev/nginx.conf +++ b/dev/nginx.conf @@ -1,5 +1,6 @@ # Step 1: Load the Firetail NGINX Module load_module modules/ngx_firetail_module.so; +# error_log stderr debug; events {} @@ -9,6 +10,7 @@ http { # Step 2: Provide your Firetail API token to the Firetail NGINX Module # You should use lua-nginx-module to pull the API token in from an environment variable here firetail_api_token "YOUR-API-TOKEN"; + firetail_url "https://api.logging.eu-west-1.prod.firetail.app/logs/bulk"; server { listen 80; @@ -24,23 +26,43 @@ http { } location /notfound { - return 404 '{"message":"Not Found"}'; + return 404 '{"message":"Not Found 🤷"}'; } location /unhealthy { return 500 '{"message":"I\'m unhealthy! 🤒"}'; } - location /health-proxy { + location /profile/alice { + return 200 '{"username":"alice", "friends": 123456789}'; + } + + location /profile/bob { + return 200 '{"username":"bob", "friends": 123456789, "address":"Oh dear, this shouldn\'t be public!"}'; + } + + location /profile/alice/comment { + return 201 '{"message":"Success!"}'; + } + + location /proxy/health { proxy_pass http://localhost:80/health; } - location /notfound-proxy { + location /proxy/notfound { proxy_pass http://localhost:80/notexists; } - location /unhealthy-proxy { + location /proxy/unhealthy { proxy_pass http://localhost:80/unhealth; } + + location /proxy/profile/alice/comment { + proxy_pass http://localhost:80/profile/alice/comment; + } + + location /proxy/profile/bob { + proxy_pass http://localhost:80/profile/bob; + } } -} \ No newline at end of file +} diff --git a/src/filter_request_body.c b/src/filter_request_body.c deleted file mode 100644 index 6000785..0000000 --- a/src/filter_request_body.c +++ /dev/null @@ -1,56 +0,0 @@ -#include -#include "filter_context.h" -#include "filter_request_body.h" -#include "firetail_module.h" - -ngx_int_t FiretailRequestBodyFilter(ngx_http_request_t *request, - ngx_chain_t *chain_head) { - // Get our context so we can store the request body data - FiretailFilterContext *ctx = GetFiretailFilterContext(request); - if (ctx == NULL) { - return NGX_ERROR; - } - - // Determine the length of the request body chain we've been given - long new_request_body_parts_size = 0; - for (ngx_chain_t *current_chain_link = chain_head; current_chain_link != NULL; - current_chain_link = current_chain_link->next) { - new_request_body_parts_size += - current_chain_link->buf->last - current_chain_link->buf->pos; - } - - // If we read in more bytes from this chain then we need to create a new char* - // and place it in ctx containing the full body - if (new_request_body_parts_size > 0) { - // Take note of the request body size before and after adding the new chain - // & update it in our ctx - long old_request_body_size = ctx->request_body_size; - long new_request_body_size = - old_request_body_size + new_request_body_parts_size; - ctx->request_body_size = new_request_body_size; - - // Create a new updated body - u_char *updated_request_body = - ngx_pcalloc(request->pool, new_request_body_size); - - // Copy the body read so far into ctx into our new updated_request_body - u_char *updated_request_body_i = ngx_copy( - updated_request_body, ctx->request_body, old_request_body_size); - - // Iterate over the chain again and copy all of the buffers over to our new - // request body char* - for (ngx_chain_t *current_chain_link = chain_head; - current_chain_link != NULL; - current_chain_link = current_chain_link->next) { - long buffer_length = - current_chain_link->buf->last - current_chain_link->buf->pos; - updated_request_body_i = ngx_copy( - updated_request_body_i, current_chain_link->buf->pos, buffer_length); - } - - // Update the ctx with the new updated body - ctx->request_body = updated_request_body; - } - - return kNextRequestBodyFilter(request, chain_head); -} diff --git a/src/filter_request_body.h b/src/filter_request_body.h deleted file mode 100644 index 1f98fa0..0000000 --- a/src/filter_request_body.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef FIRETAIL_FILTER_REQUEST_BODY_INCLUDED -#define FIRETAIL_FILTER_REQUEST_BODY_INCLUDED - -#include -#include - -ngx_int_t FiretailRequestBodyFilter(ngx_http_request_t *request, - ngx_chain_t *chain_head); - -#endif \ No newline at end of file diff --git a/src/filter_response_body.c b/src/filter_response_body.c deleted file mode 100644 index 90c49d9..0000000 --- a/src/filter_response_body.c +++ /dev/null @@ -1,267 +0,0 @@ -#include -#include -#include -#include "filter_context.h" -#include "filter_response_body.h" -#include "firetail_module.h" - -size_t LibcurlNoopWriteFunction(void *buffer, size_t size, size_t nmemb, - void *userp) { - return size * nmemb; -} - -#define MAX_WAIT_MSECS 30 * 100 /* max wait 30 secs */ - -ngx_int_t FiretailResponseBodyFilter(ngx_http_request_t *request, - ngx_chain_t *chain_head) { - // Set the logging level to debug - // TODO: remove - request->connection->log->log_level = NGX_LOG_DEBUG; - - // Get our context so we can store the response body data - FiretailFilterContext *ctx = GetFiretailFilterContext(request); - if (ctx == NULL) { - return NGX_ERROR; - } - - // Determine the length of the response body chain we've been given, and if - // the chain contains the last link - int chain_contains_last_link = 0; - long new_response_body_parts_size = 0; - - for (ngx_chain_t *current_chain_link = chain_head; current_chain_link != NULL; - current_chain_link = current_chain_link->next) { - new_response_body_parts_size += - current_chain_link->buf->last - current_chain_link->buf->pos; - if (current_chain_link->buf->last_buf) { - chain_contains_last_link = 1; - } - } - - // If we read in more bytes from this chain then we need to create a new char* - // and place it in ctx containing the full body - if (new_response_body_parts_size > 0) { - // Take note of the response body size before and after adding the new chain - // & update it in our ctx - long old_response_body_size = ctx->response_body_size; - long new_response_body_size = - old_response_body_size + new_response_body_parts_size; - ctx->response_body_size = new_response_body_size; - - // Create a new updated body - u_char *updated_response_body = - ngx_pcalloc(request->pool, new_response_body_size); - - // Copy the body read so far into ctx into our new updated_response_body - u_char *updated_response_body_i = ngx_copy( - updated_response_body, ctx->response_body, old_response_body_size); - - // Iterate over the chain again and copy all of the buffers over to our new - // response body char* - for (ngx_chain_t *current_chain_link = chain_head; - current_chain_link != NULL; - current_chain_link = current_chain_link->next) { - long buffer_length = - current_chain_link->buf->last - current_chain_link->buf->pos; - updated_response_body_i = ngx_copy( - updated_response_body_i, current_chain_link->buf->pos, buffer_length); - } - - // Update the ctx with the new updated body - ctx->response_body = updated_response_body; - } - - // If it doesn't contain the last buffer of the response body, pass everything - // onto the next filter - we do not care. - if (!chain_contains_last_link) { - return kNextResponseBodyFilter(request, chain_head); - } - - // Piece together a JSON object - // TODO: optimise the JSON generation process - // { - // "version": "1.0.0-alpha", - // "dateCreated": 123456789, - // "executionTime": 123456789, - // "request": { - // "ip": "8.8.8.8", - // "httpProtocol": "HTTP/2", - // "uri": "http://foo.bar/baz", - // "resource": "", - // "method": "POST", - // "headers": {}, - // "body": "" - // }, - // "response": { - // "statusCode": 201, - // "body": "", - // "headers": {} - // } - // } - json_object *log_root = json_object_new_object(); - - json_object *version = json_object_new_string("1.0.0-alpha"); - json_object_object_add(log_root, "version", version); - - struct timespec current_time; - clock_gettime(CLOCK_REALTIME, ¤t_time); - json_object *date_created = json_object_new_int64( - current_time.tv_sec * 1000 + current_time.tv_nsec / 1000000); - json_object_object_add(log_root, "dateCreated", date_created); - - // TODO: replace with actual executionTime - json_object *execution_time = json_object_new_int64(123456789); - json_object_object_add(log_root, "executionTime", execution_time); - - json_object *request_object = json_object_new_object(); - json_object_object_add(log_root, "request", request_object); - - json_object *request_ip = - json_object_new_string_len((char *)request->connection->addr_text.data, - request->connection->addr_text.len); - json_object_object_add(request_object, "ip", request_ip); - - json_object *request_protocol = json_object_new_string_len( - (char *)request->http_protocol.data, request->http_protocol.len); - json_object_object_add(request_object, "httpProtocol", request_protocol); - - // TODO: determine http/https - char *full_uri = ngx_palloc(request->pool, strlen((char *)ctx->server) + - request->unparsed_uri.len + - strlen("http://") + 1); - ngx_memcpy(full_uri, "http://", strlen("http://")); - ngx_memcpy(full_uri + strlen("http://"), ctx->server, - strlen((char *)ctx->server)); - ngx_memcpy(full_uri + strlen("http://") + strlen((char *)ctx->server), - request->unparsed_uri.data, request->unparsed_uri.len); - *(full_uri + strlen("http://") + strlen((char *)ctx->server) + - request->unparsed_uri.len) = '\0'; - json_object *request_uri = json_object_new_string(full_uri); - json_object_object_add(request_object, "uri", request_uri); - - json_object *request_resource = json_object_new_string_len( - (char *)request->unparsed_uri.data, request->unparsed_uri.len); - json_object_object_add(request_object, "resource", request_resource); - - json_object *request_method = json_object_new_string_len( - (char *)request->method_name.data, request->method_name.len); - json_object_object_add(request_object, "method", request_method); - - json_object *request_body = json_object_new_string_len( - (char *)ctx->request_body, (int)ctx->request_body_size); - json_object_object_add(request_object, "body", request_body); - - json_object *request_headers = json_object_new_object(); - for (HTTPHeader *request_header = ctx->request_headers; - (ngx_uint_t)request_header < - (ngx_uint_t)ctx->request_headers + - ctx->request_header_count * sizeof(HTTPHeader); - request_header++) { - json_object *header_value_array = json_object_new_array_ext(1); - json_object_object_add(request_headers, (char *)request_header->key.data, - header_value_array); - json_object *header_value = json_object_new_string_len( - (char *)request_header->value.data, (int)request_header->value.len); - json_object_array_add(header_value_array, header_value); - } - json_object_object_add(request_object, "headers", request_headers); - - json_object *response_object = json_object_new_object(); - json_object_object_add(log_root, "response", response_object); - - json_object *response_status_code = json_object_new_int(ctx->status_code); - json_object_object_add(response_object, "statusCode", response_status_code); - - json_object *response_body = json_object_new_string_len( - (char *)ctx->response_body, (int)ctx->response_body_size); - json_object_object_add(response_object, "body", response_body); - - json_object *response_headers = json_object_new_object(); - for (HTTPHeader *response_header = ctx->response_headers; - (ngx_uint_t)response_header < - (ngx_uint_t)ctx->response_headers + - ctx->response_header_count * sizeof(HTTPHeader); - response_header++) { - json_object *header_value_array = json_object_new_array_ext(1); - json_object_object_add(response_headers, (char *)response_header->key.data, - header_value_array); - json_object *header_value = json_object_new_string_len( - (char *)response_header->value.data, (int)response_header->value.len); - json_object_array_add(header_value_array, header_value); - } - json_object_object_add(response_object, "headers", response_headers); - - // Log it - ngx_log_error( - NGX_LOG_DEBUG, request->connection->log, 0, "%s", - json_object_to_json_string_ext(log_root, JSON_C_TO_STRING_PRETTY)); - - // Curl the Firetail logging API - // TODO: replace this with multi curl for non-blocking requests - CURLM *multiHandler = curl_multi_init(); - CURL *curlHandler = curl_easy_init(); - - int still_running = 0; - - if (curlHandler == NULL) { - return kNextResponseBodyFilter(request, chain_head); - } - - // Don't log the response - curl_easy_setopt(curlHandler, CURLOPT_WRITEFUNCTION, - LibcurlNoopWriteFunction); - - // Set CURLOPT_ACCEPT_ENCODING otherwise emoji will break things 🥲 - curl_easy_setopt(curlHandler, CURLOPT_ACCEPT_ENCODING, ""); - - // The request headers need to specify Content-Type: application/nd-json - struct curl_slist *curl_headers = NULL; - curl_headers = - curl_slist_append(curl_headers, "Content-Type: application/nd-json"); - - // The headers also need to provide the Firetail API key - // TODO: check this wayyyyy earlier so we don't wastefully generate JSON etc. - FiretailMainConfig *main_config = - ngx_http_get_module_main_conf(request, ngx_firetail_module); - if (main_config->FiretailApiToken.len > 0) { - char *x_ft_api_key = - ngx_palloc(request->pool, strlen("x-ft-api-key: ") + - main_config->FiretailApiToken.len); - ngx_memcpy(x_ft_api_key, "x-ft-api-key: ", strlen("x-ft-api-key: ")); - ngx_memcpy(x_ft_api_key + strlen("x-ft-api-key: "), - main_config->FiretailApiToken.data, - main_config->FiretailApiToken.len); - *(x_ft_api_key + strlen("x-ft-api-key: ") + - main_config->FiretailApiToken.len) = '\0'; - curl_headers = curl_slist_append(curl_headers, x_ft_api_key); - } else { - ngx_log_error(NGX_LOG_DEBUG, request->connection->log, 0, - "FIRETAIL_API_KEY environment variable unset. Not sending " - "log to Firetail."); - return kNextResponseBodyFilter(request, chain_head); - } - - // Add the headers to the request - curl_easy_setopt(curlHandler, CURLOPT_HTTPHEADER, curl_headers); - - // Our request body is just the log_root as a JSON string. When we add more - // logs we'll need to add '\n' characters to separate them - curl_easy_setopt(curlHandler, CURLOPT_POSTFIELDS, - json_object_to_json_string(log_root)); - - // We're making a POST request to the /logs/bulk endpoint/ - curl_easy_setopt(curlHandler, CURLOPT_CUSTOMREQUEST, "POST"); - curl_easy_setopt(curlHandler, CURLOPT_URL, - "https://api.logging.eu-west-1.prod.firetail.app/logs/bulk"); - - // Do the request - curl_multi_add_handle(multiHandler, curlHandler); - curl_multi_perform(multiHandler, &still_running); - - // removing handler will incur performance penalty - // we do cleanups - curl_easy_cleanup(curlHandler); - - // Pass the chain onto the next response body filter - return kNextResponseBodyFilter(request, chain_head); -} diff --git a/src/firetail_module.c b/src/firetail_module.c deleted file mode 100644 index d1d6ad8..0000000 --- a/src/firetail_module.c +++ /dev/null @@ -1,94 +0,0 @@ -#include -#include -#include -#include "filter_context.h" -#include "filter_headers.h" -#include "filter_request_body.h" -#include "filter_response_body.h" -#include "firetail_module.h" - -ngx_http_output_header_filter_pt kNextHeaderFilter; -ngx_http_output_body_filter_pt kNextResponseBodyFilter; -ngx_http_request_body_filter_pt kNextRequestBodyFilter; - -ngx_int_t FiretailInit(ngx_conf_t *cf) { - kNextRequestBodyFilter = ngx_http_top_request_body_filter; - ngx_http_top_request_body_filter = FiretailRequestBodyFilter; - - kNextResponseBodyFilter = ngx_http_top_body_filter; - ngx_http_top_body_filter = FiretailResponseBodyFilter; - - kNextHeaderFilter = ngx_http_top_header_filter; - ngx_http_top_header_filter = FiretailHeaderFilter; - - return NGX_OK; -} - -static void *CreateFiretailMainConfig(ngx_conf_t *configuration_object) { - FiretailMainConfig *http_main_config = - ngx_pcalloc(configuration_object->pool, sizeof(FiretailMainConfig)); - if (http_main_config == NULL) { - return NULL; - } - ngx_str_t firetail_api_token = ngx_string(""); - http_main_config->FiretailApiToken = firetail_api_token; - return http_main_config; -} - -static char *InitFiretailMainConfig(ngx_conf_t *configuration_object, - void *http_main_config) { - return NGX_CONF_OK; -} - -ngx_http_module_t kFiretailModuleContext = { - NULL, // preconfiguration - FiretailInit, // postconfiguration - CreateFiretailMainConfig, // create main configuration - InitFiretailMainConfig, // init main configuration - NULL, // create server configuration - NULL, // merge server configuration - NULL, // create location configuration - NULL // merge location configuration -}; - -char *EnableFiretailDirectiveInit(ngx_conf_t *configuration_object, - ngx_command_t *command_definition, - void *http_main_config) { - // TODO: validate the args given to the directive - - // Find the firetail_api_key_field given the config pointer & offset in cmd - char *firetail_config = http_main_config; - ngx_str_t *firetail_api_key_field = - (ngx_str_t *)(firetail_config + command_definition->offset); - - // Get the string value from the configuraion object - ngx_str_t *value = configuration_object->args->elts; - *firetail_api_key_field = value[1]; - - return NGX_CONF_OK; -} - -ngx_command_t kFiretailCommands[2] = { - {// Name of the directive - ngx_string("firetail_api_token"), - // Valid in the main config and takes one arg - NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE1, - // A callback function to be called when the directive is found in the - // configuration - EnableFiretailDirectiveInit, NGX_HTTP_MAIN_CONF_OFFSET, - offsetof(FiretailMainConfig, FiretailApiToken), NULL}, - ngx_null_command}; - -ngx_module_t ngx_firetail_module = { - NGX_MODULE_V1, - &kFiretailModuleContext, /* module context */ - kFiretailCommands, /* module directives */ - NGX_HTTP_MODULE, /* module type */ - NULL, /* init master */ - NULL, /* init module */ - NULL, /* init process */ - NULL, /* init thread */ - NULL, /* exit thread */ - NULL, /* exit process */ - NULL, /* exit master */ - NGX_MODULE_V1_PADDING}; \ No newline at end of file diff --git a/src/config b/src/nginx_module/config similarity index 91% rename from src/config rename to src/nginx_module/config index 68757a9..2518a4b 100644 --- a/src/config +++ b/src/nginx_module/config @@ -3,7 +3,7 @@ ngx_addon_name=ngx_firetail_module FIRETAIL_SRCS=" \ $ngx_addon_dir/firetail_module.c \ $ngx_addon_dir/filter_context.c \ - $ngx_addon_dir/filter_request_body.c \ + $ngx_addon_dir/filter_firetail_send.c \ $ngx_addon_dir/filter_response_body.c \ $ngx_addon_dir/filter_headers.c \ " @@ -11,7 +11,7 @@ FIRETAIL_SRCS=" \ FIRETAIL_DEPS=" \ $ngx_addon_dir/firetail_module.h \ $ngx_addon_dir/filter_context.h \ - $ngx_addon_dir/filter_request_body.h \ + $ngx_addon_dir/filter_firetail_send.h \ $ngx_addon_dir/filter_response_body.h \ $ngx_addon_dir/filter_headers.h \ " diff --git a/src/filter_context.c b/src/nginx_module/filter_context.c similarity index 99% rename from src/filter_context.c rename to src/nginx_module/filter_context.c index fe58e33..64c7f8a 100644 --- a/src/filter_context.c +++ b/src/nginx_module/filter_context.c @@ -13,4 +13,4 @@ FiretailFilterContext *GetFiretailFilterContext(ngx_http_request_t *request) { ngx_http_set_ctx(request, ctx, ngx_firetail_module); } return ctx; -} \ No newline at end of file +} diff --git a/src/filter_context.h b/src/nginx_module/filter_context.h similarity index 84% rename from src/filter_context.h rename to src/nginx_module/filter_context.h index 228e3a7..efbf98c 100644 --- a/src/filter_context.h +++ b/src/nginx_module/filter_context.h @@ -22,10 +22,15 @@ typedef struct { long response_header_count; HTTPHeader *request_headers; HTTPHeader *response_headers; + ngx_uint_t done; + ngx_uint_t bypass_response; + u_char *request_result; + HTTPHeader *recorded_request_header; + long recorded_request_header_size; } FiretailFilterContext; // This utility function will allow us to get the filter ctx whenever we need // it, and creates it if it doesn't already exist FiretailFilterContext *GetFiretailFilterContext(ngx_http_request_t *request); -#endif \ No newline at end of file +#endif diff --git a/src/nginx_module/filter_firetail_send.c b/src/nginx_module/filter_firetail_send.c new file mode 100644 index 0000000..7d640c2 --- /dev/null +++ b/src/nginx_module/filter_firetail_send.c @@ -0,0 +1,198 @@ +#include +#include +#include "filter_context.h" +#include "firetail_module.h" +#include "filter_firetail_send.h" +#include + +/* reason this exists is that we want to send our module response to the user + only when we have a response from the server to validate the request Info here: + https://mailman.nginx.org/pipermail/nginx-devel/2023-September/ARTW6X573LPVCRQJNZEWT33W4PFEKIPR.html +*/ +ngx_int_t ngx_http_firetail_send(ngx_http_request_t *request, + FiretailFilterContext *ctx, ngx_buf_t *b, + char *error) { + ngx_int_t rc; + ngx_chain_t out; + ngx_pool_cleanup_t *cln; + + struct json_object *jobj; + char *code; + + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "Start of firetail send", NULL); + + ctx->done = 1; + + if (b == NULL) { + // if there is an spec validation error by sending out + // the error response gotten from the middleware + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, "Buffer is null", + NULL); + + // response parse the middleware json response + jobj = json_tokener_parse(error); + // Get the string value in "code" json key + code = (char *)json_object_get_string(json_object_object_get(jobj, "code")); + + // Set the middleware status code after converting string status code to + // integer + request->headers_out.status = ngx_atoi((u_char *)code, strlen(code)); + + // request->headers_out.status = NGX_HTTP_BAD_REQUEST; + ngx_str_t content_type = ngx_string("application/json"); + request->headers_out.content_type = content_type; + + // allocate buffer in pool + b = ngx_calloc_buf(request->pool); + // set the error as unsigned char + u_char *msg = (u_char *)error; + b->pos = msg; + b->last = msg + strlen((char *)msg); + b->memory = 1; + + b->last_buf = 1; + } + + cln = ngx_pool_cleanup_add(request->pool, 0); + if (cln == NULL) { + ngx_free(b->pos); + return ngx_http_filter_finalize_request(request, &ngx_firetail_module, + NGX_HTTP_INTERNAL_SERVER_ERROR); + } + + if (request == request->main) { + request->headers_out.content_length_n = b->last - b->pos; + + if (request->headers_out.content_length) { + request->headers_out.content_length->hash = 0; + request->headers_out.content_length = NULL; + } + } + + request->keepalive = 0; + + rc = kNextHeaderFilter(request); + + if (rc == NGX_ERROR || rc > NGX_OK || request->header_only) { + ngx_free(b->pos); + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "SENDING HEADERS...", NULL); + return rc; + } + + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "Sending next RESPONSE body", NULL); + + cln->handler = ngx_http_firetail_cleanup; + cln->data = b->pos; + + out.buf = b; + out.next = NULL; + + return kNextResponseBodyFilter(request, &out); +} + +ngx_int_t ngx_http_firetail_request(ngx_http_request_t *request, ngx_buf_t *b, + ngx_chain_t *chain_head, char *error) { + ngx_chain_t out; + char *code; + struct json_object *jobj; + ngx_int_t rc; + + FiretailFilterContext *ctx = GetFiretailFilterContext(request); + + char empty_json[2] = "{}"; + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "INCOMING Request Body: %s, json %s", ctx->request_body, + empty_json); + + if (b == NULL) { + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "Buffer for REQUEST is null", NULL); + + // bypass response validation because we no longer need + // to go through response validation + ctx->bypass_response = 0; + if (ctx->request_body || (char *)ctx->request_body != empty_json) { + ctx->bypass_response = 1; + } + ctx->request_result = (u_char *)error; + + // response parse the middleware json response + jobj = json_tokener_parse(error); + // Get the string value in "code" json key + code = (char *)json_object_get_string(json_object_object_get(jobj, "code")); + + // return and finalize request early since we are not going to send + // it to upstream server + ngx_str_t content_type = ngx_string("application/json"); + request->headers_out.content_type = content_type; + // convert "code" which is string to integer (status), example: 200 + request->headers_out.status = ngx_atoi((u_char *)code, strlen(code)); + request->keepalive = 0; + + rc = ngx_http_send_header(request); + if (rc == NGX_ERROR || rc > NGX_OK || request->header_only) { + ngx_http_finalize_request(request, rc); + return NGX_DONE; + } + + // allocate buffer in pool + b = ngx_calloc_buf(request->pool); + // set the error as unsigned char + u_char *msg = (u_char *)error; + b->pos = msg; + b->last = msg + strlen((char *)msg); + b->memory = 1; + + b->last_buf = 1; + + out.buf = b; + out.next = NULL; + + rc = ngx_http_output_filter(request, &out); + + ngx_http_finalize_request(request, rc); + } + + if (request == request->main) { + request->headers_out.content_length_n = b->last - b->pos; + + if (request->headers_out.content_length) { + request->headers_out.content_length->hash = 0; + request->headers_out.content_length = NULL; + } + } + + out.buf = b; + out.next = NULL; + + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "Sending next REQUEST body", NULL); + return NGX_OK; + // return kNextRequestBodyFilter(request, &out); +} + +ngx_buf_t *ngx_http_filter_buffer(ngx_http_request_t *request, + u_char *response) { + ngx_buf_t *b; + + b = ngx_calloc_buf(request->pool); + if (b == NULL) { + return NULL; + } + + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "Buffer is successful", NULL); + + b->pos = response; + b->last = response + strlen((char *)response); + b->memory = 1; + + b->last_buf = 1; + + return b; +} + +void ngx_http_firetail_cleanup(void *data) { ngx_free(data); } diff --git a/src/nginx_module/filter_firetail_send.h b/src/nginx_module/filter_firetail_send.h new file mode 100644 index 0000000..b2e3766 --- /dev/null +++ b/src/nginx_module/filter_firetail_send.h @@ -0,0 +1,8 @@ +ngx_int_t ngx_http_firetail_send(ngx_http_request_t *r, + FiretailFilterContext *ctx, ngx_buf_t *b, + char *error); +ngx_int_t ngx_http_firetail_request(ngx_http_request_t *request, ngx_buf_t *b, + ngx_chain_t *chain_head, char *error); +ngx_buf_t *ngx_http_filter_buffer(ngx_http_request_t *request, + u_char *response); +void ngx_http_firetail_cleanup(void *data); diff --git a/src/filter_headers.c b/src/nginx_module/filter_headers.c similarity index 95% rename from src/filter_headers.c rename to src/nginx_module/filter_headers.c index f2cadaf..bfc9d03 100644 --- a/src/filter_headers.c +++ b/src/nginx_module/filter_headers.c @@ -5,6 +5,9 @@ ngx_int_t FiretailHeaderFilter(ngx_http_request_t *request) { FiretailFilterContext *ctx = GetFiretailFilterContext(request); + if (ctx == NULL) { + return NGX_ERROR; + } // Copy the status code and server out of the headers ctx->status_code = request->headers_out.status; @@ -68,5 +71,8 @@ ngx_int_t FiretailHeaderFilter(ngx_http_request_t *request) { } } - return kNextHeaderFilter(request); + request->main_filter_need_in_memory = 1; + request->allow_ranges = 0; + + return NGX_OK; } diff --git a/src/filter_headers.h b/src/nginx_module/filter_headers.h similarity index 100% rename from src/filter_headers.h rename to src/nginx_module/filter_headers.h diff --git a/src/nginx_module/filter_response_body.c b/src/nginx_module/filter_response_body.c new file mode 100644 index 0000000..87f39d8 --- /dev/null +++ b/src/nginx_module/filter_response_body.c @@ -0,0 +1,131 @@ +#include +#include +#include +#include "filter_context.h" +#include "filter_response_body.h" +#include "firetail_module.h" +#include "filter_firetail_send.h" + +ngx_int_t FiretailResponseBodyFilter(ngx_http_request_t *request, + ngx_chain_t *chain_head) { + struct ValidateResponseBody_return validation_result; + + // You can set the logging level to debug here + // request->connection->log->log_level = NGX_LOG_DEBUG; + + // Get our context so we can store the response body data + FiretailFilterContext *ctx = GetFiretailFilterContext(request); + if (ctx == NULL || ctx->done) { + return kNextResponseBodyFilter(request, chain_head); + } + + // Determine the length of the response body chain we've been given, and if + // the chain contains the last link + long new_response_body_parts_size = 0; + + for (ngx_chain_t *current_chain_link = chain_head; current_chain_link != NULL; + current_chain_link = current_chain_link->next) { + new_response_body_parts_size += + current_chain_link->buf->last - current_chain_link->buf->pos; + } + + // If we read in more bytes from this chain then we need to create a new char* + // and place it in ctx containing the full body + if (new_response_body_parts_size > 0) { + // Take note of the response body size before and after adding the new chain + // & update it in our ctx + long old_response_body_size = ctx->response_body_size; + long new_response_body_size = + old_response_body_size + new_response_body_parts_size; + ctx->response_body_size = new_response_body_size; + + // Create a new updated body + u_char *updated_response_body = + ngx_pcalloc(request->pool, new_response_body_size); + + // Copy the body read so far into ctx into our new updated_response_body + u_char *updated_response_body_i = ngx_copy( + updated_response_body, ctx->response_body, old_response_body_size); + + // Iterate over the chain again and copy all of the buffers over to our new + // response body char* + for (ngx_chain_t *current_chain_link = chain_head; + current_chain_link != NULL; + current_chain_link = current_chain_link->next) { + long buffer_length = + current_chain_link->buf->last - current_chain_link->buf->pos; + updated_response_body_i = ngx_copy( + updated_response_body_i, current_chain_link->buf->pos, buffer_length); + } + + // Update the ctx with the new updated body + ctx->response_body = updated_response_body; + + ngx_pfree(request->pool, updated_response_body); + } + + FiretailMainConfig *main_config = + ngx_http_get_module_main_conf(request, ngx_firetail_module); + + // If it does contain the last buffer, we can validate it with our go lib. + // NOTE: I'm currently loading this dynamic module in every time we need to + // call it. If I do it once at startup, it would just hang when I call the + // response body validator _sometimes_. Couldn't figure out why. Creating the + // middleware on the go side of things every time will be very inefficient. + + if (ctx->bypass_response == 0) { + void *validator_module = + dlopen("/etc/nginx/modules/firetail-validator.so", RTLD_LAZY); + if (!validator_module) { + return NGX_ERROR; + } + + ValidateResponseBody response_body_validator = + (ValidateResponseBody)dlsym(validator_module, "ValidateResponseBody"); + char *error; + if ((error = dlerror()) != NULL) { + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "Failed to load ValidateResponseBody: %s", error); + exit(1); + } + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "Validating response body..."); + + char *schema = ngx_palloc(request->pool, main_config->FiretailAppSpec.len); + ngx_memcpy(schema, main_config->FiretailAppSpec.data, + main_config->FiretailAppSpec.len); + + validation_result = response_body_validator( + (char *)main_config->FiretailUrl.data, main_config->FiretailUrl.len, + (char *)main_config->FiretailApiToken.data, + main_config->FiretailApiToken.len, (char *)ctx->request_body, + (int)ctx->request_body_size, schema, strlen(schema), ctx->response_body, + ctx->response_body_size, request->unparsed_uri.data, + request->unparsed_uri.len, ctx->status_code, request->method_name.data, + request->method_name.len); + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "Validation response result: %d", validation_result.r0); + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "Validating response body: %s", validation_result.r1); + + ngx_pfree(request->pool, schema); + + // if validation result is not successful + if (validation_result.r0 > 0) { + return ngx_http_firetail_send(request, ctx, NULL, validation_result.r1); + } + + dlclose(validator_module); + } else { + validation_result.r1 = (char *)ctx->request_result; + } + + if (ctx->bypass_response == 1) + return ngx_http_firetail_send( + request, ctx, + ngx_http_filter_buffer(request, (u_char *)ctx->request_result), NULL); + + return ngx_http_firetail_send( + request, ctx, + ngx_http_filter_buffer(request, (u_char *)validation_result.r1), NULL); +} diff --git a/src/filter_response_body.h b/src/nginx_module/filter_response_body.h similarity index 97% rename from src/filter_response_body.h rename to src/nginx_module/filter_response_body.h index 84f3076..c4b2004 100644 --- a/src/filter_response_body.h +++ b/src/nginx_module/filter_response_body.h @@ -6,5 +6,4 @@ ngx_int_t FiretailResponseBodyFilter(ngx_http_request_t *request, ngx_chain_t *chain_head); - -#endif \ No newline at end of file +#endif diff --git a/src/nginx_module/firetail_module.c b/src/nginx_module/firetail_module.c new file mode 100644 index 0000000..c5ef27d --- /dev/null +++ b/src/nginx_module/firetail_module.c @@ -0,0 +1,342 @@ +#include +#include +#include +#include "filter_context.h" +#include "filter_headers.h" +#include "filter_response_body.h" +#include "firetail_module.h" +#include "filter_firetail_send.h" +#include + +#define SIZE 65536 + +ngx_http_output_header_filter_pt kNextHeaderFilter; +ngx_http_output_body_filter_pt kNextResponseBodyFilter; + +typedef struct { + ngx_int_t status; +} ngx_http_firetail_ctx_t; + +static ngx_int_t ngx_http_firetail_handler_internal(ngx_http_request_t *r); +static void ngx_http_firetail_body_handler(ngx_http_request_t *r); +static ngx_int_t ngx_http_firetail_handler(ngx_http_request_t *r); + +static ngx_int_t ngx_http_firetail_handler_internal( + ngx_http_request_t *request) { + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "✅️✅️✅️HANDLER INTERNAL✅️✅️✅️"); + + ngx_chain_t *chain_head; + chain_head = request->request_body->bufs; + + // Get our context so we can store the request body data + FiretailFilterContext *ctx = GetFiretailFilterContext(request); + if (ctx == NULL) { + return NGX_ERROR; + } + + // get the header values + ngx_list_part_t *part; + ngx_table_elt_t *h; + ngx_uint_t i; + + part = &request->headers_in.headers.part; + h = part->elts; + + json_object *log_root = json_object_new_object(); + + for (i = 0;; i++) { + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + json_object *jobj = json_object_new_string((char *)h[i].value.data); + + json_object *array = json_object_new_array(); + json_object_array_add(array, jobj); + + json_object_object_add(log_root, (char *)h[i].key.data, array); + } + + void *h_json_string = (void *)json_object_to_json_string(log_root); + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, "json value %s", + h_json_string); + + // Determine the length of the request body chain we've been given + long new_request_body_parts_size = 0; + for (ngx_chain_t *current_chain_link = chain_head; current_chain_link != NULL; + current_chain_link = current_chain_link->next) { + new_request_body_parts_size += + current_chain_link->buf->last - current_chain_link->buf->pos; + } + + // Take note of the request body size before and after adding the new chain + // & update it in our ctx + long old_request_body_size = ctx->request_body_size; + long new_request_body_size = + old_request_body_size + new_request_body_parts_size; + ctx->request_body_size = new_request_body_size; + + // Create a new updated body + u_char *updated_request_body = + ngx_pcalloc(request->pool, new_request_body_size); + + // Copy the body read so far into ctx into our new updated_request_body + u_char *updated_request_body_i = + ngx_copy(updated_request_body, ctx->request_body, old_request_body_size); + + // Iterate over the chain again and copy all of the buffers over to our new + // request body char* + for (ngx_chain_t *current_chain_link = chain_head; current_chain_link != NULL; + current_chain_link = current_chain_link->next) { + long buffer_length = + current_chain_link->buf->last - current_chain_link->buf->pos; + updated_request_body_i = ngx_copy( + updated_request_body_i, current_chain_link->buf->pos, buffer_length); + } + + // Update the ctx with the new updated body + ctx->request_body = updated_request_body; + // run the validation + FiretailMainConfig *main_config = + ngx_http_get_module_main_conf(request, ngx_firetail_module); + + void *validator_module = + dlopen("/etc/nginx/modules/firetail-validator.so", RTLD_LAZY); + if (!validator_module) { + return NGX_ERROR; + } + + ValidateRequestBody request_body_validator = + (ValidateRequestBody)dlsym(validator_module, "ValidateRequestBody"); + char *error; + if ((error = dlerror()) != NULL) { + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "Failed to load ValidateRequestBody: %s", error); + exit(1); + } + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "Validating request body..."); + + char *schema = ngx_palloc(request->pool, main_config->FiretailAppSpec.len); + ngx_memcpy(schema, main_config->FiretailAppSpec.data, + main_config->FiretailAppSpec.len); + + struct ValidateRequestBody_return validation_result = request_body_validator( + schema, strlen(schema), ctx->request_body, ctx->request_body_size, + request->unparsed_uri.data, request->unparsed_uri.len, + request->method_name.data, request->method_name.len, h_json_string, + strlen(h_json_string)); + + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "Validation request result: %d", validation_result.r0); + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + "Validating request body: %s", validation_result.r1); + + // if validation is unsuccessful, return bad request + if (validation_result.r0 > 0) + return ngx_http_firetail_request(request, NULL, chain_head, + validation_result.r1); + + // else continue request + ngx_pfree(request->pool, schema); + + dlclose(validator_module); + + return NGX_OK; // can be NGX_DECLINED - see ngx_http_mirror_handler_internal + // function in nginx mirror module +} + +static void ngx_http_firetail_body_handler(ngx_http_request_t *request) { + ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, + " ✅️✅️✅️RUNNING BODY HANDLER %s✅️✅️✅️", + request->request_body); + + ngx_http_firetail_handler_internal(request); + + request->preserve_body = 1; + + request->write_event_handler = ngx_http_core_run_phases; + ngx_http_core_run_phases(request); +} + +static ngx_int_t ngx_http_firetail_handler(ngx_http_request_t *r) { + ngx_int_t rc; + ngx_http_firetail_ctx_t *ctx; + + if (r != r->main) { + return NGX_DECLINED; + } + + ctx = ngx_http_get_module_ctx(r, ngx_firetail_module); + + if (ctx) { + return ctx->status; + } + + ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_firetail_ctx_t)); + if (ctx == NULL) { + return NGX_ERROR; + } + + ctx->status = NGX_DONE; + + rc = ngx_http_read_client_request_body(r, ngx_http_firetail_body_handler); + if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { + return rc; + } + + ngx_http_finalize_request(r, NGX_DONE); + return NGX_DONE; +} + +ngx_int_t FiretailInit(ngx_conf_t *cf) { + ngx_http_handler_pt *h; + ngx_http_core_main_conf_t *cmcf; + + cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); + + h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); + if (h == NULL) { + return NGX_ERROR; + } + + *h = ngx_http_firetail_handler; + + kNextResponseBodyFilter = ngx_http_top_body_filter; + ngx_http_top_body_filter = FiretailResponseBodyFilter; + + kNextHeaderFilter = ngx_http_top_header_filter; + ngx_http_top_header_filter = FiretailHeaderFilter; + + return NGX_OK; +} + +static void *CreateFiretailMainConfig(ngx_conf_t *configuration_object) { + FiretailMainConfig *http_main_config = + ngx_pcalloc(configuration_object->pool, sizeof(FiretailMainConfig)); + if (http_main_config == NULL) { + return NULL; + } + + ngx_str_t firetail_api_token = ngx_string(""); + ngx_str_t firetail_url = ngx_string(""); + http_main_config->FiretailApiToken = firetail_api_token; + http_main_config->FiretailUrl = firetail_url; + + // load appspec schema + // schema file pointer + FILE *schema; + + // initialize variables with an arbitary size + char data[SIZE]; + char str[SIZE]; + + printf("Loading AppSpec Schema...\n"); + + // open schema spec + schema = fopen("/etc/nginx/appspec.yml", "r"); + if (schema == NULL) { + printf("Error! count not load schema"); + exit(1); + } + + // resize data + while (fgets(str, SIZE, schema)) { + // concatenate the string with line termination \0 + // at the end + strcat(data, str); + } + + ngx_str_t spec = ngx_string(data); + + http_main_config->FiretailAppSpec = spec; + + fclose(schema); + + return http_main_config; +} + +static char *InitFiretailMainConfig(ngx_conf_t *configuration_object, + void *http_main_config) { + return NGX_CONF_OK; +} + +ngx_http_module_t kFiretailModuleContext = { + NULL, // preconfiguration + FiretailInit, // postconfiguration + CreateFiretailMainConfig, // create main configuration + InitFiretailMainConfig, // init main configuration + NULL, // create server configuration + NULL, // merge server configuration + NULL, // create location configuration + NULL // merge location configuration +}; + +char *EnableFiretailDirectiveInit(ngx_conf_t *configuration_object, + ngx_command_t *command_definition, + void *http_main_config) { + // TODO: validate the args given to the directive + + // Find the firetail_api_key_field given the config pointer & offset in cmd + char *firetail_config = http_main_config; + ngx_str_t *firetail_api_key_field = + (ngx_str_t *)(firetail_config + command_definition->offset); + + // Get the string value from the configuraion object + ngx_str_t *value = configuration_object->args->elts; + *firetail_api_key_field = value[1]; + + return NGX_CONF_OK; +} + +char *EnableFiretailUrlInit(ngx_conf_t *configuration_object, + ngx_command_t *command_definition, + void *http_main_config) { + // TODO: validate the args given to the directive + + // Find the firetail_api_key_field given the config pointer & offset in cmd + char *firetail_config = http_main_config; + ngx_str_t *firetail_url_field = + (ngx_str_t *)(firetail_config + command_definition->offset); + + // Get the string value from the configuraion object + ngx_str_t *value = configuration_object->args->elts; + *firetail_url_field = value[1]; + + return NGX_CONF_OK; +} + +ngx_command_t kFiretailCommands[3] = { + {// Name of the directive + ngx_string("firetail_api_token"), + // Valid in the main config and takes one arg + NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE1, + // A callback function to be called when the directive is found in the + // configuration + EnableFiretailDirectiveInit, NGX_HTTP_MAIN_CONF_OFFSET, + offsetof(FiretailMainConfig, FiretailApiToken), NULL}, + {ngx_string("firetail_url"), NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE1, + EnableFiretailUrlInit, NGX_HTTP_MAIN_CONF_OFFSET, + offsetof(FiretailMainConfig, FiretailUrl), NULL}, + ngx_null_command}; + +ngx_module_t ngx_firetail_module = { + NGX_MODULE_V1, + &kFiretailModuleContext, /* module context */ + kFiretailCommands, /* module directives */ + NGX_HTTP_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + NULL, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING}; diff --git a/src/firetail_module.h b/src/nginx_module/firetail_module.h similarity index 53% rename from src/firetail_module.h rename to src/nginx_module/firetail_module.h index 8aa5eea..90afd64 100644 --- a/src/firetail_module.h +++ b/src/nginx_module/firetail_module.h @@ -3,18 +3,36 @@ #include +typedef int (*CreateMiddlewareFunc)(void*, int); + +struct ValidateResponseBody_return { + int r0; + char* r1; +}; +typedef struct ValidateResponseBody_return (*ValidateResponseBody)( + char*, int, char*, int, char*, int, char*, int, void*, int, void*, int, int, + void*, int); + +struct ValidateRequestBody_return { + int r0; + char* r1; +}; +typedef struct ValidateRequestBody_return (*ValidateRequestBody)( + char*, int, void*, int, void*, int, void*, int, void*, int); + // This config struct will hold our API key typedef struct { ngx_str_t FiretailApiToken; // TODO: this should probably be a *ngx_str_t + ngx_str_t FiretailAppSpec; + ngx_str_t FiretailUrl; } FiretailMainConfig; // The header and body filters of the filter that was added just before ours. // These make up part of a singly linked list of header and body filters. extern ngx_http_output_header_filter_pt kNextHeaderFilter; extern ngx_http_output_body_filter_pt kNextResponseBodyFilter; -extern ngx_http_request_body_filter_pt kNextRequestBodyFilter; // This struct defines the Firetail NGINX module's hooks extern ngx_module_t ngx_firetail_module; -#endif \ No newline at end of file +#endif diff --git a/src/validator/go.mod b/src/validator/go.mod new file mode 100644 index 0000000..05d96bf --- /dev/null +++ b/src/validator/go.mod @@ -0,0 +1,19 @@ +module firetail-validator + +go 1.20 + +require github.com/FireTail-io/firetail-go-lib v0.0.0 + +require ( + github.com/getkin/kin-openapi v0.110.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/invopop/yaml v0.2.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/FireTail-io/firetail-go-lib => github.com/FireTail-io/firetail-go-lib v0.2.1 diff --git a/src/validator/go.sum b/src/validator/go.sum new file mode 100644 index 0000000..0fd7ba7 --- /dev/null +++ b/src/validator/go.sum @@ -0,0 +1,51 @@ +github.com/FireTail-io/firetail-go-lib v0.2.1 h1:Jf3C1mAtCHAHHqSeuaMx6sSRmUS7OjgY4B2nY8XfibU= +github.com/FireTail-io/firetail-go-lib v0.2.1/go.mod h1:PH4aGBwry6z/3vzXEdcMaxK22E3xqPq2+w2y3FzETj4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/getkin/kin-openapi v0.110.0 h1:1GnJALxsltcSzCMqgtqKlLhYQeULv3/jesmV2sC5qE0= +github.com/getkin/kin-openapi v0.110.0/go.mod h1:QtwUNt0PAAgIIBEvFWYfB7dfngxtAaqCX1zYHMZDeK8= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= +github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/sbabiv/xml2map v1.2.1 h1:1lT7t0hhUvXZCkdxqtq4n8/ZCnwLWGq4rDuDv5XOoFE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/validator/main.go b/src/validator/main.go new file mode 100644 index 0000000..5ba34a8 --- /dev/null +++ b/src/validator/main.go @@ -0,0 +1,182 @@ +package main + +import ( + "C" + "encoding/json" + "log" + "strings" + "unsafe" + + "bytes" + "io" + "net/http/httptest" + _ "net/http/pprof" + + firetail "github.com/FireTail-io/firetail-go-lib/middlewares/http" +) + +//export ValidateRequestBody +func ValidateRequestBody(specBytes unsafe.Pointer, specLength C.int, + bodyCharPtr unsafe.Pointer, bodyLength C.int, + pathCharPtr unsafe.Pointer, pathLength C.int, + methodCharPtr unsafe.Pointer, methodLength C.int, + headersCharPtr unsafe.Pointer, headersLength C.int) (C.int, *C.char) { + + specSlice := C.GoBytes(specBytes, specLength) + + firetailMiddleware, err := firetail.GetMiddleware(&firetail.Options{ + OpenapiBytes: specSlice, + LogsApiToken: "", + LogsApiUrl: "", + DebugErrs: true, + EnableRequestValidation: true, + EnableResponseValidation: false, + }) + if err != nil { + log.Println("Failed to initialise Firetail middleware, err:", err.Error()) + // return 1 is error by convention + return 1, nil + } + + pathSlice := C.GoBytes(pathCharPtr, pathLength) + bodySlice := C.GoBytes(bodyCharPtr, bodyLength) + methodSlice := C.GoBytes(methodCharPtr, methodLength) + headersSlice := C.GoBytes(headersCharPtr, headersLength) + + var headers map[string][]string + if err := json.Unmarshal(headersSlice, &headers); err != nil { + panic(err) + } + + // Create a fake handler + placeholderResponse := []byte{} + myHandler := &stubHandler{ + responseCode: 200, + responseBytes: placeholderResponse, + } + + // Create our middleware instance with the stub handler + myMiddleware := firetailMiddleware(myHandler) + + // Create a local response writer to record what the middleware says we should respond with + localResponseWriter := httptest.NewRecorder() + + mockRequest := httptest.NewRequest( + string(methodSlice), string(pathSlice), + io.NopCloser(bytes.NewBuffer(bodySlice))) + + for k, v := range headers { + // convert value (v) to comma-delimited values + // key "k" is still as it is + mockRequest.Header.Add(k, strings.Join(v[:], ", ")) + } + + // Serve the request to the middlware + myMiddleware.ServeHTTP(localResponseWriter, mockRequest) + + // If the body differs after being passed through the middleware then we'll just infer it doesn't + // match the spec + middlewareResponseBodyBytes, err := io.ReadAll(localResponseWriter.Body) + response := C.CString(string(middlewareResponseBodyBytes)) + + if err != nil { + log.Println("Failed to read request body bytes from middleware, err:", err.Error()) + // return 1 is error by convention + return 1, response + } + if string(middlewareResponseBodyBytes) != string(placeholderResponse) { + log.Printf("Middleware altered response body, original: %s, new: %s", string(placeholderResponse), string(middlewareResponseBodyBytes)) + // return 1 is error by convention + return 1, response + } + + // return 0 is success by convention + return 0, response +} + +//export ValidateResponseBody +func ValidateResponseBody(urlCharPtr unsafe.Pointer, + urlLength C.int, + tokenCharPtr unsafe.Pointer, tokenLength C.int, + reqBodyCharPtr unsafe.Pointer, reqBodyLength C.int, + specBytes unsafe.Pointer, specLength C.int, + resBodyCharPtr unsafe.Pointer, resBodyLength C.int, + pathCharPtr unsafe.Pointer, pathLength C.int, + statusCode C.int, + methodCharPtr unsafe.Pointer, methodLength C.int) (C.int, *C.char) { + + specSlice := C.GoBytes(specBytes, specLength) + tokenSlice := C.GoBytes(tokenCharPtr, tokenLength) + urlSlice := C.GoBytes(urlCharPtr, urlLength) + + log.Println("URL: ", string(urlSlice)) + trimTokenSlice := strings.TrimSpace(string(tokenSlice)) + trimUrlSlice := strings.TrimSpace(string(urlSlice)) + + firetailMiddleware, err := firetail.GetMiddleware(&firetail.Options{ + OpenapiBytes: specSlice, + LogsApiToken: trimTokenSlice, + LogsApiUrl: trimUrlSlice, + DebugErrs: true, + EnableRequestValidation: false, + EnableResponseValidation: true, + }) + if err != nil { + log.Println("Failed to initialise Firetail middleware, err:", err.Error()) + return 0, nil + } + + resBodySlice := C.GoBytes(resBodyCharPtr, resBodyLength) + pathSlice := C.GoBytes(pathCharPtr, pathLength) + methodSlice := C.GoBytes(methodCharPtr, methodLength) + + // Create a handler returning the response body and status code from nginx + myHandler := &stubHandler{ + responseCode: int(statusCode), + responseBytes: resBodySlice, + } + + // Create our middleware instance with the stub handler + myMiddleware := firetailMiddleware(myHandler) + + // Create a local response writer to record what the middleware says we should respond with + localResponseWriter := httptest.NewRecorder() + + // Serve the request to the middlware + myMiddleware.ServeHTTP(localResponseWriter, httptest.NewRequest( + string(methodSlice), string(pathSlice), + io.NopCloser(bytes.NewBuffer(C.GoBytes(reqBodyCharPtr, reqBodyLength))), + )) + + // for profiling the CPU, uncomment this and run + // go tool pprof http://localhost:6060/debug/pprof/profile\?seconds\=30 + /*go func() { + log.Println(http.ListenAndServe("localhost:6060", nil)) + }() */ + + // If the response code or body differs after being passed through the middleware then we'll just infer it doesn't + // match the spec + middlewareResponseBodyBytes, err := io.ReadAll(localResponseWriter.Body) + response := C.CString(string(middlewareResponseBodyBytes)) + + if err != nil { + log.Println("Failed to read response body bytes from middleware, err:", err.Error()) + // return 1 is error by convention + return 1, response + } + if localResponseWriter.Code != int(statusCode) { + log.Printf("Middleware altered status code from %d to %d", statusCode, localResponseWriter.Code) + // return 1 is error by convention + return 1, response + } + if string(middlewareResponseBodyBytes) != string(resBodySlice) { + log.Printf("Middleware altered response body, original: %s, new: %s", string(resBodySlice), string(middlewareResponseBodyBytes)) + // return 1 is error by convention + return 1, response + } + + // return 0 is success by convention + return 0, response +} + +func main() {} diff --git a/src/validator/mock_backend.go b/src/validator/mock_backend.go new file mode 100644 index 0000000..c418d4f --- /dev/null +++ b/src/validator/mock_backend.go @@ -0,0 +1,20 @@ +package main + +import ( + "log" + "net/http" +) + +type stubHandler struct { + responseCode int + responseBytes []byte +} + +func (m *stubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(m.responseCode) + w.Header().Add("Content-Type", "application/json") + _, err := w.Write(m.responseBytes) + if err != nil { + log.Println("Mock backend failed to write bytes to ResponseWriter, err:", err.Error()) + } +}