Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/nginx_module/filter_response_body.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
54 changes: 42 additions & 12 deletions src/nginx_module/firetail_module.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions src/nginx_module/firetail_module.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
59 changes: 55 additions & 4 deletions src/validator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,21 @@ import (
)
import (
"net/http"
"strconv"
)

var firetailRequestMiddleware func(next http.Handler) http.Handler
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
Expand Down Expand Up @@ -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
}

Expand All @@ -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,
Expand Down Expand Up @@ -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() {}
Loading