From 127e53cf98efe7340f8762e31112ae80110d78c5 Mon Sep 17 00:00:00 2001 From: Andy Green Date: Mon, 7 Oct 2019 14:28:18 +0100 Subject: [PATCH] client: multipart mime generation helpers lws has been able to generate client multipart mime as shown in minimal-http-client-post, but it requires a lot of user boilerplate to handle the boundary, related transaction header, and multipart headers. This patch adds a client creation flag to indicate it will carry multipart mime, which autocreates the boundary string and applies the transaction header with it, and an api to form the boundary headers between the different mime parts and the terminating boundary. --- include/libwebsockets/lws-client.h | 22 +++++- lib/misc/base64-decode.c | 8 +- lib/roles/h2/http2.c | 9 ++- lib/roles/http/client/client-http.c | 75 ++++++++++++++++++- lib/roles/http/private-lib-roles-http.h | 9 ++- lib/roles/http/server/lws-spa.c | 3 +- .../minimal-http-client-post.c | 72 ++++-------------- 7 files changed, 131 insertions(+), 67 deletions(-) diff --git a/include/libwebsockets/lws-client.h b/include/libwebsockets/lws-client.h index 0c7839ef6..41f6569c4 100644 --- a/include/libwebsockets/lws-client.h +++ b/include/libwebsockets/lws-client.h @@ -43,6 +43,7 @@ enum lws_client_connect_ssl_connection_flags { LCCSCF_H2_QUIRK_NGHTTP2_END_STREAM = (1 << 5), LCCSCF_H2_QUIRK_OVERFLOWS_TXCR = (1 << 6), LCCSCF_H2_AUTH_BEARER = (1 << 7), + LCCSCF_HTTP_MULTIPART_MIME = (1 << 8), LCCSCF_PIPELINE = (1 << 16), /**< Serialize / pipeline multiple client connections @@ -244,7 +245,7 @@ lws_tls_client_vhost_extra_cert_mem(struct lws_vhost *vh, const uint8_t *der, size_t der_len); /** - * lws_client_http_body_pending() - control if client connection neeeds to send body + * lws_client_http_body_pending() - control if client connection needs to send body * * \param wsi: client connection * \param something_left_to_send: nonzero if need to send more body, 0 (default) @@ -265,4 +266,23 @@ lws_tls_client_vhost_extra_cert_mem(struct lws_vhost *vh, LWS_VISIBLE LWS_EXTERN void lws_client_http_body_pending(struct lws *wsi, int something_left_to_send); +/** + * lws_client_http_multipart() - issue appropriate multipart header or trailer + * + * \param wsi: client connection + * \param name: multipart header name field, or NULL if end of multipart + * \param filename: multipart header filename field, or NULL if none + * \param content_type: multipart header content-type part, or NULL if none + * \param p: pointer to position in buffer + * \param end: end of buffer + * + * This issues a multipart mime boundary, or terminator if name = NULL. + * + * Returns 0 if OK or nonzero if couldn't fit in buffer + */ +LWS_VISIBLE LWS_EXTERN int +lws_client_http_multipart(struct lws *wsi, const char *name, + const char *filename, const char *content_type, + char **p, char *end); + ///@} diff --git a/lib/misc/base64-decode.c b/lib/misc/base64-decode.c index 4bd9e0102..3a88c84ee 100644 --- a/lib/misc/base64-decode.c +++ b/lib/misc/base64-decode.c @@ -71,10 +71,10 @@ _lws_b64_encode_string(const char *encode, const char *in, int in_len, return -1; *out++ = encode[triple[0] >> 2]; - *out++ = encode[((triple[0] & 0x03) << 4) | - ((triple[1] & 0xf0) >> 4)]; - *out++ = (len > 1 ? encode[((triple[1] & 0x0f) << 2) | - ((triple[2] & 0xc0) >> 6)] : '='); + *out++ = encode[(((triple[0] & 0x03) << 4) & 0x30) | + (((triple[1] & 0xf0) >> 4) & 0x0f)]; + *out++ = (len > 1 ? encode[(((triple[1] & 0x0f) << 2) & 0x3c) | + (((triple[2] & 0xc0) >> 6) & 3)] : '='); *out++ = (len > 2 ? encode[triple[2] & 0x3f] : '='); done += 4; diff --git a/lib/roles/h2/http2.c b/lib/roles/h2/http2.c index a625368b2..5bb65b0cb 100644 --- a/lib/roles/h2/http2.c +++ b/lib/roles/h2/http2.c @@ -2203,7 +2203,7 @@ int lws_h2_client_handshake(struct lws *wsi) { struct lws_context_per_thread *pt = &wsi->context->pt[(int)wsi->tsi]; - uint8_t *buf, *start, *p, *end, *q; + uint8_t *buf, *start, *p, *p1, *end, *q; char *meth = lws_hdr_simple_ptr(wsi, _WSI_TOKEN_CLIENT_METHOD), *uri = lws_hdr_simple_ptr(wsi, _WSI_TOKEN_CLIENT_URI); struct lws *nwsi = lws_get_network_wsi(wsi); @@ -2289,6 +2289,13 @@ lws_h2_client_handshake(struct lws *wsi) &p, end)) goto fail_length; + if (wsi->flags & LCCSCF_HTTP_MULTIPART_MIME) { + p1 = lws_http_multipart_headers(wsi, p); + if (!p1) + goto fail_length; + p = p1; + } + if (wsi->flags & LCCSCF_H2_AUTH_BEARER) { uint8_t *qend = q + (wsi->context->pt_serv_buf_size / 2) - 1; diff --git a/lib/roles/http/client/client-http.c b/lib/roles/http/client/client-http.c index a57853ab9..5235439f4 100644 --- a/lib/roles/http/client/client-http.c +++ b/lib/roles/http/client/client-http.c @@ -1040,13 +1040,75 @@ bail2: } #endif +/* + * set the boundary string and the content-type for client multipart mime + */ + +uint8_t * +lws_http_multipart_headers(struct lws *wsi, uint8_t *p) +{ + char buf[10], arg[48]; + int n; + + lws_get_random(wsi->context, (uint8_t *)buf, sizeof(buf)); + lws_b64_encode_string(buf, sizeof(buf), + wsi->http.multipart_boundary, + sizeof(wsi->http.multipart_boundary)); + + n = lws_snprintf(arg, sizeof(arg), "multipart/form-data;boundary=%s", + wsi->http.multipart_boundary); + + if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, + (uint8_t *)arg, n, &p, p + 100)) + return NULL; + + wsi->http.multipart = wsi->http.multipart_issue_boundary = 1; + + return p; +} + +int +lws_client_http_multipart(struct lws *wsi, const char *name, + const char *filename, const char *content_type, + char **p, char *end) +{ + /* + * Client conn must have been created with LCCSCF_HTTP_MULTIPART_MIME + * flag to use this api + */ + assert(wsi->http.multipart); + + if (!name) { + *p += lws_snprintf((char *)(*p), lws_ptr_diff(end, p), + "\xd\xa--%s--\xd\xa", + wsi->http.multipart_boundary); + + return 0; + } + + *p += lws_snprintf((char *)(*p), lws_ptr_diff(end, p), "\xd\xa--%s\xd\xa" + "Content-Disposition: form-data; " + "name=\"%s\"", + wsi->http.multipart_boundary, name); + if (filename) + *p += lws_snprintf((char *)(*p), lws_ptr_diff(end, p), + "; filename=\"%s\"", filename); + + if (content_type) + *p += lws_snprintf((char *)(*p), lws_ptr_diff(end, p), "\xd\xa" + "Content-Type: %s", content_type); + + *p += lws_snprintf((char *)(*p), lws_ptr_diff(end, p), "\xd\xa\xd\xa"); + + return *p == end; +} + char * lws_generate_client_handshake(struct lws *wsi, char *pkt) { - char *p = pkt; - const char *meth; - const char *pp = lws_hdr_simple_ptr(wsi, + const char *meth, *pp = lws_hdr_simple_ptr(wsi, _WSI_TOKEN_CLIENT_SENT_PROTOCOLS); + char *p = pkt, *p1; meth = lws_hdr_simple_ptr(wsi, _WSI_TOKEN_CLIENT_METHOD); if (!meth) { @@ -1119,6 +1181,13 @@ lws_generate_client_handshake(struct lws *wsi, char *pkt) _WSI_TOKEN_CLIENT_ORIGIN)); } + if (wsi->flags & LCCSCF_HTTP_MULTIPART_MIME) { + p1 = (char *)lws_http_multipart_headers(wsi, (uint8_t *)p); + if (!p1) + return NULL; + p = p1; + } + #if defined(LWS_WITH_HTTP_PROXY) if (wsi->parent && lws_hdr_total_length(wsi->parent, WSI_TOKEN_HTTP_CONTENT_LENGTH)) { diff --git a/lib/roles/http/private-lib-roles-http.h b/lib/roles/http/private-lib-roles-http.h index 50a332d6e..e9ce4e5c1 100644 --- a/lib/roles/http/private-lib-roles-http.h +++ b/lib/roles/http/private-lib-roles-http.h @@ -233,7 +233,9 @@ struct _lws_http_mode_related { lws_filepos_t filelen; lws_fop_fd_t fop_fd; #endif - +#if defined(LWS_WITH_CLIENT) + char multipart_boundary[16]; +#endif #if defined(LWS_WITH_RANGES) struct lws_range_parsing range; char multipart_content_type[64]; @@ -266,6 +268,8 @@ struct _lws_http_mode_related { unsigned int deferred_transaction_completed:1; unsigned int content_length_explicitly_zero:1; unsigned int did_stream_close:1; + unsigned int multipart:1; + unsigned int multipart_issue_boundary:1; }; @@ -313,3 +317,6 @@ lws_http_proxy_start(struct lws *wsi, const struct lws_http_mount *hit, void lws_sul_http_ah_lifecheck(lws_sorted_usec_list_t *sul); + +uint8_t * +lws_http_multipart_headers(struct lws *wsi, uint8_t *p); diff --git a/lib/roles/http/server/lws-spa.c b/lib/roles/http/server/lws-spa.c index d45e54f76..73a047d7d 100644 --- a/lib/roles/http/server/lws-spa.c +++ b/lib/roles/http/server/lws-spa.c @@ -186,7 +186,8 @@ lws_urldecode_s_process(struct lws_urldecode_stateful *s, const char *in, continue; } if (s->pos >= (int)sizeof(s->name) - 1) { - lwsl_notice("Name too long\n"); + lwsl_hexdump_notice(s->name, s->pos); + lwsl_notice("Name too long...\n"); return -1; } s->name[s->pos++] = *in++; diff --git a/minimal-examples/http-client/minimal-http-client-post/minimal-http-client-post.c b/minimal-examples/http-client/minimal-http-client-post/minimal-http-client-post.c index d6b080a52..3f139f1e1 100644 --- a/minimal-examples/http-client/minimal-http-client-post/minimal-http-client-post.c +++ b/minimal-examples/http-client/minimal-http-client-post/minimal-http-client-post.c @@ -21,7 +21,6 @@ static int interrupted, bad = 0, status, count_clients = 1, completed; static struct lws *client_wsi[4]; struct pss { - char boundary[32]; char body_part; }; @@ -31,9 +30,7 @@ callback_http(struct lws *wsi, enum lws_callback_reasons reason, { struct pss *pss = (struct pss *)user; char buf[LWS_PRE + 1024], *start = &buf[LWS_PRE], *p = start, - *end = &buf[sizeof(buf) - 1]; - uint8_t **up, *uend; - uint32_t r; + *end = &buf[sizeof(buf) - LWS_PRE - 1]; int n; switch (reason) { @@ -96,29 +93,7 @@ callback_http(struct lws *wsi, enum lws_callback_reasons reason, /* ...callbacks related to generating the POST... */ case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER: - lwsl_user("LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER\n"); - up = (uint8_t **)in; - uend = *up + len - 1; - - /* generate a random boundary string */ - - lws_get_random(lws_get_context(wsi), &r, sizeof(r)); - lws_snprintf(pss->boundary, sizeof(pss->boundary) - 1, - "---boundary-%08x", r); - - n = lws_snprintf(buf, sizeof(buf) - 1, - "multipart/form-data; boundary=%s", pss->boundary); - if (lws_add_http_header_by_token(wsi, - WSI_TOKEN_HTTP_CONTENT_TYPE, - (uint8_t *)buf, n, up, uend)) - return 1; /* - * Notice because we are sending multipart/form-data we can - * usually rely on the server to understand where the form - * payload ends without having to give it an overall - * content-length (which can be troublesome to compute ahead - * of generating the data to send). - * * Tell lws we are going to send the body next... */ lws_client_http_body_pending(wsi, 1); @@ -138,28 +113,25 @@ callback_http(struct lws *wsi, enum lws_callback_reasons reason, switch (pss->body_part++) { case 0: + if (lws_client_http_multipart(wsi, "text", NULL, NULL, + &p, end)) + return -1; /* notice every usage of the boundary starts with -- */ - p += lws_snprintf(p, end - p, "--%s\xd\xa" - "content-disposition: " - "form-data; name=\"text\"\xd\xa" - "\xd\xa" - "my text field" - "\xd\xa", pss->boundary); + p += lws_snprintf(p, end - p, "my text field\xd\xa"); break; case 1: + if (lws_client_http_multipart(wsi, "file", "myfile.txt", + "text/plain", &p, end)) + return -1; p += lws_snprintf(p, end - p, - "--%s\xd\xa" - "content-disposition: form-data; name=\"file\";" - "filename=\"myfile.txt\"\xd\xa" - "content-type: text/plain\xd\xa" - "\xd\xa" "This is the contents of the " "uploaded file.\xd\xa" - "\xd\xa", pss->boundary); + "\xd\xa"); break; case 2: - p += lws_snprintf(p, end - p, "--%s--\xd\xa", - pss->boundary); + if (lws_client_http_multipart(wsi, NULL, NULL, NULL, + &p, end)) + return -1; lws_client_http_body_pending(wsi, 0); /* necessary to support H2, it means we will write no * more on this stream */ @@ -211,29 +183,17 @@ int main(int argc, const char **argv) struct lws_context_creation_info info; struct lws_client_connect_info i; struct lws_context *context; - const char *p; - int n = 0, logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE - /* - * For LLL_ verbosity above NOTICE to be built into lws, - * lws must have been configured and built with - * -DCMAKE_BUILD_TYPE=DEBUG instead of =RELEASE - * - * | LLL_INFO | LLL_PARSER | LLL_HEADER | LLL_EXT | - * LLL_CLIENT | LLL_LATENCY | LLL_DEBUG - */ ; + int n = 0; signal(SIGINT, sigint_handler); - if ((p = lws_cmdline_option(argc, argv, "-d"))) - logs = atoi(p); - - lws_set_log_level(logs, NULL); + memset(&info, 0, sizeof info); /* otherwise uninitialized garbage */ + lws_cmdline_option_handle_builtin(argc, argv, &info); lwsl_user("LWS minimal http client - POST [-d] [-l] [--h1]\n"); if (lws_cmdline_option(argc, argv, "-m")) count_clients = LWS_ARRAY_SIZE(client_wsi); - memset(&info, 0, sizeof info); /* otherwise uninitialized garbage */ info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; info.port = CONTEXT_PORT_NO_LISTEN; /* we do not run any server */ info.protocols = protocols; @@ -263,7 +223,7 @@ int main(int argc, const char **argv) memset(&i, 0, sizeof i); /* otherwise uninitialized garbage */ i.context = context; - i.ssl_connection = LCCSCF_USE_SSL; + i.ssl_connection = LCCSCF_USE_SSL | LCCSCF_HTTP_MULTIPART_MIME; if (lws_cmdline_option(argc, argv, "-l")) { i.port = 7681;