diff options
author | Matus Fabian <matfabia@cisco.com> | 2024-07-12 11:07:17 +0200 |
---|---|---|
committer | Matus Fabian <matfabia@cisco.com> | 2024-07-29 14:21:12 +0200 |
commit | ba9ea13e260fab758c3d0c87201f33748972662d (patch) | |
tree | 15ebb1a5b11e665df36689e041e9833d50f24bc9 | |
parent | dd4356dc97c642744e3963157ca9a5a89d63ef08 (diff) |
http: client code improvement
Client app can sends request target, custom header and body to
HTTP layer. In response it receives all bytes as received from
transport, aditionally we provide offset and length of headers
and body.
In addtion client app is now able to receive response with all
status codes and Host header field is set in request at protocol
layer.
Type: improvement
Change-Id: I8c8e2c8f99cdf500126b7c2c722aebc254aa0d9f
Signed-off-by: Matus Fabian <matfabia@cisco.com>
-rw-r--r-- | extras/hs-test/http_test.go | 87 | ||||
-rw-r--r-- | src/plugins/hs_apps/http_cli.c | 4 | ||||
-rw-r--r-- | src/plugins/hs_apps/http_client_cli.c | 102 | ||||
-rw-r--r-- | src/plugins/hs_apps/http_tps.c | 6 | ||||
-rw-r--r-- | src/plugins/http/http.c | 354 | ||||
-rw-r--r-- | src/plugins/http/http.h | 5 | ||||
-rw-r--r-- | src/plugins/http/http_plugin.rst | 202 | ||||
-rw-r--r-- | src/plugins/http/http_status_codes.h | 27 | ||||
-rw-r--r-- | src/plugins/http_static/static_server.c | 4 |
9 files changed, 626 insertions, 165 deletions
diff --git a/extras/hs-test/http_test.go b/extras/hs-test/http_test.go index dcfe56199ed..75db60d43ae 100644 --- a/extras/hs-test/http_test.go +++ b/extras/hs-test/http_test.go @@ -3,6 +3,7 @@ package main import ( "bytes" "fmt" + "github.com/onsi/gomega/ghttp" "github.com/onsi/gomega/gmeasure" "io" "net" @@ -18,6 +19,7 @@ import ( func init() { RegisterVethTests(HttpCliTest, HttpCliConnectErrorTest) + RegisterSoloVethTests(HttpClientGetMemLeakTest) RegisterNoTopoTests(HeaderServerTest, HttpPersistentConnectionTest, HttpPipeliningTest, HttpStaticMovedTest, HttpStaticNotFoundTest, HttpCliMethodNotAllowedTest, HttpCliBadRequestTest, HttpStaticBuildInUrlGetIfStatsTest, HttpStaticBuildInUrlPostIfStatsTest, @@ -25,7 +27,7 @@ func init() { HttpContentLengthTest, HttpStaticBuildInUrlGetIfListTest, HttpStaticBuildInUrlGetVersionTest, HttpStaticMacTimeTest, HttpStaticBuildInUrlGetVersionVerboseTest, HttpVersionNotSupportedTest, HttpInvalidContentLengthTest, HttpInvalidTargetSyntaxTest, HttpStaticPathTraversalTest, HttpUriDecodeTest, - HttpHeadersTest, HttpStaticFileHandler) + HttpHeadersTest, HttpStaticFileHandler, HttpClientTest, HttpClientErrRespTest) RegisterNoTopoSoloTests(HttpStaticPromTest, HttpTpsTest, HttpTpsInterruptModeTest, PromConcurrentConnections, PromMemLeakTest) } @@ -169,6 +171,7 @@ func HttpCliTest(s *VethsSuite) { s.Log(o) s.AssertContains(o, "<html>", "<html> not found in the result!") + s.AssertContains(o, "</html>", "</html> not found in the result!") } func HttpCliConnectErrorTest(s *VethsSuite) { @@ -185,6 +188,51 @@ func HttpCliConnectErrorTest(s *VethsSuite) { s.AssertContains(o, "failed to connect") } +func HttpClientTest(s *NoTopoSuite) { + serverAddress := s.GetInterfaceByName(TapInterfaceName).Ip4AddressString() + server := ghttp.NewUnstartedServer() + l, err := net.Listen("tcp", serverAddress+":80") + s.AssertNil(err, fmt.Sprint(err)) + server.HTTPTestServer.Listener = l + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/test"), + ghttp.VerifyHeader(http.Header{"User-Agent": []string{"http_cli_client"}}), + ghttp.VerifyHeader(http.Header{"Accept": []string{"text / html"}}), + ghttp.RespondWith(http.StatusOK, "<html><body><p>Hello</p></body></html>"), + )) + server.Start() + defer server.Close() + uri := "http://" + serverAddress + "/80" + vpp := s.GetContainerByName("vpp").VppInstance + o := vpp.Vppctl("http cli client uri " + uri + " query /test") + + s.Log(o) + s.AssertContains(o, "<html>", "<html> not found in the result!") + s.AssertContains(o, "</html>", "</html> not found in the result!") +} + +func HttpClientErrRespTest(s *NoTopoSuite) { + serverAddress := s.GetInterfaceByName(TapInterfaceName).Ip4AddressString() + server := ghttp.NewUnstartedServer() + l, err := net.Listen("tcp", serverAddress+":80") + s.AssertNil(err, fmt.Sprint(err)) + server.HTTPTestServer.Listener = l + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/test"), + ghttp.RespondWith(http.StatusNotFound, "404: Not Found"), + )) + server.Start() + defer server.Close() + uri := "http://" + serverAddress + "/80" + vpp := s.GetContainerByName("vpp").VppInstance + o := vpp.Vppctl("http cli client uri " + uri + " query /test") + + s.Log(o) + s.AssertContains(o, "404: Not Found", "error not found in the result!") +} + func HttpStaticPromTest(s *NoTopoSuite) { query := "stats.prom" vpp := s.GetContainerByName("vpp").VppInstance @@ -276,9 +324,40 @@ func PromMemLeakTest(s *NoTopoSuite) { vpp.MemLeakCheck(traces1, traces2) } +func HttpClientGetMemLeakTest(s *VethsSuite) { + s.SkipUnlessLeakCheck() + + serverContainer := s.GetContainerByName("server-vpp").VppInstance + clientContainer := s.GetContainerByName("client-vpp").VppInstance + serverVeth := s.GetInterfaceByName(ServerInterfaceName) + + /* no goVPP less noise */ + clientContainer.Disconnect() + + serverContainer.Vppctl("http cli server") + + uri := "http://" + serverVeth.Ip4AddressString() + "/80" + + /* warmup request (FIB) */ + clientContainer.Vppctl("http cli client uri " + uri + " query /show/version") + + clientContainer.EnableMemoryTrace() + traces1, err := clientContainer.GetMemoryTrace() + s.AssertNil(err, fmt.Sprint(err)) + + clientContainer.Vppctl("http cli client uri " + uri + " query /show/vlib/graph") + + /* let's give it some time to clean up sessions */ + time.Sleep(time.Second * 12) + + traces2, err := clientContainer.GetMemoryTrace() + s.AssertNil(err, fmt.Sprint(err)) + clientContainer.MemLeakCheck(traces1, traces2) +} + func HttpStaticFileHandler(s *NoTopoSuite) { - content := "<http><body><p>Hello</p></body></http>" - content2 := "<http><body><p>Page</p></body></http>" + content := "<html><body><p>Hello</p></body></html>" + content2 := "<html><body><p>Page</p></body></html>" vpp := s.GetContainerByName("vpp").VppInstance vpp.Container.Exec("mkdir -p " + wwwRootPath) vpp.Container.CreateFile(wwwRootPath+"/index.html", content) @@ -357,7 +436,7 @@ func HttpStaticPathTraversalTest(s *NoTopoSuite) { func HttpStaticMovedTest(s *NoTopoSuite) { vpp := s.GetContainerByName("vpp").VppInstance vpp.Container.Exec("mkdir -p " + wwwRootPath + "/tmp.aaa") - vpp.Container.CreateFile(wwwRootPath+"/tmp.aaa/index.html", "<http><body><p>Hello</p></body></http>") + vpp.Container.CreateFile(wwwRootPath+"/tmp.aaa/index.html", "<html><body><p>Hello</p></body></html>") serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() s.Log(vpp.Vppctl("http static server www-root " + wwwRootPath + " uri tcp://" + serverAddress + "/80 debug")) diff --git a/src/plugins/hs_apps/http_cli.c b/src/plugins/hs_apps/http_cli.c index dfe50c58092..814f5f15fcf 100644 --- a/src/plugins/hs_apps/http_cli.c +++ b/src/plugins/hs_apps/http_cli.c @@ -207,7 +207,7 @@ start_send_data (hcs_session_t *hs, http_status_code_t status) done: if (svm_fifo_set_event (ts->tx_fifo)) - session_send_io_evt_to_thread (ts->tx_fifo, SESSION_IO_EVT_TX); + session_program_tx_io_evt (ts->handle, SESSION_IO_EVT_TX); } static void @@ -469,7 +469,7 @@ hcs_ts_tx_callback (session_t *ts) } if (svm_fifo_set_event (ts->tx_fifo)) - session_send_io_evt_to_thread (ts->tx_fifo, SESSION_IO_EVT_TX); + session_program_tx_io_evt (ts->handle, SESSION_IO_EVT_TX); return 0; } diff --git a/src/plugins/hs_apps/http_client_cli.c b/src/plugins/hs_apps/http_client_cli.c index a99169bafea..722499b24c3 100644 --- a/src/plugins/hs_apps/http_client_cli.c +++ b/src/plugins/hs_apps/http_client_cli.c @@ -16,6 +16,9 @@ #include <vnet/session/application_interface.h> #include <vnet/session/session.h> #include <http/http.h> +#include <http/http_header_names.h> +#include <http/http_content_types.h> +#include <http/http_status_codes.h> #define HCC_DEBUG 0 @@ -34,12 +37,12 @@ typedef struct u32 vpp_session_index; u32 to_recv; u8 is_closed; + http_header_t *req_headers; } hcc_session_t; typedef struct { hcc_session_t *sessions; - u8 *rx_buf; u32 thread_index; } hcc_worker_t; @@ -95,13 +98,6 @@ hcc_session_get (u32 hs_index, u32 thread_index) return pool_elt_at_index (wrk->sessions, hs_index); } -static void -hcc_session_free (u32 thread_index, hcc_session_t *hs) -{ - hcc_worker_t *wrk = hcc_worker_get (thread_index); - pool_put (wrk->sessions, hs); -} - static int hcc_ts_accept_callback (session_t *ts) { @@ -128,6 +124,7 @@ hcc_ts_connected_callback (u32 app_index, u32 hc_index, session_t *as, hcc_session_t *hs, *new_hs; hcc_worker_t *wrk; http_msg_t msg; + u8 *headers_buf; int rv; HCC_DBG ("hc_index: %d", hc_index); @@ -149,24 +146,42 @@ hcc_ts_connected_callback (u32 app_index, u32 hc_index, session_t *as, hs->vpp_session_index = as->session_index; + http_add_header (&hs->req_headers, + http_header_name_token (HTTP_HEADER_ACCEPT), + http_content_type_token (HTTP_CONTENT_TEXT_HTML)); + headers_buf = http_serialize_headers (hs->req_headers); + vec_free (hs->req_headers); + msg.type = HTTP_MSG_REQUEST; msg.method_type = HTTP_REQ_GET; - msg.content_type = HTTP_CONTENT_TEXT_HTML; + /* request target */ + msg.data.target_form = HTTP_TARGET_ORIGIN_FORM; + msg.data.target_path_offset = 0; + msg.data.target_path_len = vec_len (hcm->http_query); + /* custom headers */ + msg.data.headers_offset = msg.data.target_path_len; + msg.data.headers_len = vec_len (headers_buf); + /* request body */ + msg.data.body_len = 0; + /* data type and total length */ msg.data.type = HTTP_MSG_DATA_INLINE; - msg.data.len = vec_len (hcm->http_query); + msg.data.len = + msg.data.target_path_len + msg.data.headers_len + msg.data.body_len; - svm_fifo_seg_t segs[2] = { { (u8 *) &msg, sizeof (msg) }, - { hcm->http_query, vec_len (hcm->http_query) } }; + svm_fifo_seg_t segs[3] = { { (u8 *) &msg, sizeof (msg) }, + { hcm->http_query, vec_len (hcm->http_query) }, + { headers_buf, vec_len (headers_buf) } }; - rv = svm_fifo_enqueue_segments (as->tx_fifo, segs, 2, 0 /* allow partial */); - if (rv < 0 || rv != sizeof (msg) + vec_len (hcm->http_query)) + rv = svm_fifo_enqueue_segments (as->tx_fifo, segs, 3, 0 /* allow partial */); + vec_free (headers_buf); + if (rv < 0 || rv != sizeof (msg) + msg.data.len) { clib_warning ("failed app enqueue"); return -1; } if (svm_fifo_set_event (as->tx_fifo)) - session_send_io_evt_to_thread (as->tx_fifo, SESSION_IO_EVT_TX); + session_program_tx_io_evt (as->handle, SESSION_IO_EVT_TX); return 0; } @@ -221,17 +236,26 @@ hcc_ts_rx_callback (session_t *ts) if (hs->to_recv == 0) { + /* read the http message header */ rv = svm_fifo_dequeue (ts->rx_fifo, sizeof (msg), (u8 *) &msg); ASSERT (rv == sizeof (msg)); - if (msg.type != HTTP_MSG_REPLY || msg.code != HTTP_STATUS_OK) + if (msg.type != HTTP_MSG_REPLY) { clib_warning ("unexpected msg type %d", msg.type); return 0; } - vec_validate (hcm->http_response, msg.data.len - 1); + /* drop everything up to body */ + svm_fifo_dequeue_drop (ts->rx_fifo, msg.data.body_offset); + hs->to_recv = msg.data.body_len; + if (msg.code != HTTP_STATUS_OK && hs->to_recv == 0) + { + hcm->http_response = format (0, "request failed, response code: %U", + format_http_status_code, msg.code); + goto done; + } + vec_validate (hcm->http_response, msg.data.body_len - 1); vec_reset_length (hcm->http_response); - hs->to_recv = msg.data.len; } u32 max_deq = svm_fifo_max_dequeue (ts->rx_fifo); @@ -253,8 +277,10 @@ hcc_ts_rx_callback (session_t *ts) hs->to_recv -= rv; HCC_DBG ("app rcvd %d, remains %d", rv, hs->to_recv); +done: if (hs->to_recv == 0) { + HCC_DBG ("all data received, going to disconnect"); hcc_session_disconnect (ts); vlib_process_signal_event_mt (hcm->vlib_main, hcm->cli_node_index, HCC_REPLY_RECEIVED, 0); @@ -264,18 +290,6 @@ hcc_ts_rx_callback (session_t *ts) } static void -hcc_ts_cleanup_callback (session_t *s, session_cleanup_ntf_t ntf) -{ - hcc_session_t *hs; - - hs = hcc_session_get (s->thread_index, s->opaque); - if (!hs) - return; - - hcc_session_free (s->thread_index, hs); -} - -static void hcc_ts_transport_closed (session_t *s) { hcc_main_t *hcm = &hcc_main; @@ -293,7 +307,6 @@ static session_cb_vft_t hcc_session_cb_vft = { .builtin_app_rx_callback = hcc_ts_rx_callback, .builtin_app_tx_callback = hcc_ts_tx_callback, .session_reset_callback = hcc_ts_reset_callback, - .session_cleanup_callback = hcc_ts_cleanup_callback, .session_transport_closed_callback = hcc_ts_transport_closed, }; @@ -423,7 +436,6 @@ hcc_run (vlib_main_t *vm, int print_output) case HCC_REPLY_RECEIVED: if (print_output) vlib_cli_output (vm, "%v", hcm->http_response); - vec_free (hcm->http_response); break; case HCC_TRANSPORT_CLOSED: err = clib_error_return (0, "error, transport closed"); @@ -460,6 +472,28 @@ hcc_detach () return rv; } +static void +hcc_worker_cleanup (hcc_worker_t *wrk) +{ + pool_free (wrk->sessions); +} + +static void +hcc_cleanup () +{ + hcc_main_t *hcm = &hcc_main; + hcc_worker_t *wrk; + + vec_foreach (wrk, hcm->wrk) + hcc_worker_cleanup (wrk); + + vec_free (hcm->uri); + vec_free (hcm->http_query); + vec_free (hcm->http_response); + vec_free (hcm->appns_id); + vec_free (hcm->wrk); +} + static clib_error_t * hcc_command_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli_command_t *cmd) @@ -509,7 +543,6 @@ hcc_command_fn (vlib_main_t *vm, unformat_input_t *input, } } - vec_free (hcm->appns_id); hcm->appns_id = appns_id; hcm->cli_node_index = vlib_get_current_process (vm)->node_runtime.node_index; @@ -540,8 +573,7 @@ hcc_command_fn (vlib_main_t *vm, unformat_input_t *input, } done: - vec_free (hcm->uri); - vec_free (hcm->http_query); + hcc_cleanup (); unformat_free (line_input); return err; } diff --git a/src/plugins/hs_apps/http_tps.c b/src/plugins/hs_apps/http_tps.c index 9cc592cd746..b37c447295a 100644 --- a/src/plugins/hs_apps/http_tps.c +++ b/src/plugins/hs_apps/http_tps.c @@ -154,7 +154,7 @@ hts_session_tx_zc (hts_session_t *hs, session_t *ts) svm_fifo_add_want_deq_ntf (ts->tx_fifo, SVM_FIFO_WANT_DEQ_NOTIF); if (svm_fifo_set_event (ts->tx_fifo)) - session_send_io_evt_to_thread (ts->tx_fifo, SESSION_IO_EVT_TX); + session_program_tx_io_evt (ts->handle, SESSION_IO_EVT_TX); } static void @@ -201,7 +201,7 @@ hts_session_tx_no_zc (hts_session_t *hs, session_t *ts) svm_fifo_add_want_deq_ntf (ts->tx_fifo, SVM_FIFO_WANT_DEQ_NOTIF); if (svm_fifo_set_event (ts->tx_fifo)) - session_send_io_evt_to_thread (ts->tx_fifo, SESSION_IO_EVT_TX); + session_program_tx_io_evt (ts->handle, SESSION_IO_EVT_TX); } static inline void @@ -263,7 +263,7 @@ hts_start_send_data (hts_session_t *hs, http_status_code_t status) if (!msg.data.body_len) { if (svm_fifo_set_event (ts->tx_fifo)) - session_send_io_evt_to_thread (ts->tx_fifo, SESSION_IO_EVT_TX); + session_program_tx_io_evt (ts->handle, SESSION_IO_EVT_TX); return; } diff --git a/src/plugins/http/http.c b/src/plugins/http/http.c index 416ba8fe817..017fd50ebae 100644 --- a/src/plugins/http/http.c +++ b/src/plugins/http/http.c @@ -16,11 +16,11 @@ #include <http/http.h> #include <vnet/session/session.h> #include <http/http_timer.h> +#include <http/http_status_codes.h> static http_main_t http_main; #define HTTP_FIFO_THRESH (16 << 10) -#define CONTENT_LEN_STR "Content-Length: " /* HTTP state machine result */ typedef enum http_sm_result_t_ @@ -30,18 +30,12 @@ typedef enum http_sm_result_t_ HTTP_SM_ERROR = -1, } http_sm_result_t; -const char *http_status_code_str[] = { -#define _(c, s, str) str, - foreach_http_status_code -#undef _ -}; - const http_buffer_type_t msg_to_buf_type[] = { [HTTP_MSG_DATA_INLINE] = HTTP_BUFFER_FIFO, [HTTP_MSG_DATA_PTR] = HTTP_BUFFER_PTR, }; -u8 * +static u8 * format_http_state (u8 *s, va_list *va) { http_state_t state = va_arg (*va, http_state_t); @@ -388,9 +382,13 @@ static const char *http_response_template = "HTTP/1.1 %s\r\n" "Content-Length: %u\r\n" "%s"; +/** + * http request boilerplate + */ static const char *http_request_template = "GET %s HTTP/1.1\r\n" + "Host: %v\r\n" "User-Agent: %v\r\n" - "Accept: */*\r\n\r\n"; + "%s"; static u32 http_send_data (http_conn_t *hc, u8 *data, u32 length, u32 offset) @@ -409,7 +407,7 @@ http_send_data (http_conn_t *hc, u8 *data, u32 length, u32 offset) return offset; if (svm_fifo_set_event (ts->tx_fifo)) - session_send_io_evt_to_thread (ts->tx_fifo, SESSION_IO_EVT_TX); + session_program_tx_io_evt (ts->handle, SESSION_IO_EVT_TX); return (offset + sent); } @@ -655,6 +653,111 @@ http_parse_request_line (http_conn_t *hc, http_status_code_t *ec) return 0; } +#define expect_char(c) \ + if (*p++ != c) \ + { \ + clib_warning ("unexpected character"); \ + return -1; \ + } + +#define parse_int(val, mul) \ + do \ + { \ + if (!isdigit (*p)) \ + { \ + clib_warning ("expected digit"); \ + return -1; \ + } \ + val += mul * (*p++ - '0'); \ + } \ + while (0) + +static int +http_parse_status_line (http_conn_t *hc) +{ + int i; + u32 next_line_offset; + u8 *p, *end; + u16 status_code = 0; + + i = v_find_index (hc->rx_buf, 0, 0, "\r\n"); + /* status-line = HTTP-version SP status-code SP [ reason-phrase ] CRLF */ + if (i < 0) + { + clib_warning ("status line incomplete"); + return -1; + } + HTTP_DBG (0, "status line length: %d", i); + if (i < 12) + { + clib_warning ("status line too short (%d)", i); + return -1; + } + next_line_offset = i + 2; + p = hc->rx_buf; + end = hc->rx_buf + i; + + /* there should be at least one more CRLF */ + if (vec_len (hc->rx_buf) < (next_line_offset + 2)) + { + clib_warning ("malformed message, too short"); + return -1; + } + + /* parse version */ + expect_char ('H'); + expect_char ('T'); + expect_char ('T'); + expect_char ('P'); + expect_char ('/'); + expect_char ('1'); + expect_char ('.'); + if (!isdigit (*p++)) + { + clib_warning ("invalid HTTP minor version"); + return -1; + } + + /* skip space(s) */ + if (*p != ' ') + { + clib_warning ("no space after HTTP version"); + return -1; + } + do + { + p++; + if (p == end) + { + clib_warning ("no status code"); + return -1; + } + } + while (*p == ' '); + + /* parse status code */ + if ((end - p) < 3) + { + clib_warning ("not enough characters for status code"); + return -1; + } + parse_int (status_code, 100); + parse_int (status_code, 10); + parse_int (status_code, 1); + if (status_code < 100 || status_code > 599) + { + clib_warning ("invalid status code %d", status_code); + return -1; + } + hc->status_code = status_code; + HTTP_DBG (0, "status code: %d", hc->status_code); + + /* set buffer offset to nex line start */ + hc->rx_buf_offset = next_line_offset; + + return 0; +} + static int http_identify_headers (http_conn_t *hc, http_status_code_t *ec) { @@ -692,6 +795,7 @@ http_identify_message_body (http_conn_t *hc, http_status_code_t *ec) unformat_input_t input; int i, len; u8 *line; + u32 body_len; hc->body_len = 0; @@ -733,13 +837,14 @@ http_identify_message_body (http_conn_t *hc, http_status_code_t *ec) HTTP_DBG (0, "%v", line); unformat_init_vector (&input, line); - if (!unformat (&input, "%lu", &hc->body_len)) + if (!unformat (&input, "%u", &body_len)) { clib_warning ("failed to unformat content length value"); *ec = HTTP_STATUS_BAD_REQUEST; return -1; } unformat_free (&input); + hc->body_len = body_len; hc->body_offset = hc->headers_offset + hc->headers_len + 2; HTTP_DBG (0, "body length: %u", hc->body_len); @@ -748,61 +853,16 @@ http_identify_message_body (http_conn_t *hc, http_status_code_t *ec) return 0; } -static int -http_parse_header (http_conn_t *hc, int *content_length) -{ - unformat_input_t input; - int i, len; - u8 *line; - - i = v_find_index (hc->rx_buf, hc->rx_buf_offset, 0, CONTENT_LEN_STR); - if (i < 0) - { - clib_warning ("cannot find '%s' in the header!", CONTENT_LEN_STR); - return -1; - } - - hc->rx_buf_offset = i; - - i = v_find_index (hc->rx_buf, hc->rx_buf_offset, 0, "\n"); - if (i < 0) - { - clib_warning ("end of line missing; incomplete data"); - return -1; - } - - len = i - hc->rx_buf_offset; - line = vec_new (u8, len); - clib_memcpy (line, hc->rx_buf + hc->rx_buf_offset, len); - - unformat_init_vector (&input, line); - if (!unformat (&input, CONTENT_LEN_STR "%d", content_length)) - { - clib_warning ("failed to unformat content length!"); - return -1; - } - unformat_free (&input); - - /* skip rest of the header */ - hc->rx_buf_offset += len; - i = v_find_index (hc->rx_buf, hc->rx_buf_offset, 0, "<html>"); - if (i < 0) - { - clib_warning ("<html> tag not found"); - return -1; - } - hc->rx_buf_offset = i; - - return 0; -} - static http_sm_result_t http_state_wait_server_reply (http_conn_t *hc, transport_send_params_t *sp) { - int i, rv, content_length; + int rv; http_msg_t msg = {}; app_worker_t *app_wrk; session_t *as; + u32 len; + http_status_code_t ec; + http_main_t *hm = &http_main; rv = http_read_message (hc); @@ -813,57 +873,58 @@ http_state_wait_server_reply (http_conn_t *hc, transport_send_params_t *sp) return HTTP_SM_STOP; } + HTTP_DBG (0, "%v", hc->rx_buf); + if (vec_len (hc->rx_buf) < 8) { clib_warning ("response buffer too short"); goto error; } - if ((i = v_find_index (hc->rx_buf, 0, 0, "200 OK")) >= 0) - { - msg.type = HTTP_MSG_REPLY; - msg.content_type = HTTP_CONTENT_TEXT_HTML; - msg.code = HTTP_STATUS_OK; - msg.data.type = HTTP_MSG_DATA_INLINE; - msg.data.len = 0; + rv = http_parse_status_line (hc); + if (rv) + goto error; - rv = http_parse_header (hc, &content_length); - if (rv) - { - clib_warning ("failed to parse http reply"); - goto error; - } - msg.data.len = content_length; - u32 dlen = vec_len (hc->rx_buf) - hc->rx_buf_offset; - as = session_get_from_handle (hc->h_pa_session_handle); - svm_fifo_seg_t segs[2] = { { (u8 *) &msg, sizeof (msg) }, - { &hc->rx_buf[hc->rx_buf_offset], dlen } }; - - rv = svm_fifo_enqueue_segments (as->rx_fifo, segs, 2, - 0 /* allow partial */); - if (rv < 0) - { - clib_warning ("error enqueue"); - return HTTP_SM_ERROR; - } + rv = http_identify_headers (hc, &ec); + if (rv) + goto error; - hc->rx_buf_offset += dlen; - hc->to_recv = content_length - dlen; + rv = http_identify_message_body (hc, &ec); + if (rv) + goto error; - if (hc->rx_buf_offset == vec_len (hc->rx_buf)) - { - vec_reset_length (hc->rx_buf); - hc->rx_buf_offset = 0; - } + len = vec_len (hc->rx_buf); - if (hc->to_recv == 0) - { - hc->rx_buf_offset = 0; - vec_reset_length (hc->rx_buf); - http_state_change (hc, HTTP_STATE_WAIT_APP_METHOD); - } + msg.type = HTTP_MSG_REPLY; + msg.code = hm->sc_by_u16[hc->status_code]; + msg.data.headers_offset = hc->headers_offset; + msg.data.headers_len = hc->headers_len; + msg.data.body_offset = hc->body_offset; + msg.data.body_len = hc->body_len; + msg.data.type = HTTP_MSG_DATA_INLINE; + msg.data.len = len; + + as = session_get_from_handle (hc->h_pa_session_handle); + svm_fifo_seg_t segs[2] = { { (u8 *) &msg, sizeof (msg) }, + { hc->rx_buf, len } }; + + rv = svm_fifo_enqueue_segments (as->rx_fifo, segs, 2, 0 /* allow partial */); + if (rv < 0) + { + clib_warning ("error enqueue"); + return HTTP_SM_ERROR; + } + + vec_free (hc->rx_buf); + + if (hc->body_len <= (len - hc->body_offset)) + { + hc->to_recv = 0; + http_state_change (hc, HTTP_STATE_WAIT_APP_METHOD); + } else { + hc->to_recv = hc->body_len - (len - hc->body_offset); http_state_change (hc, HTTP_STATE_CLIENT_IO_MORE_DATA); } @@ -871,12 +932,6 @@ http_state_wait_server_reply (http_conn_t *hc, transport_send_params_t *sp) if (app_wrk) app_worker_rx_notify (app_wrk, as); return HTTP_SM_STOP; - } - else - { - clib_warning ("Unknown http method %v", hc->rx_buf); - goto error; - } error: session_transport_closing_notify (&hc->connection); @@ -925,7 +980,6 @@ http_state_wait_client_method (http_conn_t *hc, transport_send_params_t *sp) msg.type = HTTP_MSG_REQUEST; msg.method_type = hc->method; - msg.content_type = HTTP_CONTENT_TEXT_HTML; msg.data.type = HTTP_MSG_DATA_INLINE; msg.data.len = len; msg.data.target_form = hc->target_form; @@ -1080,7 +1134,7 @@ http_state_wait_app_method (http_conn_t *hc, transport_send_params_t *sp) { http_msg_t msg; session_t *as; - u8 *buf = 0, *request; + u8 *target = 0, *request; u32 offset; int rv; @@ -1107,12 +1161,44 @@ http_state_wait_app_method (http_conn_t *hc, transport_send_params_t *sp) clib_warning ("unsupported method %d", msg.method_type); goto error; } + if (msg.data.body_len != 0) + { + clib_warning ("GET request shouldn't include data"); + goto error; + } - vec_validate (buf, msg.data.len - 1); - rv = svm_fifo_dequeue (as->tx_fifo, msg.data.len, buf); - ASSERT (rv == msg.data.len); + /* read request target */ + vec_validate (target, msg.data.target_path_len - 1); + rv = svm_fifo_dequeue (as->tx_fifo, msg.data.target_path_len, target); + ASSERT (rv == msg.data.target_path_len); + + /* + * Add "protocol layer" headers: + * - host + * - user agent + */ + request = format (0, http_request_template, + /* target */ + target, + /* Host */ + hc->host, + /* User-Agent*/ + hc->app_name, + /* Any headers from app? */ + msg.data.headers_len ? "" : "\r\n"); + + /* Add headers from app (if any) */ + if (msg.data.headers_len) + { + HTTP_DBG (0, "get headers from app, len %d", msg.data.headers_len); + u32 orig_len = vec_len (request); + vec_resize (request, msg.data.headers_len); + u8 *p = request + orig_len; + rv = svm_fifo_dequeue (as->tx_fifo, msg.data.headers_len, p); + ASSERT (rv == msg.data.headers_len); + } + HTTP_DBG (0, "%v", request); - request = format (0, http_request_template, buf, hc->app_name); offset = http_send_data (hc, request, vec_len (request), 0); if (offset != vec_len (request)) { @@ -1122,7 +1208,7 @@ http_state_wait_app_method (http_conn_t *hc, transport_send_params_t *sp) http_state_change (hc, HTTP_STATE_WAIT_SERVER_REPLY); - vec_free (buf); + vec_free (target); vec_free (request); return HTTP_SM_STOP; @@ -1232,7 +1318,7 @@ http_state_app_io_more_data (http_conn_t *hc, transport_send_params_t *sp) if (!http_buffer_is_drained (hb)) { if (sent && svm_fifo_set_event (ts->tx_fifo)) - session_send_io_evt_to_thread (ts->tx_fifo, SESSION_IO_EVT_TX); + session_program_tx_io_evt (ts->handle, SESSION_IO_EVT_TX); if (svm_fifo_max_enqueue (ts->tx_fifo) < HTTP_FIFO_THRESH) { @@ -1246,7 +1332,7 @@ http_state_app_io_more_data (http_conn_t *hc, transport_send_params_t *sp) else { if (sent && svm_fifo_set_event (ts->tx_fifo)) - session_send_io_evt_to_thread (ts->tx_fifo, SESSION_IO_EVT_TX_FLUSH); + session_program_tx_io_evt (ts->handle, SESSION_IO_EVT_TX_FLUSH); /* Finished transaction, back to HTTP_STATE_WAIT_METHOD */ http_state_change (hc, HTTP_STATE_WAIT_CLIENT_METHOD); @@ -1346,6 +1432,7 @@ http_ts_cleanup_callback (session_t *ts, session_cleanup_ntf_t ntf) clib_warning ("no http connection for %u", ts->session_index); return; } + HTTP_DBG (1, "going to free session %x", ts->opaque); vec_free (hc->rx_buf); @@ -1353,6 +1440,12 @@ http_ts_cleanup_callback (session_t *ts, session_cleanup_ntf_t ntf) http_conn_timer_stop (hc); session_transport_delete_notify (&hc->connection); + + if (!hc->is_server) + { + vec_free (hc->app_name); + vec_free (hc->host); + } http_conn_free (hc); } @@ -1458,11 +1551,20 @@ http_transport_connect (transport_endpoint_cfg_t *tep) hc->state = HTTP_CONN_STATE_CONNECTING; cargs->api_context = hc_index; + hc->is_server = 0; + if (vec_len (app->name)) hc->app_name = vec_dup (app->name); else hc->app_name = format (0, "VPP HTTP client"); + if (sep->is_ip4) + hc->host = format (0, "%U:%d", format_ip4_address, &sep->ip.ip4, + clib_net_to_host_u16 (sep->port)); + else + hc->host = format (0, "%U:%d", format_ip6_address, &sep->ip.ip6, + clib_net_to_host_u16 (sep->port)); + HTTP_DBG (1, "hc ho_index %x", hc_index); if ((error = vnet_connect (cargs))) @@ -1515,6 +1617,8 @@ http_start_listen (u32 app_listener_index, transport_endpoint_cfg_t *tep) lhc->c_s_index = app_listener_index; lhc->c_flags |= TRANSPORT_CONNECTION_F_NO_LOOKUP; + lhc->is_server = 1; + if (vec_len (app->name)) lhc->app_name = vec_dup (app->name); else @@ -1762,6 +1866,7 @@ static clib_error_t * http_transport_init (vlib_main_t *vm) { http_main_t *hm = &http_main; + int i; transport_register_protocol (TRANSPORT_PROTO_HTTP, &http_proto, FIB_PROTOCOL_IP4, ~0); @@ -1773,7 +1878,26 @@ http_transport_init (vlib_main_t *vm) hm->first_seg_size = 32 << 20; hm->fifo_size = 512 << 10; - return 0; + /* Setup u16 to http_status_code_t map */ + /* Unrecognized status code is equivalent to the x00 status */ + vec_validate (hm->sc_by_u16, 599); + for (i = 100; i < 200; i++) + hm->sc_by_u16[i] = HTTP_STATUS_CONTINUE; + for (i = 200; i < 300; i++) + hm->sc_by_u16[i] = HTTP_STATUS_OK; + for (i = 300; i < 400; i++) + hm->sc_by_u16[i] = HTTP_STATUS_MULTIPLE_CHOICES; + for (i = 400; i < 500; i++) + hm->sc_by_u16[i] = HTTP_STATUS_BAD_REQUEST; + for (i = 500; i < 600; i++) + hm->sc_by_u16[i] = HTTP_STATUS_INTERNAL_ERROR; + + /* Registered status codes */ +#define _(c, s, str) hm->sc_by_u16[c] = HTTP_STATUS_##s; + foreach_http_status_code +#undef _ + + return 0; } VLIB_INIT_FUNCTION (http_transport_init); diff --git a/src/plugins/http/http.h b/src/plugins/http/http.h index 07d5472346e..e3267dfc3b2 100644 --- a/src/plugins/http/http.h +++ b/src/plugins/http/http.h @@ -368,7 +368,6 @@ typedef struct http_msg_ http_req_method_t method_type; http_status_code_t code; }; - http_content_type_t content_type; http_msg_data_t data; } http_msg_t; @@ -388,6 +387,8 @@ typedef struct http_tc_ http_conn_state_t state; u32 timer_handle; u8 *app_name; + u8 *host; + u8 is_server; /* * Current request @@ -408,6 +409,7 @@ typedef struct http_tc_ u32 headers_len; u32 body_offset; u32 body_len; + u16 status_code; } http_conn_t; typedef struct http_worker_ @@ -423,6 +425,7 @@ typedef struct http_main_ clib_timebase_t timebase; + u16 *sc_by_u16; /* * Runtime config */ diff --git a/src/plugins/http/http_plugin.rst b/src/plugins/http/http_plugin.rst index 2f7a58ef9bf..273812735f7 100644 --- a/src/plugins/http/http_plugin.rst +++ b/src/plugins/http/http_plugin.rst @@ -191,10 +191,10 @@ Following example shows how to create headers section: http_add_header (resp_headers, http_header_name_token (HTTP_HEADER_CACHE_CONTROL), http_token_lit ("max-age=600")); - http_add_header (&hs->resp_headers, + http_add_header (resp_headers, http_header_name_token (HTTP_HEADER_LOCATION), (const char *) redirect, vec_len (redirect)); - headers_buf = http_serialize_headers (hs->resp_headers); + headers_buf = http_serialize_headers (resp_headers); The example below show how to create and send response HTTP message metadata: @@ -236,7 +236,7 @@ Finally application sends response body: vec_free (tx_buf); } if (svm_fifo_set_event (ts->tx_fifo)) - session_send_io_evt_to_thread (ts->tx_fifo, SESSION_IO_EVT_TX); + session_program_tx_io_evt (ts->handle, SESSION_IO_EVT_TX); Example above shows how to send body data by copy, alternatively you could pass it as pointer: @@ -249,3 +249,199 @@ Example above shows how to send body data by copy, alternatively you could pass ASSERT (rv == sizeof (data)); In this case you need to free data when you receive next request or when session is closed. + + +Client application +^^^^^^^^^^^^^^^^^^ + +Client application opens connection with vnet URI where transport protocol is set to ``http``. + +Sending data +"""""""""""""" + +HTTP request is sent when connection is successfully established in ``session_connected_callback``. + +When client application sends message to HTTP layer it starts with message metadata, followed by request target, optional headers and body (if any) buffers. + +Application should set following items: + +* HTTP method +* target form, offset and length +* header section offset and length +* body offset and length + +Application could pass headers to HTTP layer. Header list is created dynamically as vector of ``http_header_t``, +where we store only pointers to buffers (zero copy). +Well known header names are predefined. +The list is serialized just before you send buffer to HTTP layer. + +.. note:: + Following headers are added at protocol layer and **MUST NOT** be set by application: Host, User-Agent + + +The example below shows how to create headers section: + +.. code-block:: C + + #include <http/http.h> + #include <http/http_header_names.h> + #include <http/http_content_types.h> + http_header_t *req_headers = 0; + u8 *headers_buf = 0; + http_add_header (req_headers, + http_header_name_token (HTTP_HEADER_ACCEPT), + http_content_type_token (HTTP_CONTENT_TEXT_HTML)); + headers_buf = http_serialize_headers (req_headers); + vec_free (hs->req_headers); + +Following example shows how to set message metadata: + +.. code-block:: C + + http_msg_t msg; + msg.type = HTTP_MSG_REQUEST; + msg.method_type = HTTP_REQ_GET; + msg.data.headers_offset = 0; + /* request target */ + msg.data.target_form = HTTP_TARGET_ORIGIN_FORM; + msg.data.target_path_offset = 0; + msg.data.target_path_len = vec_len (target); + /* custom headers */ + msg.data.headers_offset = msg.data.target_path_len; + msg.data.headers_len = vec_len (headers_buf); + /* no request body because we are doing GET request */ + msg.data.body_len = 0; + /* data type and total length */ + msg.data.type = HTTP_MSG_DATA_INLINE; + msg.data.len = msg.data.target_path_len + msg.data.headers_len + msg.data.body_len; + +Finally application sends everything to HTTP layer: + +.. code-block:: C + + svm_fifo_seg_t segs[3] = { { (u8 *) &msg, sizeof (msg) }, /* message metadata */ + { target, vec_len (target) }, /* request target */ + { headers_buf, vec_len (headers_buf) } }; /* serialized headers */ + rv = svm_fifo_enqueue_segments (as->tx_fifo, segs, 3, 0 /* allow partial */); + vec_free (headers_buf); + if (rv < 0 || rv != sizeof (msg) + msg.data.len) + { + clib_warning ("failed app enqueue"); + return -1; + } + if (svm_fifo_set_event (as->tx_fifo)) + session_program_tx_io_evt (as->handle, SESSION_IO_EVT_TX); + +Receiving data +"""""""""""""" + +HTTP plugin sends message header with metadata for parsing, in form of offset and length, followed by all data bytes as received from transport. + +Application will get pre-parsed following items: + +* status code +* header section offset and length +* body offset and length + +The example below reads HTTP message header in ``builtin_app_rx_callback``, which is first step application should do: + +.. code-block:: C + + #include <http/http.h> + http_msg_t msg; + rv = svm_fifo_dequeue (ts->rx_fifo, sizeof (msg), (u8 *) &msg); + ASSERT (rv == sizeof (msg)); + +As next step application might validate message type and status code: + +.. code-block:: C + + if (msg.type != HTTP_MSG_REPLY) + { + /* your error handling */ + } + if (msg.code != HTTP_STATUS_OK) + { + /* your error handling */ + /* of course you can continue with steps bellow */ + /* you might be interested in some headers or body content (if any) */ + } + +Headers are parsed using a generic algorithm, independent of the individual header names. +When header is repeated, its combined value consists of all values separated by comma, concatenated in order as received. +Following example shows how to parse headers: + +.. code-block:: C + + #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); + rv = svm_fifo_peek (ts->rx_fifo, msg.data.headers_offset, + msg.data.headers_len, headers); + ASSERT (rv == msg.data.headers_len); + if (http_parse_headers (headers, &ht)) + { + /* your error handling */ + } + /* get Content-Type header */ + const char *content_type = http_get_header (ht, http_header_name_str (HTTP_HEADER_CONTENT_TYPE)); + if (content_type) + { + /* do something interesting */ + } + http_free_header_table (ht); + vec_free (headers); + } + +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``. +We will add following members to our session context structure: + +.. code-block:: C + + typedef struct + { + /* ... */ + u32 to_recv; + u8 *resp_body; + } session_ctx_t; + +First we prepare vector for response body, do it only once when you are reading metadata: + +.. code-block:: C + + /* drop everything up to body */ + svm_fifo_dequeue_drop (ts->rx_fifo, msg.data.body_offset); + ctx->to_recv = msg.data.body_len; + /* prepare vector for response body */ + vec_validate (ctx->resp_body, msg.data.body_len - 1); + vec_reset_length (ctx->resp_body); + +Now we can start reading body content, following block of code could be executed multiple times: + +.. code-block:: C + + /* dequeue */ + u32 max_deq = svm_fifo_max_dequeue (ts->rx_fifo); + u32 n_deq = clib_min (to_recv, max_deq); + /* current offset */ + u32 curr = vec_len (ctx->resp_body); + rv = svm_fifo_dequeue (ts->rx_fifo, n_deq, ctx->resp_body + curr); + if (rv < 0 || rv != n_deq) + { + /* your error handling */ + } + /* update length of the vector */ + vec_set_len (ctx->resp_body, curr + n_deq); + /* update number of remaining bytes to receive */ + ASSERT (to_recv >= rv); + ctx->to_recv -= rv; + /* check if all data received */ + if (ctx->to_recv == 0) + { + /* we are done */ + /* close the session if you don't want to send another request */ + /* and update state machine... */ + } diff --git a/src/plugins/http/http_status_codes.h b/src/plugins/http/http_status_codes.h new file mode 100644 index 00000000000..14b6b7db42d --- /dev/null +++ b/src/plugins/http/http_status_codes.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright(c) 2024 Cisco Systems, Inc. + */ + +#ifndef SRC_PLUGINS_HTTP_HTTP_STATUS_CODES_H_ +#define SRC_PLUGINS_HTTP_HTTP_STATUS_CODES_H_ + +#include <http/http.h> + +const char *http_status_code_str[] = { +#define _(c, s, str) str, + foreach_http_status_code +#undef _ +}; + +static inline u8 * +format_http_status_code (u8 *s, va_list *va) +{ + http_status_code_t status_code = va_arg (*va, http_status_code_t); + if (status_code < HTTP_N_STATUS) + s = format (s, "%s", http_status_code_str[status_code]); + else + s = format (s, "invalid status code %d", status_code); + return s; +} + +#endif /* SRC_PLUGINS_HTTP_HTTP_STATUS_CODES_H_ */ diff --git a/src/plugins/http_static/static_server.c b/src/plugins/http_static/static_server.c index d987a3eb7fb..b47fc9c3f0b 100644 --- a/src/plugins/http_static/static_server.c +++ b/src/plugins/http_static/static_server.c @@ -158,7 +158,7 @@ start_send_data (hss_session_t *hs, http_status_code_t status) done: if (svm_fifo_set_event (ts->tx_fifo)) - session_send_io_evt_to_thread (ts->tx_fifo, SESSION_IO_EVT_TX); + session_program_tx_io_evt (ts->handle, SESSION_IO_EVT_TX); } __clib_export void @@ -618,7 +618,7 @@ hss_ts_tx_callback (session_t *ts) } if (svm_fifo_set_event (ts->tx_fifo)) - session_send_io_evt_to_thread (ts->tx_fifo, SESSION_IO_EVT_TX); + session_program_tx_io_evt (ts->handle, SESSION_IO_EVT_TX); return 0; } |