diff options
author | Matus Fabian <matfabia@cisco.com> | 2024-11-21 17:01:45 +0100 |
---|---|---|
committer | Florin Coras <florin.coras@gmail.com> | 2024-12-10 05:10:41 +0000 |
commit | 82b3cc18260abc8e77375c337cf3f62aafb132ab (patch) | |
tree | 3438ee9175feda4940b40ccd497626919b5d014e | |
parent | c4b4cd5e77cc5f3305c90eecf802c601392b7975 (diff) |
http: connection upgrade mechanism
Handle "Connection" and "Upgrade" headers in http transport layer which
are used to create a tunnel for some other protocol on the same
connection.
Type: improvement
Change-Id: Icf5479f36fbcc7259b157eaad957211be5ea2aae
Signed-off-by: Matus Fabian <matfabia@cisco.com>
-rw-r--r-- | extras/hs-test/http_test.go | 72 | ||||
-rw-r--r-- | extras/hs-test/infra/suite_vpp_proxy.go | 2 | ||||
-rw-r--r-- | src/plugins/hs_apps/http_cli.c | 36 | ||||
-rw-r--r-- | src/plugins/hs_apps/http_client.c | 23 | ||||
-rw-r--r-- | src/plugins/http/http.c | 204 | ||||
-rw-r--r-- | src/plugins/http/http.h | 270 | ||||
-rw-r--r-- | src/plugins/http/http_plugin.rst | 79 |
7 files changed, 480 insertions, 206 deletions
diff --git a/extras/hs-test/http_test.go b/extras/hs-test/http_test.go index 8127fdca0e7..e84e67fe395 100644 --- a/extras/hs-test/http_test.go +++ b/extras/hs-test/http_test.go @@ -36,7 +36,7 @@ func init() { HttpClientErrRespTest, HttpClientPostFormTest, HttpClientGet128kbResponseTest, HttpClientGetResponseBodyTest, HttpClientGetNoResponseBodyTest, HttpClientPostFileTest, HttpClientPostFilePtrTest, HttpUnitTest, HttpRequestLineTest, HttpClientGetTimeout, HttpStaticFileHandlerWrkTest, HttpStaticUrlHandlerWrkTest, HttpConnTimeoutTest, - HttpClientGetRepeat, HttpClientPostRepeat) + HttpClientGetRepeat, HttpClientPostRepeat, HttpIgnoreH2UpgradeTest) RegisterNoTopoSoloTests(HttpStaticPromTest, HttpGetTpsTest, HttpGetTpsInterruptModeTest, PromConcurrentConnectionsTest, PromMemLeakTest, HttpClientPostMemLeakTest, HttpInvalidClientRequestMemLeakTest, HttpPostTpsTest, HttpPostTpsInterruptModeTest, PromConsecutiveConnectionsTest) @@ -1227,12 +1227,12 @@ func HttpContentLengthTest(s *NoTopoSuite) { validatePostInterfaceStats(s, resp) resp, err = TcpSendReceive(serverAddress+":80", - "POST /interface_stats.json HTTP/1.1\r\n Content-Length: 4 \r\n\r\n"+ifName) + "POST /interface_stats.json HTTP/1.1\r\nContent-Length: 4 \r\n\r\n"+ifName) s.AssertNil(err, fmt.Sprint(err)) validatePostInterfaceStats(s, resp) resp, err = TcpSendReceive(serverAddress+":80", - "POST /interface_stats.json HTTP/1.1\r\n\tContent-Length:\t\t4\r\n\r\n"+ifName) + "POST /interface_stats.json HTTP/1.1\r\nContent-Length:\t\t4\r\n\r\n"+ifName) s.AssertNil(err, fmt.Sprint(err)) validatePostInterfaceStats(s, resp) } @@ -1289,13 +1289,42 @@ func HttpHeadersTest(s *NoTopoSuite) { serverAddress := s.VppAddr() vpp.Vppctl("http cli server") - resp, err := TcpSendReceive( - serverAddress+":80", - "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\r\nUser-Agent:test\r\nAccept:text/xml\r\nAccept:\ttext/plain\t \r\nAccept:text/html\r\n\r\n") + transport := http.DefaultTransport + transport.(*http.Transport).Proxy = nil + transport.(*http.Transport).DisableKeepAlives = false + client := &http.Client{ + Transport: transport, + Timeout: time.Second * 30, + } + + req, err := http.NewRequest("GET", "http://"+serverAddress+":80/show/version", nil) s.AssertNil(err, fmt.Sprint(err)) - s.AssertContains(resp, "HTTP/1.1 200 OK") - s.AssertContains(resp, "Content-Type: text/plain") - s.AssertNotContains(resp, "<html>", "html content received instead of plain text") + req.Header.Add("Accept", "text/xml") + req.Header.Add("Accept-Language", "*") + req.Header.Add("Accept", "text/plain") + req.Header.Add("Accept", "text/html") + resp, err := client.Do(req) + s.AssertNil(err, fmt.Sprint(err)) + defer resp.Body.Close() + s.Log(DumpHttpResp(resp, true)) + s.AssertHttpStatus(resp, 200) + s.AssertHttpHeaderWithValue(resp, "Content-Type", "text/plain") + data, err := io.ReadAll(resp.Body) + s.AssertNil(err, fmt.Sprint(err)) + s.AssertNotContains(string(data), "<html>", "html content received instead of plain text") + + req2, err := http.NewRequest("GET", "http://"+serverAddress+":80/show/version", nil) + s.AssertNil(err, fmt.Sprint(err)) + req2.Header.Add("Accept", "text/html") + resp2, err := client.Do(req2) + s.AssertNil(err, fmt.Sprint(err)) + defer resp2.Body.Close() + s.Log(DumpHttpResp(resp2, true)) + s.AssertHttpStatus(resp2, 200) + s.AssertHttpHeaderWithValue(resp2, "Content-Type", "text/html") + data2, err := io.ReadAll(resp2.Body) + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(string(data2), "<html>", "html content not received") } func HttpInvalidHeadersTest(s *NoTopoSuite) { @@ -1382,3 +1411,28 @@ func HttpConnTimeoutTest(s *NoTopoSuite) { _, err = conn.Read(reply) s.AssertMatchError(err, io.EOF, "connection not closed by server") } + +func HttpIgnoreH2UpgradeTest(s *NoTopoSuite) { + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.VppAddr() + s.Log(vpp.Vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers")) + + transport := http.DefaultTransport + transport.(*http.Transport).Proxy = nil + transport.(*http.Transport).DisableKeepAlives = false + client := &http.Client{ + Transport: transport, + Timeout: time.Second * 30, + } + + req, err := http.NewRequest("GET", "http://"+serverAddress+":80/version.json", nil) + s.AssertNil(err, fmt.Sprint(err)) + req.Header.Add("Connection", "Upgrade") + req.Header.Add("Upgrade", "HTTP/2.0") + resp, err := client.Do(req) + s.AssertNil(err, fmt.Sprint(err)) + defer resp.Body.Close() + s.Log(DumpHttpResp(resp, true)) + s.AssertHttpStatus(resp, 200) + s.AssertHttpHeaderNotPresent(resp, "Upgrade") +} diff --git a/extras/hs-test/infra/suite_vpp_proxy.go b/extras/hs-test/infra/suite_vpp_proxy.go index d696109b31b..57308dfe076 100644 --- a/extras/hs-test/infra/suite_vpp_proxy.go +++ b/extras/hs-test/infra/suite_vpp_proxy.go @@ -150,6 +150,7 @@ func (s *VppProxySuite) CurlDownloadResourceViaTunnel(uri string, proxyUri strin s.AssertContains(writeOut, "GET response code: 200") s.AssertNotContains(log, "bytes remaining to read") s.AssertNotContains(log, "Operation timed out") + s.AssertNotContains(log, "Upgrade:") } func (s *VppProxySuite) CurlUploadResourceViaTunnel(uri, proxyUri, file string) { @@ -158,6 +159,7 @@ func (s *VppProxySuite) CurlUploadResourceViaTunnel(uri, proxyUri, file string) s.AssertContains(writeOut, "CONNECT response code: 200") s.AssertContains(writeOut, "PUT response code: 201") s.AssertNotContains(log, "Operation timed out") + s.AssertNotContains(log, "Upgrade:") } var _ = Describe("VppProxySuite", Ordered, ContinueOnFailure, func() { diff --git a/src/plugins/hs_apps/http_cli.c b/src/plugins/hs_apps/http_cli.c index dfa90f9eced..3ca86d24673 100644 --- a/src/plugins/hs_apps/http_cli.c +++ b/src/plugins/hs_apps/http_cli.c @@ -51,6 +51,7 @@ typedef struct u8 *tx_buf; u32 tx_offset; u32 vpp_session_index; + http_header_table_t req_headers; http_header_t *resp_headers; } hcs_session_t; @@ -177,7 +178,7 @@ start_send_data (hcs_session_t *hs, http_status_code_t status) if (vec_len (hs->resp_headers)) { headers_buf = http_serialize_headers (hs->resp_headers); - vec_free (hs->resp_headers); + vec_reset_length (hs->resp_headers); msg.data.headers_offset = 0; msg.data.headers_len = vec_len (headers_buf); } @@ -370,7 +371,8 @@ hcs_ts_rx_callback (session_t *ts) hs = hcs_session_get (ts->thread_index, ts->opaque); hs->tx_buf = 0; - hs->resp_headers = 0; + vec_reset_length (hs->resp_headers); + http_reset_header_table (&hs->req_headers); /* Read the http message header */ rv = svm_fifo_dequeue (ts->rx_fifo, sizeof (msg), (u8 *) &msg); @@ -413,30 +415,22 @@ hcs_ts_rx_callback (session_t *ts) if (msg.data.headers_len) { - u8 *headers = 0; - http_header_table_t *ht; - vec_validate (headers, msg.data.headers_len - 1); + http_init_header_table_buf (&hs->req_headers, msg); rv = svm_fifo_peek (ts->rx_fifo, msg.data.headers_offset, - msg.data.headers_len, headers); + msg.data.headers_len, hs->req_headers.buf); ASSERT (rv == msg.data.headers_len); - if (http_parse_headers (headers, &ht)) + http_build_header_table (&hs->req_headers, msg); + const http_header_t *accept = http_get_header ( + &hs->req_headers, http_header_name_token (HTTP_HEADER_ACCEPT)); + if (accept) { - start_send_data (hs, HTTP_STATUS_BAD_REQUEST); - vec_free (args.buf); - vec_free (headers); - goto done; - } - const char *accept_value = - http_get_header (ht, http_header_name_str (HTTP_HEADER_ACCEPT)); - if (accept_value) - { - HCS_DBG ("client accept: %s", accept_value); + HCS_DBG ("client accept: %U", format_http_bytes, accept->value.base, + accept->value.len); /* just for testing purpose, we don't care about precedence */ - if (strstr (accept_value, "text/plain")) + if (http_token_contains (accept->value.base, accept->value.len, + http_token_lit ("text/plain"))) args.plain_text = 1; } - http_free_header_table (ht); - vec_free (headers); } args.hs_index = hs->session_index; @@ -547,6 +541,8 @@ hcs_ts_cleanup_callback (session_t *s, session_cleanup_ntf_t ntf) return; vec_free (hs->tx_buf); + vec_free (hs->resp_headers); + http_free_header_table (&hs->req_headers); hcs_session_free (hs); } diff --git a/src/plugins/hs_apps/http_client.c b/src/plugins/hs_apps/http_client.c index a86bf1f3db3..cfa5e9c1001 100644 --- a/src/plugins/hs_apps/http_client.c +++ b/src/plugins/hs_apps/http_client.c @@ -40,6 +40,12 @@ typedef struct typedef struct { + u8 *name; + u8 *value; +} hc_http_header_t; + +typedef struct +{ u32 app_index; u32 cli_node_index; u8 attached; @@ -52,7 +58,7 @@ typedef struct u8 *resp_headers; u8 *http_response; u8 *response_status; - http_header_ht_t *custom_header; + hc_http_header_t *custom_header; u8 is_file; u8 use_ptr; u8 *filename; @@ -181,7 +187,7 @@ hc_session_connected_callback (u32 app_index, u32 hc_session_index, hc_main_t *hcm = &hc_main; hc_worker_t *wrk; u32 new_hc_index; - http_header_ht_t *header; + hc_http_header_t *header; HTTP_DBG (1, "ho hc_index: %d", hc_session_index); if (err) @@ -344,7 +350,6 @@ hc_rx_callback (session_t *s) format (0, "%U", format_http_status_code, msg.code); svm_fifo_dequeue_drop (s->rx_fifo, msg.data.headers_offset); - http_header_table_t *ht; vec_validate (hcm->resp_headers, msg.data.headers_len - 1); vec_set_len (hcm->resp_headers, msg.data.headers_len); rv = svm_fifo_dequeue (s->rx_fifo, msg.data.headers_len, @@ -352,14 +357,6 @@ hc_rx_callback (session_t *s) ASSERT (rv == msg.data.headers_len); HTTP_DBG (1, (char *) format (0, "%v", hcm->resp_headers)); - if (http_parse_headers (hcm->resp_headers, &ht)) - { - clib_warning ("invalid headers received"); - vlib_process_signal_event_mt ( - wrk->vlib_main, hcm->cli_node_index, HC_GENERIC_ERR, 0); - return -1; - } - http_free_header_table (ht); msg.data.body_offset -= msg.data.headers_len + msg.data.headers_offset; } @@ -666,7 +663,7 @@ hc_cleanup () HTTP_DBG (1, "cleanup"); hc_main_t *hcm = &hc_main; hc_worker_t *wrk; - http_header_ht_t *header; + hc_http_header_t *header; vec_foreach (wrk, hcm->wrk) hcc_worker_cleanup (wrk); @@ -696,7 +693,7 @@ hc_command_fn (vlib_main_t *vm, unformat_input_t *input, unformat_input_t _line_input, *line_input = &_line_input; u8 *path = 0; u8 *file_data; - http_header_ht_t new_header; + hc_http_header_t new_header; u8 *name; u8 *value; int rv; diff --git a/src/plugins/http/http.c b/src/plugins/http/http.c index 7c0b55e26d2..c5cd894cb59 100644 --- a/src/plugins/http/http.c +++ b/src/plugins/http/http.c @@ -17,6 +17,7 @@ #include <vnet/session/session.h> #include <http/http_timer.h> #include <http/http_status_codes.h> +#include <http/http_header_names.h> static http_main_t http_main; @@ -35,6 +36,12 @@ const http_buffer_type_t msg_to_buf_type[] = { [HTTP_MSG_DATA_PTR] = HTTP_BUFFER_PTR, }; +const char *http_upgrade_proto_str[] = { "", +#define _(sym, str) str, + foreach_http_upgrade_proto +#undef _ +}; + static u8 * format_http_req_state (u8 *s, va_list *va) { @@ -446,6 +453,9 @@ static const char *http_response_template = "HTTP/1.1 %s\r\n" static const char *content_len_template = "Content-Length: %llu\r\n"; +static const char *connection_upgrade_template = "Connection: upgrade\r\n" + "Upgrade: %s\r\n"; + /** * http request boilerplate */ @@ -706,6 +716,7 @@ http_parse_request_line (http_req_t *req, http_status_code_t *ec) { HTTP_DBG (0, "CONNECT method"); req->method = HTTP_REQ_CONNECT; + req->upgrade_proto = HTTP_UPGRADE_PROTO_NA; req->target_path_offset = method_offset + 8; req->is_tunnel = 1; } @@ -892,7 +903,17 @@ http_parse_status_line (http_req_t *req) static int http_identify_headers (http_req_t *req, http_status_code_t *ec) { - int i; + int rv; + u8 *p, *end, *name_start, *value_start; + u32 name_len, value_len; + http_field_line_t *field_line; + uword header_index; + + vec_reset_length (req->headers); + req->content_len_header_index = ~0; + req->connection_header_index = ~0; + req->upgrade_header_index = ~0; + req->headers_offset = req->rx_buf_offset; /* check if we have any header */ if ((req->rx_buf[req->rx_buf_offset] == '\r') && @@ -905,16 +926,53 @@ http_identify_headers (http_req_t *req, http_status_code_t *ec) return 0; } - /* find empty line indicating end of header section */ - i = v_find_index (req->rx_buf, req->rx_buf_offset, 0, "\r\n\r\n"); - if (i < 0) + end = req->rx_buf + vec_len (req->rx_buf); + p = req->rx_buf + req->rx_buf_offset; + + while (1) { - clib_warning ("cannot find header section end"); - *ec = HTTP_STATUS_BAD_REQUEST; - return -1; - } - req->headers_offset = req->rx_buf_offset; - req->headers_len = i - req->rx_buf_offset + 2; + rv = _parse_field_name (&p, end, &name_start, &name_len); + if (rv != 0) + { + *ec = HTTP_STATUS_BAD_REQUEST; + return -1; + } + rv = _parse_field_value (&p, end, &value_start, &value_len); + if (rv != 0 || (end - p) < 2) + { + *ec = HTTP_STATUS_BAD_REQUEST; + return -1; + } + + vec_add2 (req->headers, field_line, 1); + field_line->name_offset = + (name_start - req->rx_buf) - req->headers_offset; + field_line->name_len = name_len; + field_line->value_offset = + (value_start - req->rx_buf) - req->headers_offset; + field_line->value_len = value_len; + header_index = field_line - req->headers; + + /* find headers that will be used later in preprocessing */ + if (req->content_len_header_index == ~0 && + http_token_is ((const char *) name_start, name_len, + http_header_name_token (HTTP_HEADER_CONTENT_LENGTH))) + req->content_len_header_index = header_index; + else if (req->connection_header_index == ~0 && + http_token_is ((const char *) name_start, name_len, + http_header_name_token (HTTP_HEADER_CONNECTION))) + req->connection_header_index = header_index; + else if (req->upgrade_header_index == ~0 && + http_token_is ((const char *) name_start, name_len, + http_header_name_token (HTTP_HEADER_UPGRADE))) + req->upgrade_header_index = header_index; + + /* are we done? */ + if (*p == '\r' && *(p + 1) == '\n') + break; + } + + req->headers_len = p - (req->rx_buf + req->headers_offset); req->control_data_len += (req->headers_len + 2); HTTP_DBG (2, "headers length: %u", req->headers_len); HTTP_DBG (2, "headers offset: %u", req->headers_offset); @@ -925,9 +983,10 @@ http_identify_headers (http_req_t *req, http_status_code_t *ec) static int http_identify_message_body (http_req_t *req, http_status_code_t *ec) { - int i, value_len; - u8 *end, *p, *value_start; + int i; + u8 *p; u64 body_len = 0, digit; + http_field_line_t *field_line; req->body_len = 0; @@ -944,67 +1003,15 @@ http_identify_message_body (http_req_t *req, http_status_code_t *ec) /* TODO check for chunked transfer coding */ - /* try to find Content-Length header */ - i = v_find_index (req->rx_buf, req->headers_offset, req->headers_len, - "Content-Length:"); - if (i < 0) + if (req->content_len_header_index == ~0) { HTTP_DBG (2, "Content-Length header not present, no message-body"); return 0; } - req->rx_buf_offset = i + 15; + field_line = vec_elt_at_index (req->headers, req->content_len_header_index); - i = v_find_index (req->rx_buf, req->rx_buf_offset, req->headers_len, "\r\n"); - if (i < 0) - { - clib_warning ("end of line missing"); - *ec = HTTP_STATUS_BAD_REQUEST; - return -1; - } - value_len = i - req->rx_buf_offset; - if (value_len < 1) - { - clib_warning ("invalid header, content length value missing"); - *ec = HTTP_STATUS_BAD_REQUEST; - return -1; - } - - end = req->rx_buf + req->rx_buf_offset + value_len; - p = req->rx_buf + req->rx_buf_offset; - /* skip leading whitespace */ - while (1) - { - if (p == end) - { - clib_warning ("value not found"); - *ec = HTTP_STATUS_BAD_REQUEST; - return -1; - } - else if (*p != ' ' && *p != '\t') - { - break; - } - p++; - value_len--; - } - value_start = p; - /* skip trailing whitespace */ - p = value_start + value_len - 1; - while (*p == ' ' || *p == '\t') - { - p--; - value_len--; - } - - if (value_len < 1) - { - clib_warning ("value not found"); - *ec = HTTP_STATUS_BAD_REQUEST; - return -1; - } - - p = value_start; - for (i = 0; i < value_len; i++) + p = req->rx_buf + req->headers_offset + field_line->value_offset; + for (i = 0; i < field_line->value_len; i++) { /* check for digit */ if (!isdigit (*p)) @@ -1095,6 +1102,7 @@ http_req_state_wait_transport_reply (http_conn_t *hc, msg.data.body_len = hc->req.body_len; msg.data.type = HTTP_MSG_DATA_INLINE; msg.data.len = len; + msg.data.headers_ctx = pointer_to_uword (hc->req.headers); svm_fifo_seg_t segs[2] = { { (u8 *) &msg, sizeof (msg) }, { hc->req.rx_buf, len } }; @@ -1130,6 +1138,49 @@ error: return HTTP_SM_ERROR; } +#define http_field_line_value_token(_fl, _req) \ + (const char *) ((_req)->rx_buf + (_req)->headers_offset + \ + (_fl)->value_offset), \ + (_fl)->value_len + +static void +http_check_connection_upgrade (http_req_t *req) +{ + http_field_line_t *connection, *upgrade; + u8 skip; + + skip = (req->method != HTTP_REQ_GET) + (req->connection_header_index == ~0) + + (req->upgrade_header_index == ~0); + if (skip) + return; + + connection = vec_elt_at_index (req->headers, req->connection_header_index); + /* connection options are case-insensitive (RFC9110 7.6.1) */ + if (http_token_is_case (http_field_line_value_token (connection, req), + http_token_lit ("upgrade"))) + { + upgrade = vec_elt_at_index (req->headers, req->upgrade_header_index); + + /* check upgrade protocol, we want to ignore something like upgrade to + * newer HTTP version, only tunnels are supported */ + if (0) + ; +#define _(sym, str) \ + else if (http_token_is (http_field_line_value_token (upgrade, req), \ + http_token_lit (str))) req->upgrade_proto = \ + HTTP_UPGRADE_PROTO_##sym; + foreach_http_upgrade_proto +#undef _ + else return; + + HTTP_DBG (1, "connection upgrade: %U", format_http_bytes, + req->rx_buf + req->headers_offset + upgrade->value_offset, + upgrade->value_len); + req->is_tunnel = 1; + req->method = HTTP_REQ_CONNECT; + } +} + static http_sm_result_t http_req_state_wait_transport_method (http_conn_t *hc, transport_send_params_t *sp) @@ -1164,6 +1215,8 @@ http_req_state_wait_transport_method (http_conn_t *hc, if (rv) goto error; + http_check_connection_upgrade (&hc->req); + rv = http_identify_message_body (&hc->req, &ec); if (rv) goto error; @@ -1196,6 +1249,8 @@ http_req_state_wait_transport_method (http_conn_t *hc, msg.data.headers_len = hc->req.headers_len; msg.data.body_offset = hc->req.body_offset; msg.data.body_len = hc->req.body_len; + msg.data.headers_ctx = pointer_to_uword (hc->req.headers); + msg.data.upgrade_proto = hc->req.upgrade_proto; svm_fifo_seg_t segs[2] = { { (u8 *) &msg, sizeof (msg) }, { hc->req.rx_buf, len } }; @@ -1286,15 +1341,21 @@ http_req_state_wait_app_reply (http_conn_t *hc, transport_send_params_t *sp) /* Server */ hc->app_name); - /* RFC9110 9.3.6: A server MUST NOT send Content-Length header field in a - * 2xx (Successful) response to CONNECT. */ - if (hc->req.is_tunnel && http_status_code_str[msg.code][0] == '2') + /* RFC9110 8.6: A server MUST NOT send Content-Length header field in a + * 2xx (Successful) response to CONNECT or with a status code of 101 + * (Switching Protocols). */ + if (hc->req.is_tunnel && (http_status_code_str[msg.code][0] == '2' || + msg.code == HTTP_STATUS_SWITCHING_PROTOCOLS)) { ASSERT (msg.data.body_len == 0); next_state = HTTP_REQ_STATE_TUNNEL; + if (hc->req.upgrade_proto > HTTP_UPGRADE_PROTO_NA) + response = format (response, connection_upgrade_template, + http_upgrade_proto_str[hc->req.upgrade_proto]); /* cleanup some stuff we don't need anymore in tunnel mode */ http_conn_timer_stop (hc); vec_free (hc->req.rx_buf); + vec_free (hc->req.headers); http_buffer_free (&hc->req.tx_buf); } else @@ -1860,6 +1921,7 @@ http_ts_cleanup_callback (session_t *ts, session_cleanup_ntf_t ntf) HTTP_DBG (1, "going to free hc [%u]%x", ts->thread_index, ts->opaque); vec_free (hc->req.rx_buf); + vec_free (hc->req.headers); http_buffer_free (&hc->req.tx_buf); diff --git a/src/plugins/http/http.h b/src/plugins/http/http.h index f54b4a040a2..bcee69b7bb4 100644 --- a/src/plugins/http/http.h +++ b/src/plugins/http/http.h @@ -352,12 +352,36 @@ typedef enum http_header_name_ #undef _ } http_header_name_t; +#define HTTP_BOOLEAN_TRUE "?1" + +#define foreach_http_upgrade_proto \ + _ (CONNECT_UDP, "connect-udp") \ + _ (CONNECT_IP, "connect-ip") \ + _ (WEBSOCKET, "websocket") + +typedef enum http_upgrade_proto_ +{ + HTTP_UPGRADE_PROTO_NA = + 0, /* indicating standard CONNECT where protocol is omitted */ +#define _(sym, str) HTTP_UPGRADE_PROTO_##sym, + foreach_http_upgrade_proto +#undef _ +} http_upgrade_proto_t; + typedef enum http_msg_data_type_ { HTTP_MSG_DATA_INLINE, HTTP_MSG_DATA_PTR } http_msg_data_type_t; +typedef struct http_field_line_ +{ + u32 name_offset; + u32 name_len; + u32 value_offset; + u32 value_len; +} http_field_line_t; + typedef struct http_msg_data_ { http_msg_data_type_t type; @@ -371,6 +395,8 @@ typedef struct http_msg_data_ u32 headers_len; u32 body_offset; u64 body_len; + uword headers_ctx; + http_upgrade_proto_t upgrade_proto; u8 data[0]; } http_msg_data_t; @@ -422,6 +448,13 @@ typedef struct http_req_ u32 body_offset; u64 body_len; + + http_field_line_t *headers; + uword content_len_header_index; + uword connection_header_index; + uword upgrade_header_index; + + http_upgrade_proto_t upgrade_proto; } http_req_t; typedef struct http_tc_ @@ -477,6 +510,27 @@ typedef struct http_main_ u32 fifo_size; } http_main_t; +always_inline u8 * +format_http_bytes (u8 *s, va_list *va) +{ + u8 *bytes = va_arg (*va, u8 *); + int n_bytes = va_arg (*va, int); + uword i; + + if (n_bytes == 0) + return s; + + for (i = 0; i < n_bytes; i++) + { + if (isprint (bytes[i])) + s = format (s, "%c", bytes[i]); + else + s = format (s, "\\x%02x", bytes[i]); + } + + return s; +} + always_inline int _validate_target_syntax (u8 *target, u32 len, int is_query, int *is_encoded) { @@ -767,22 +821,94 @@ _parse_field_value (u8 **pos, u8 *end, u8 **field_value_start, typedef struct { - u8 *name; - u8 *value; -} http_header_ht_t; - -typedef struct -{ http_token_t name; http_token_t value; } http_header_t; typedef struct { - http_header_ht_t *headers; + http_header_t *headers; uword *value_by_name; + u8 *buf; + char **concatenated_values; } http_header_table_t; +#define HTTP_HEADER_TABLE_NULL \ + { \ + .headers = 0, .value_by_name = 0, .buf = 0, .concatenated_values = 0, \ + } + +always_inline u8 +http_token_is (const char *actual, uword actual_len, const char *expected, + uword expected_len) +{ + ASSERT (actual != 0); + if (actual_len != expected_len) + return 0; + return memcmp (actual, expected, expected_len) == 0 ? 1 : 0; +} + +always_inline u8 +http_token_is_case (const char *actual, uword actual_len, const char *expected, + uword expected_len) +{ + uword i; + ASSERT (actual != 0); + if (actual_len != expected_len) + return 0; + for (i = 0; i < expected_len; i++) + { + if (tolower (actual[i]) != expected[i]) + return 0; + } + return 1; +} + +always_inline u8 +http_token_contains (const char *haystack, uword haystack_len, + const char *needle, uword needle_len) +{ + uword end_index, i; + ASSERT (haystack != 0); + if (haystack_len < needle_len) + return 0; + end_index = haystack_len - needle_len; + for (i = 0; i <= end_index; i++) + { + if (!memcmp (haystack + i, needle, needle_len)) + return 1; + } + return 0; +} + +/** + * Reset header table before reuse. + * + * @param ht Header table to reset. + */ +always_inline void +http_reset_header_table (http_header_table_t *ht) +{ + int i; + for (i = 0; i < vec_len (ht->concatenated_values); i++) + vec_free (ht->concatenated_values[i]); + vec_reset_length (ht->concatenated_values); + vec_reset_length (ht->headers); + vec_reset_length (ht->buf); + hash_free (ht->value_by_name); +} + +/** + * Initialize header table input buffer. + * @param ht Header table. + * @param msg HTTP transport message metadata. + */ +always_inline void +http_init_header_table_buf (http_header_table_t *ht, http_msg_t msg) +{ + vec_validate (ht->buf, msg.data.headers_len - 1); +} + /** * Free header table's memory. * @@ -791,86 +917,88 @@ typedef struct always_inline void http_free_header_table (http_header_table_t *ht) { - http_header_ht_t *header; - vec_foreach (header, ht->headers) - { - vec_free (header->name); - vec_free (header->value); - } + int i; + for (i = 0; i < vec_len (ht->concatenated_values); i++) + vec_free (ht->concatenated_values[i]); + vec_free (ht->concatenated_values); vec_free (ht->headers); + vec_free (ht->buf); hash_free (ht->value_by_name); clib_mem_free (ht); } +static uword +_http_ht_hash_key_sum (hash_t *h, uword key) +{ + http_token_t *name = uword_to_pointer (key, http_token_t *); + return hash_memory (name->base, name->len, 0); +} + +static uword +_http_ht_hash_key_equal (hash_t *h, uword key1, uword key2) +{ + http_token_t *name1 = uword_to_pointer (key1, http_token_t *); + http_token_t *name2 = uword_to_pointer (key2, http_token_t *); + return name1 && name2 && + http_token_is (name1->base, name1->len, name2->base, name2->len); +} + /** - * Parse headers in given vector. + * Build header table. * - * @param headers Vector to parse. - * @param [out] header_table Parsed headers in case of success. - * - * @return @c 0 on success. + * @param header_table Header table with loaded buffer. + * @param msg HTTP transport message metadata. * - * The caller is responsible to free the returned @c header_table - * using @c http_free_header_table . + * @note If reusing already allocated header table use + * @c http_reset_header_table first. */ -always_inline int -http_parse_headers (u8 *headers, http_header_table_t **header_table) +always_inline void +http_build_header_table (http_header_table_t *ht, http_msg_t msg) { - u8 *pos, *end, *name_start, *value_start, *name; - u32 name_len, value_len; - int rv; - http_header_ht_t *header; - http_header_table_t *ht; + http_token_t name; + http_header_t *header; + http_field_line_t *field_lines, *field_line; uword *p; - end = headers + vec_len (headers); - pos = headers; + ASSERT (ht); + field_lines = uword_to_pointer (msg.data.headers_ctx, http_field_line_t *); + ht->value_by_name = + hash_create2 (0, 0, sizeof (uword), _http_ht_hash_key_sum, + _http_ht_hash_key_equal, 0, 0); - ht = clib_mem_alloc (sizeof (*ht)); - ht->value_by_name = hash_create_string (0, sizeof (uword)); - ht->headers = 0; - do + vec_foreach (field_line, field_lines) { - rv = _parse_field_name (&pos, end, &name_start, &name_len); - if (rv != 0) - { - http_free_header_table (ht); - return rv; - } - rv = _parse_field_value (&pos, end, &value_start, &value_len); - if (rv != 0) - { - http_free_header_table (ht); - return rv; - } - name = vec_new (u8, name_len); - clib_memcpy (name, name_start, name_len); - vec_terminate_c_string (name); + name.base = (char *) (ht->buf + field_line->name_offset); + name.len = field_line->name_len; /* check if header is repeated */ - p = hash_get_mem (ht->value_by_name, name); + p = hash_get_mem (ht->value_by_name, &name); if (p) { - /* if yes combine values */ + char *new_value = 0; header = vec_elt_at_index (ht->headers, p[0]); - vec_pop (header->value); /* drop null byte */ - header->value = format (header->value, ", %U%c", format_ascii_bytes, - value_start, value_len, 0); - vec_free (name); + u32 new_len = header->value.len + field_line->value_len + 2; + vec_validate (new_value, new_len - 1); + clib_memcpy (new_value, header->value.base, header->value.len); + new_value[header->value.len] = ','; + new_value[header->value.len + 1] = ' '; + clib_memcpy (new_value + header->value.len + 2, + ht->buf + field_line->value_offset, + field_line->value_len); + vec_add1 (ht->concatenated_values, new_value); + header->value.base = new_value; + header->value.len = new_len; continue; } /* or create new record */ - vec_add2 (ht->headers, header, sizeof (*header)); - header->name = name; - header->value = vec_new (u8, value_len); - clib_memcpy (header->value, value_start, value_len); - vec_terminate_c_string (header->value); - hash_set_mem (ht->value_by_name, header->name, header - ht->headers); + vec_add2 (ht->headers, header, 1); + header->name.base = name.base; + header->name.len = name.len; + header->value.base = (char *) (ht->buf + field_line->value_offset); + header->value.len = field_line->value_len; + HTTP_DBG (1, "value: %U", format_http_bytes, header->value.base, + header->value.len); + hash_set_mem (ht->value_by_name, &header->name, header - ht->headers); } - while (pos != end); - - *header_table = ht; - - return 0; } /** @@ -881,17 +1009,19 @@ http_parse_headers (u8 *headers, http_header_table_t **header_table) * * @return Header's value in case of success, @c 0 otherwise. */ -always_inline const char * -http_get_header (http_header_table_t *header_table, const char *name) +always_inline const http_header_t * +http_get_header (http_header_table_t *header_table, const char *name, + uword name_len) { uword *p; - http_header_ht_t *header; + http_header_t *header; + http_token_t name_token = { (char *) name, name_len }; - p = hash_get_mem (header_table->value_by_name, name); + p = hash_get_mem (header_table->value_by_name, &name_token); if (p) { header = vec_elt_at_index (header_table->headers, p[0]); - return (const char *) header->value; + return header; } return 0; diff --git a/src/plugins/http/http_plugin.rst b/src/plugins/http/http_plugin.rst index f86c796bd83..bf414cf96ef 100644 --- a/src/plugins/http/http_plugin.rst +++ b/src/plugins/http/http_plugin.rst @@ -15,9 +15,11 @@ Usage ----- The plugin exposes following inline functions: ``http_validate_abs_path_syntax``, ``http_validate_query_syntax``, -``http_percent_decode``, ``http_path_remove_dot_segments``, ``http_parse_headers``, ``http_get_header``, -``http_free_header_table``, ``http_add_header``, ``http_serialize_headers``, ``http_parse_authority_form_target``, -``http_serialize_authority_form_target``, ``http_parse_absolute_form``, ``http_parse_masque_host_port``. +``http_percent_decode``, ``http_path_remove_dot_segments``, ``http_build_header_table``, ``http_get_header``, +``http_reset_header_table``, ``http_free_header_table``, ``http_add_header``, +``http_serialize_headers``, ``http_parse_authority_form_target``, ``http_serialize_authority_form_target``, +``http_parse_absolute_form``, ``http_parse_masque_host_port``, ``http_decap_udp_payload_datagram``, +``http_encap_udp_payload_datagram``. ``http_token_is``, ``http_token_is_case``, ``http_token_contains`` It relies on the hoststack constructs and uses ``http_msg_data_t`` data structure for passing metadata to/from applications. @@ -125,24 +127,60 @@ Following example shows how to parse headers: #include <http/http_header_names.h> if (msg.data.headers_len) { - u8 *headers = 0; - http_header_table_t *ht; - vec_validate (headers, msg.data.headers_len - 1); + http_header_table_t ht = HTTP_HEADER_TABLE_NULL; + http_init_header_table_buf (&ht, msg); rv = svm_fifo_peek (ts->rx_fifo, msg.data.headers_offset, - msg.data.headers_len, headers); + msg.data.headers_len, ht.buf); ASSERT (rv == msg.data.headers_len); - if (http_parse_headers (headers, &ht)) + http_build_header_table (&ht, msg); + /* get Accept header */ + const http_header_t *accept = http_get_header (&ht, http_header_name_token (HTTP_HEADER_ACCEPT)); + if (accept_value) { - /* your error handling */ + /* do something interesting */ } + http_free_header_table (&ht); + } + +Allocated header table memory can be reused, you just need to reset it using ``http_reset_header_table`` before reuse. +We will add following member to our session context structure: + +.. code-block:: C + + typedef struct + { + /* ... */ + http_header_table_t ht; + } session_ctx_t; + +Don't forget to zero allocated session context. + +And in ``session_cleanup_callback`` we free header table memory: + +.. code-block:: C + + http_free_header_table (&ctx->ht); + +Modified example above: + +.. code-block:: C + + #include <http/http_header_names.h> + http_reset_header_table (&ctx->ht); + /* ... */ + if (msg.data.headers_len) + { + http_init_header_table_buf (&ctx->ht, msg); + rv = svm_fifo_peek (ts->rx_fifo, msg.data.headers_offset, + msg.data.headers_len, ctx->ht.buf); + ASSERT (rv == msg.data.headers_len); + http_build_header_table (&ctx->ht, msg); /* get Accept header */ - const char *accept_value = http_get_header (ht, http_header_name_str (HTTP_HEADER_ACCEPT)); + const http_header_t *accept = http_get_header (&ctx->ht, http_header_name_token (HTTP_HEADER_ACCEPT)); if (accept_value) { /* do something interesting */ } - http_free_header_table (ht); - vec_free (headers); } Finally application reads body (if any), which might be received in multiple pieces (depends on size), so we might need some state machine in ``builtin_app_rx_callback``. @@ -437,24 +475,19 @@ Following example shows how to parse headers: #include <http/http_header_names.h> if (msg.data.headers_len) { - u8 *headers = 0; - http_header_table_t *ht; - vec_validate (headers, msg.data.headers_len - 1); + http_header_table_t ht = HTTP_HEADER_TABLE_NULL; + http_init_header_table_buf (&ht, msg); rv = svm_fifo_peek (ts->rx_fifo, msg.data.headers_offset, - msg.data.headers_len, headers); + msg.data.headers_len, ht.buf); ASSERT (rv == msg.data.headers_len); - if (http_parse_headers (headers, &ht)) - { - /* your error handling */ - } + http_build_header_table (&ht, msg); /* get Content-Type header */ - const char *content_type = http_get_header (ht, http_header_name_str (HTTP_HEADER_CONTENT_TYPE)); + const http_header_t *content_type = http_get_header (&ht, http_header_name_token (HTTP_HEADER_CONTENT_TYPE)); if (content_type) { /* do something interesting */ } - http_free_header_table (ht); - vec_free (headers); + http_free_header_table (&ht); } Finally application reads body, which might be received in multiple pieces (depends on size), so we might need some state machine in ``builtin_app_rx_callback``. |