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>
This commit is contained in:
Matus Fabian 2024-11-21 17:01:45 +01:00 committed by Florin Coras
parent c4b4cd5e77
commit 82b3cc1826
7 changed files with 485 additions and 211 deletions

View File

@ -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")
}

View File

@ -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() {

View File

@ -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);
}

View File

@ -38,6 +38,12 @@ typedef struct
http_msg_t msg;
} hc_worker_t;
typedef struct
{
u8 *name;
u8 *value;
} hc_http_header_t;
typedef struct
{
u32 app_index;
@ -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;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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))
{
/* your error handling */
}
http_build_header_table (&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 (&ht, http_header_name_token (HTTP_HEADER_ACCEPT));
if (accept_value)
{
/* 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 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``.