/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ #include "test-lib.h" #include "buffer.h" #include "str.h" #include "str-sanitize.h" #include "istream.h" #include "ostream.h" #include "test-common.h" #include "http-url.h" #include "http-request-parser.h" #include /* * Test: valid requests */ struct http_request_valid_parse_test { const char *request; const char *method; const char *target_raw; struct { enum http_request_target_format format; struct http_url url; } target; unsigned char version_major; unsigned char version_minor; uoff_t content_length; const char *payload; bool connection_close; bool expect_100_continue; enum http_request_parse_flags flags; }; static const struct http_url default_base_url = { .host = { .name = "example.org" }, }; static const struct http_request_valid_parse_test valid_request_parse_tests[] = { { .request = "GET / HTTP/1.1\r\n" "Host: example.com\r\n" "\r\n", .method = "GET", .target_raw = "/", .target = { .format = HTTP_REQUEST_TARGET_FORMAT_ORIGIN, .url = { .host = { .name = "example.com" }, .path = "/", }, }, .version_major = 1, .version_minor = 1, }, { .request = "GET / HTTP/1.1\r\n" "Host: \r\n" "\r\n", .method = "GET", .target_raw = "/", .target = { .format = HTTP_REQUEST_TARGET_FORMAT_ORIGIN, .url = { .host = { .name = "example.org" }, .path = "/", }, }, .version_major = 1, .version_minor = 1, }, { .request = "GET / HTTP/1.0\r\n" "\r\n", .method = "GET", .target_raw = "/", .target = { .format = HTTP_REQUEST_TARGET_FORMAT_ORIGIN, .url = { .host = { .name = "example.org" }, .path = "/", }, }, .version_major = 1, .version_minor = 0, .connection_close = TRUE, }, { .request = "OPTIONS * HTTP/1.1\r\n" "Host: example.com\r\n" "Connection: Keep-Alive\r\n" "\r\n", .method = "OPTIONS", .target_raw = "*", .target = { .format = HTTP_REQUEST_TARGET_FORMAT_ASTERISK, .url = { .host = { .name = "example.com" }, }, }, .version_major = 1, .version_minor = 1, }, { .request = "OPTIONS * HTTP/1.0\r\n" "Connection: Keep-Alive\r\n" "\r\n", .method = "OPTIONS", .target_raw = "*", .target = { .format = HTTP_REQUEST_TARGET_FORMAT_ASTERISK, .url = { .host = { .name = "example.org" }, }, }, .version_major = 1, .version_minor = 0, }, { .request = "CONNECT example.com:443 HTTP/1.2\r\n" "Host: example.com:443\r\n" "\r\n", .method = "CONNECT", .target_raw = "example.com:443", .target = { .format = HTTP_REQUEST_TARGET_FORMAT_AUTHORITY, .url = { .host = { .name = "example.com" }, .port = 443, } }, .version_major = 1, .version_minor = 2, }, { .request = "GET https://www.example.com:443 HTTP/1.1\r\n" "Host: www.example.com:80\r\n" "\r\n", .method = "GET", .target_raw = "https://www.example.com:443", .target = { .format = HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE, .url = { .host = { .name = "www.example.com" }, .port = 443, .have_ssl = TRUE, }, }, .version_major = 1, .version_minor = 1, }, { .request = "POST http://api.example.com:8080/commit?user=dirk HTTP/1.1\r\n" "Host: api.example.com:8080\r\n" "Content-Length: 10\r\n" "\r\n" "Content!\r\n", .method = "POST", .target_raw = "http://api.example.com:8080/commit?user=dirk", .target = { .format = HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE, .url = { .host = { .name = "api.example.com" }, .port = 8080, .path = "/commit", }, }, .version_major = 1, .version_minor = 1, .payload = "Content!\r\n", }, { .request = "GET http://www.example.com/index.php?seq=1 HTTP/1.1\r\n" "Host: www.example.com\r\n" "Connection: close\r\n" "\r\n", .method = "GET", .target_raw = "http://www.example.com/index.php?seq=1", .target = { .format = HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE, .url = { .host = { .name = "www.example.com" }, .path = "/index.php", }, }, .version_major = 1, .version_minor = 1, .connection_close = TRUE, }, { .request = "GET http://www.example.com/index.html HTTP/1.0\r\n" "Host: www.example.com\r\n" "\r\n", .method = "GET", .target_raw = "http://www.example.com/index.html", .target = { .format = HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE, .url = { .host = { .name = "www.example.com" }, .path = "/index.html", }, }, .version_major = 1, .version_minor = 0, .connection_close = TRUE, }, { .request = "GET http://www.example.com/index.html HTTP/1.1\r\n" "Host: www.example.com\r\n" "Expect: 100-continue\r\n" "\r\n", .method = "GET", .target_raw = "http://www.example.com/index.html", .target = { .format = HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE, .url = { .host = { .name = "www.example.com" }, .path = "/index.html", }, }, .version_major = 1, .version_minor = 1, .expect_100_continue = TRUE }, { .request = "GET / HTTP/1.1\r\n" "Date: Mon, 09 Kul 2018 02:24:29 GMT\r\n" "Host: example.com\r\n" "\r\n", .method = "GET", .target_raw = "/", .target = { .format = HTTP_REQUEST_TARGET_FORMAT_ORIGIN, .url = { .host = { .name = "example.com" }, .path = "/", }, }, .version_major = 1, .version_minor = 1, }, { .request = "GET / HTTP/1.1\r\n" "Date: Sun, 07 Oct 2012 19:52:03 GMT\r\n" "Host: example.com\r\n" "Date: Sun, 13 Oct 2013 13:13:13 GMT\r\n" "\r\n", .method = "GET", .target_raw = "/", .target = { .format = HTTP_REQUEST_TARGET_FORMAT_ORIGIN, .url = { .host = { .name = "example.com" }, .path = "/", }, }, .version_major = 1, .version_minor = 1, }, { .request = "GET //index.php HTTP/1.1\r\n" "Date: Sun, 07 Oct 2012 19:52:03 GMT\r\n" "Host: example.com\r\n" "Date: Sun, 13 Oct 2013 13:13:13 GMT\r\n" "\r\n", .method = "GET", .target_raw = "//index.php", .target = { .format = HTTP_REQUEST_TARGET_FORMAT_ORIGIN, .url = { .host = { .name = "example.com" }, .path = "//index.php", }, }, .version_major = 1, .version_minor = 1, }, }; static const unsigned int valid_request_parse_test_count = N_ELEMENTS(valid_request_parse_tests); static const char * _request_target_format(enum http_request_target_format target_format) { switch (target_format) { case HTTP_REQUEST_TARGET_FORMAT_ORIGIN: return "origin"; case HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE: return "absolute"; case HTTP_REQUEST_TARGET_FORMAT_AUTHORITY: return "authority"; case HTTP_REQUEST_TARGET_FORMAT_ASTERISK: return "asterisk"; } return t_strdup_printf("<>", target_format); } static void test_http_request_url_equal(const struct http_url *urlt, const struct http_url *urlp) { if (urlp == NULL) { test_out("request->target.url = (null)", (urlt->host.name == NULL && urlt->port == 0)); return; } if (urlp->host.name == NULL || urlt->host.name == NULL) { test_out(t_strdup_printf("request->target.url->host.name = %s", urlp->host.name), (urlp->host.name == urlt->host.name)); } else { test_out(t_strdup_printf("request->target.url->host.name = %s", urlp->host.name), strcmp(urlp->host.name, urlt->host.name) == 0); } if (urlp->port == 0) { test_out("request->target.url->port = (unspecified)", urlp->port == urlt->port); } else { test_out(t_strdup_printf("request->target.url->port = %u", urlp->port), urlp->port == urlt->port); } test_out(t_strdup_printf("request->target.url->have_ssl = %s", (urlp->have_ssl ? "yes" : "no")), urlp->have_ssl == urlt->have_ssl); if (urlp->path == NULL || urlt->path == NULL) { test_out(t_strdup_printf("request->target.url->path = %s", urlp->path), urlp->path == urlt->path); } else { test_out(t_strdup_printf("request->target.url->path = %s", urlp->path), strcmp(urlp->path, urlt->path) == 0); } } static void test_http_request_equal(const struct http_request_valid_parse_test *test, struct http_request request, const char *payload) { if (request.method == NULL || test->method == NULL) { test_out(t_strdup_printf("request->method = %s", request.method), request.method == test->method); } else { test_out(t_strdup_printf("request->method = %s", request.method), strcmp(request.method, test->method) == 0); } if (request.target_raw == NULL || test->target_raw == NULL) { test_out(t_strdup_printf("request->target_raw = %s", request.target_raw), request.target_raw == test->target_raw); } else { test_out(t_strdup_printf("request->target_raw = %s", request.target_raw), strcmp(request.target_raw, test->target_raw) == 0); } test_http_request_url_equal(&test->target.url, request.target.url); test_out(t_strdup_printf("request->target_format = %s", _request_target_format(request.target.format)), request.target.format == test->target.format); test_out(t_strdup_printf("request->version = %u.%u", request.version_major, request.version_minor), (request.version_major == test->version_major && request.version_minor == test->version_minor)); test_out(t_strdup_printf("request->connection_close = %s", (request.connection_close ? "yes" : "no")), request.connection_close == test->connection_close); test_out(t_strdup_printf("request->expect_100_continue = %s", (request.expect_100_continue ? "yes" : "no")), request.expect_100_continue == test->expect_100_continue); if (payload == NULL || test->payload == NULL) { test_out(t_strdup_printf("request->payload = %s", str_sanitize(payload, 80)), payload == test->payload); } else { test_out(t_strdup_printf("request->payload = %s", str_sanitize(payload, 80)), strcmp(payload, test->payload) == 0); } } static void test_http_request_parse_valid(void) { unsigned int i; buffer_t *payload_buffer = buffer_create_dynamic(default_pool, 1024); for (i = 0; i < valid_request_parse_test_count; i++) T_BEGIN { struct istream *input; struct ostream *output; const struct http_request_valid_parse_test *test; struct http_request_parser *parser; struct http_request request, request_parsed; enum http_request_parse_error error_code; const char *request_text, *payload, *error; unsigned int pos, request_text_len; int ret = 0; test = &valid_request_parse_tests[i]; request_text = test->request; request_text_len = strlen(request_text); input = test_istream_create_data(request_text, request_text_len); parser = http_request_parser_init(input, &default_base_url, NULL, test->flags); test_begin(t_strdup_printf("http request valid [%d]", i)); payload = NULL; for (pos = 0; pos <= request_text_len && ret == 0; pos++) { test_istream_set_size(input, pos); ret = http_request_parse_next(parser, NULL, &request_parsed, &error_code, &error); } test_istream_set_size(input, request_text_len); i_stream_unref(&input); request = request_parsed; while (ret > 0) { if (request.payload != NULL) { buffer_set_used_size(payload_buffer, 0); output = o_stream_create_buffer(payload_buffer); test_out("payload receive", o_stream_send_istream( output, request.payload) == OSTREAM_SEND_ISTREAM_RESULT_FINISHED); o_stream_destroy(&output); payload = str_c(payload_buffer); } else { payload = NULL; } ret = http_request_parse_next( parser, NULL, &request_parsed, &error_code, &error); } test_out_reason("parse success", ret == 0, error); if (ret == 0) { /* verify last request only */ test_http_request_equal(test, request, payload); } test_end(); http_request_parser_deinit(&parser); } T_END; buffer_free(&payload_buffer); } /* * Test: invalid requests */ struct http_request_invalid_parse_test { const char *request; enum http_request_parse_flags flags; enum http_request_parse_error error_code; }; static const struct http_request_invalid_parse_test invalid_request_parse_tests[] = { { .request = "GET: / HTTP/1.1\r\n" "Host: example.com\r\n" "\r\n", .error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST }, { .request = "GET % HTTP/1.1\r\n" "Host: example.com\r\n" "\r\n", .error_code = HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST }, { .request = "GET /frop\" HTTP/1.1\r\n" "Host: example.com\r\n" "\r\n", .error_code = HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST }, { .request = "GET / HTCPCP/1.0\r\n" "Host: example.com\r\n" "\r\n", .error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST }, { .request = "GET / HTTP/1.0.1\r\n" "Host: example.com\r\n" "\r\n", .error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST }, { .request = "GET / HTTP/1.1\r\n" "Host: \"example.com\r\n" "\r\n", .error_code = HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST }, { .request = "GET / HTTP/1.1\r\n" "\r\n", .error_code = HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST }, { .request = "GET / HTTP/1.1\r\n" "Host: www.example.com\r\n" "Transfer-Encoding: gzip\r\n" "\r\n", .error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST }, { .request = "GET / HTTP/1.1\r\n" "Host: www.example.com\r\n" "Expect: payment\r\n" "\r\n", .error_code = HTTP_REQUEST_PARSE_ERROR_EXPECTATION_FAILED }, { .request = "GET / HTTP/1.1\r\n" "Host: www.example.com\r\n" "Transfer-Encoding: cuneiform, chunked\r\n" "\r\n", .error_code = HTTP_REQUEST_PARSE_ERROR_NOT_IMPLEMENTED }, { .request = "GET / HTTP/1.1\r\n" "Date: Mon, 09 Kul 2018 02:24:29 GMT\r\n" "Host: example.com\r\n" "\r\n", .flags = HTTP_REQUEST_PARSE_FLAG_STRICT, .error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST }, { .request = "GET / HTTP/1.1\r\n" "Date: Sun, 07 Oct 2012 19:52:03 GMT\r\n" "Host: example.com\r\n" "Date: Sun, 13 Oct 2013 13:13:13 GMT\r\n" "\r\n", .flags = HTTP_REQUEST_PARSE_FLAG_STRICT, .error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST } // FIXME: test request limits }; static unsigned int invalid_request_parse_test_count = N_ELEMENTS(invalid_request_parse_tests); static const char * _request_parse_error(enum http_request_parse_error error) { switch (error) { case HTTP_REQUEST_PARSE_ERROR_NONE: return "none?!"; case HTTP_REQUEST_PARSE_ERROR_BROKEN_STREAM: return "broken stream"; case HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST: return "broken request"; case HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST: return "bad request"; case HTTP_REQUEST_PARSE_ERROR_NOT_IMPLEMENTED: return "not implemented"; case HTTP_REQUEST_PARSE_ERROR_EXPECTATION_FAILED: return "expectation failed"; case HTTP_REQUEST_PARSE_ERROR_METHOD_TOO_LONG: return "method too long"; case HTTP_REQUEST_PARSE_ERROR_TARGET_TOO_LONG: return "target too long"; case HTTP_REQUEST_PARSE_ERROR_PAYLOAD_TOO_LARGE: return "payload too large"; } return t_strdup_printf("<>", error); } static void test_http_request_parse_invalid(void) { const struct http_request_invalid_parse_test *test; struct http_request_parser *parser; struct http_request request; enum http_request_parse_error error_code; const char *request_text, *error; struct istream *input; int ret; unsigned int i; for (i = 0; i < invalid_request_parse_test_count; i++) T_BEGIN { test = &invalid_request_parse_tests[i]; request_text = test->request; input = i_stream_create_from_data(request_text, strlen(request_text)); parser = http_request_parser_init(input, &default_base_url, NULL, test->flags); i_stream_unref(&input); test_begin(t_strdup_printf("http request invalid [%d]", i)); while ((ret=http_request_parse_next (parser, NULL, &request, &error_code, &error)) > 0); test_out_reason("parse failure", ret < 0, error); if (ret < 0) { const char *error = _request_parse_error(error_code); test_out(t_strdup_printf("parse error code = %s", error), error_code == test->error_code); } test_end(); http_request_parser_deinit(&parser); } T_END; } /* * Bad request tests */ static const unsigned char bad_request_with_nuls[] = "GET / HTTP/1.1\r\n" "Host: example.com\r\n" "User-Agent: text\0client\r\n" "\r\n"; static void test_http_request_parse_bad(void) { struct http_request_parser *parser; struct http_request request; enum http_request_parse_error error_code; const char *header, *error; struct istream *input; int ret; /* parse failure guarantees http_request_header.size equals strlen(http_request_header.value) */ test_begin("http request with NULs (strict)"); input = i_stream_create_from_data(bad_request_with_nuls, sizeof(bad_request_with_nuls)-1); parser = http_request_parser_init(input, &default_base_url, NULL, HTTP_REQUEST_PARSE_FLAG_STRICT); i_stream_unref(&input); while ((ret=http_request_parse_next (parser, NULL, &request, &error_code, &error)) > 0); test_assert(ret < 0); http_request_parser_deinit(&parser); test_end(); /* even when lenient, bad characters like NUL must not be returned */ test_begin("http request with NULs (lenient)"); input = i_stream_create_from_data(bad_request_with_nuls, sizeof(bad_request_with_nuls)-1); parser = http_request_parser_init(input, &default_base_url, NULL, 0); i_stream_unref(&input); ret = http_request_parse_next (parser, NULL, &request, &error_code, &error); test_out("parse success", ret > 0); header = http_request_header_get(&request, "user-agent"); test_out("header present", header != NULL); if (header != NULL) { test_out(t_strdup_printf("header User-Agent: %s", header), strcmp(header, "textclient") == 0); } ret = http_request_parse_next (parser, NULL, &request, &error_code, &error); test_out("parse end", ret == 0); http_request_parser_deinit(&parser); test_end(); } int main(void) { static void (*const test_functions[])(void) = { test_http_request_parse_valid, test_http_request_parse_invalid, test_http_request_parse_bad, NULL }; return test_run(test_functions); }