diff --git a/README.md b/README.md index 307db35..8899d5f 100644 --- a/README.md +++ b/README.md @@ -235,7 +235,7 @@ To demonstrate request validation a `POST /proxy/profile/{username}/comment` ope 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!"} +curl localhost:8080/proxy/profile/alice/comment -X POST -H "Content-Type: application/json" -d '{"comment":"Hello world!"}' ``` ```json diff --git a/src/nginx_module/filter_response_body.c b/src/nginx_module/filter_response_body.c index 8011d3d..901ec7c 100644 --- a/src/nginx_module/filter_response_body.c +++ b/src/nginx_module/filter_response_body.c @@ -98,7 +98,8 @@ ngx_int_t FiretailResponseBodyFilter(ngx_http_request_t *request, ngx_chain_t *c 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, + main_config->FiretailApiToken.len, (char *)main_config->FiretailAllowUndefinedRoutes.data, + (int)main_config->FiretailAllowUndefinedRoutes.len, (char *)ctx->request_body, (int)ctx->request_body_size, (char *)ctx->request_headers_json, (int)ctx->request_headers_json_size, ctx->response_body, ctx->response_body_size, response_headers_json_string, strlen(response_headers_json_string), request->unparsed_uri.data, request->unparsed_uri.len, ctx->status_code, request->method_name.data, diff --git a/src/nginx_module/firetail_module.c b/src/nginx_module/firetail_module.c index 846cee3..05efb7b 100644 --- a/src/nginx_module/firetail_module.c +++ b/src/nginx_module/firetail_module.c @@ -88,6 +88,9 @@ static ngx_int_t ngx_http_firetail_handler_internal(ngx_http_request_t *request) // Update the ctx with the new updated body ctx->request_body = updated_request_body; + // Get the main config so we can check if we have 404s disabled from the middleware + FiretailMainConfig *main_config = ngx_http_get_module_main_conf(request, ngx_firetail_module); + // run the validation void *validator_module = dlopen("/etc/nginx/modules/firetail-validator.so", RTLD_LAZY); if (!validator_module) { @@ -102,10 +105,10 @@ static ngx_int_t ngx_http_firetail_handler_internal(ngx_http_request_t *request) } ngx_log_debug(NGX_LOG_DEBUG, request->connection->log, 0, "Validating request body..."); - struct ValidateRequestBody_return validation_result = - request_body_validator(ctx->request_body, ctx->request_body_size, request->unparsed_uri.data, - request->unparsed_uri.len, request->method_name.data, request->method_name.len, - (char *)ctx->request_headers_json, ctx->request_headers_json_size); + struct ValidateRequestBody_return validation_result = request_body_validator( + main_config->FiretailAllowUndefinedRoutes.data, main_config->FiretailAllowUndefinedRoutes.len, ctx->request_body, + ctx->request_body_size, request->unparsed_uri.data, request->unparsed_uri.len, request->method_name.data, + request->method_name.len, (char *)ctx->request_headers_json, ctx->request_headers_json_size); 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); @@ -210,8 +213,8 @@ ngx_http_module_t kFiretailModuleContext = { NULL // merge location configuration }; -char *EnableFiretailDirectiveInit(ngx_conf_t *configuration_object, ngx_command_t *command_definition, - void *http_main_config) { +char *FiretailApiTokenDirectiveCallback(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 @@ -225,8 +228,8 @@ char *EnableFiretailDirectiveInit(ngx_conf_t *configuration_object, ngx_command_ return NGX_CONF_OK; } -char *EnableFiretailUrlInit(ngx_conf_t *configuration_object, ngx_command_t *command_definition, - void *http_main_config) { +char *FiretailUrlDirectiveCallback(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 @@ -240,16 +243,43 @@ char *EnableFiretailUrlInit(ngx_conf_t *configuration_object, ngx_command_t *com return NGX_CONF_OK; } -ngx_command_t kFiretailCommands[3] = { +char *FiretailAllowUndefinedRoutesDirectiveCallback(ngx_conf_t *configuration_object, ngx_command_t *command_definition, + void *http_main_config) { + // Find the firetail_api_key_field given the config pointer & offset in cmd + char *firetail_config = http_main_config; + ngx_str_t *firetail_allow_undefined_routes_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_allow_undefined_routes_field = value[1]; + + return NGX_CONF_OK; +} + +ngx_command_t kFiretailCommands[4] = { {// 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}, + FiretailApiTokenDirectiveCallback, NGX_HTTP_MAIN_CONF_OFFSET, offsetof(FiretailMainConfig, FiretailApiToken), + NULL}, + {// Name of the directive + ngx_string("firetail_url"), + // 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 + FiretailUrlDirectiveCallback, NGX_HTTP_MAIN_CONF_OFFSET, offsetof(FiretailMainConfig, FiretailUrl), NULL}, + {// Name of the directive + ngx_string("firetail_allow_undefined_routes"), + // 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 + FiretailAllowUndefinedRoutesDirectiveCallback, NGX_HTTP_MAIN_CONF_OFFSET, + offsetof(FiretailMainConfig, FiretailAllowUndefinedRoutes), NULL}, ngx_null_command}; ngx_module_t ngx_firetail_module = {NGX_MODULE_V1, diff --git a/src/nginx_module/firetail_module.h b/src/nginx_module/firetail_module.h index 634722c..ef948cb 100644 --- a/src/nginx_module/firetail_module.h +++ b/src/nginx_module/firetail_module.h @@ -10,18 +10,21 @@ struct ValidateResponseBody_return { char* r1; }; typedef struct ValidateResponseBody_return (*ValidateResponseBody)(char*, int, char*, int, char*, int, char*, int, - void*, int, char*, int, void*, int, int, void*, int); + char*, int, void*, int, char*, int, void*, int, int, + void*, int); struct ValidateRequestBody_return { int r0; char* r1; }; -typedef struct ValidateRequestBody_return (*ValidateRequestBody)(void*, int, void*, int, void*, int, void*, int); +typedef struct ValidateRequestBody_return (*ValidateRequestBody)(void*, 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 FiretailUrl; + ngx_str_t FiretailAllowUndefinedRoutes; } FiretailMainConfig; // The header and body filters of the filter that was added just before ours. diff --git a/src/validator/main.go b/src/validator/main.go index d5aa238..4ac87cb 100644 --- a/src/validator/main.go +++ b/src/validator/main.go @@ -16,6 +16,7 @@ import ( ) import ( "net/http" + "strconv" ) var firetailRequestMiddleware func(next http.Handler) http.Handler @@ -23,11 +24,13 @@ var firetailResponseMiddleware func(next http.Handler) http.Handler //export ValidateRequestBody func ValidateRequestBody( + allowUndefinedRoutes unsafe.Pointer, allowUndefinedRoutesLength 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) { + log.Println("✅ Validating request body...") // Create the middleware if it hasn't already been done if firetailRequestMiddleware == nil { var err error @@ -78,11 +81,35 @@ func ValidateRequestBody( // 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 + // Get the response body from the middleware middlewareResponseBodyBytes, err := io.ReadAll(localResponseWriter.Body) responseCString := C.CString(string(middlewareResponseBodyBytes)) + if err != nil { + return 1, responseCString // return 1 is error by convention + } + + // If the body differs after being passed through the middleware then we'll just infer it doesn't match the spec + if string(middlewareResponseBodyBytes) != string(placeholderResponse) { + // If allowing undefined routes, then we need to check if the response is a 404 from the middleware. If it is, + // we return success. + allowUndefinedRoutesBool, err := strconv.ParseBool(string(C.GoBytes(allowUndefinedRoutes, allowUndefinedRoutesLength))) + if err != nil { + return 1, responseCString // return 1 is error by convention + } + if allowUndefinedRoutesBool { + response_json := map[string]interface{}{} + if err := json.Unmarshal(middlewareResponseBodyBytes, &response_json); err != nil { + return 1, responseCString // return 1 is error by convention + } + if code, ok := response_json["code"]; ok { + if code_float, ok := code.(float64); ok { + if code_float == 404 { + return 0, responseCString // return 0 is success by convention + } + } + } - if err != nil || string(middlewareResponseBodyBytes) != string(placeholderResponse) { + } return 1, responseCString // return 1 is error by convention } @@ -94,6 +121,7 @@ func ValidateResponseBody( urlCharPtr unsafe.Pointer, urlLength C.int, tokenCharPtr unsafe.Pointer, tokenLength C.int, + allowUndefinedRoutes unsafe.Pointer, allowUndefinedRoutesLength C.int, reqBodyCharPtr unsafe.Pointer, reqBodyLength C.int, reqHeadersJsonCharPtr unsafe.Pointer, reqHeadersJsonLength C.int, resBodyCharPtr unsafe.Pointer, resBodyLength C.int, @@ -170,12 +198,35 @@ func ValidateResponseBody( // match the spec middlewareResponseBodyBytes, err := io.ReadAll(localResponseWriter.Body) responseCString := C.CString(string(middlewareResponseBodyBytes)) + if err != nil { + return 1, responseCString // return 1 is error by convention + } - if err != nil || localResponseWriter.Code != int(statusCode) || string(middlewareResponseBodyBytes) != string(resBodySlice) { + // If the body differs after being passed through the middleware then we'll just infer it doesn't match the spec + if string(middlewareResponseBodyBytes) != string(resBodySlice) || localResponseWriter.Code != int(statusCode) { + // If allowing undefined routes, then we need to check if the response is a 404 from the middleware. If it is, + // we return success. + allowUndefinedRoutesBool, err := strconv.ParseBool(string(C.GoBytes(allowUndefinedRoutes, allowUndefinedRoutesLength))) + if err != nil { + return 1, responseCString // return 1 is error by convention + } + if allowUndefinedRoutesBool { + response_json := map[string]interface{}{} + if err := json.Unmarshal(middlewareResponseBodyBytes, &response_json); err != nil { + return 1, responseCString // return 1 is error by convention + } + if code, ok := response_json["code"]; ok { + if code_float, ok := code.(float64); ok { + if code_float == 404 { + return 0, C.CString(string(resBodySlice)) // return 0 is success by convention + } + } + } + } return 1, responseCString // return 1 is error by convention } - return 0, responseCString // return 0 is success by convention + return 0, C.CString(string(resBodySlice)) // return 0 is success by convention } func main() {}