From 25490a300910e357ac864a1916a4285e239fbf30 Mon Sep 17 00:00:00 2001 From: Garrett D'Amore Date: Sat, 29 Sep 2018 21:55:31 -0700 Subject: fixes #738 http server needs a way to collect request entity data --- docs/man/libnng.3.adoc | 1 + docs/man/nng_http_handler_alloc.3http.adoc | 1 + docs/man/nng_http_handler_collect_body.3http.adoc | 78 ++++++++++++++++++ src/supplemental/http/http.h | 10 +++ src/supplemental/http/http_api.h | 6 ++ src/supplemental/http/http_msg.c | 11 +++ src/supplemental/http/http_public.c | 11 +++ src/supplemental/http/http_server.c | 99 ++++++++++++++++++----- tests/httpserver.c | 92 +++++++++++++++++++++ 9 files changed, 289 insertions(+), 20 deletions(-) create mode 100644 docs/man/nng_http_handler_collect_body.3http.adoc diff --git a/docs/man/libnng.3.adoc b/docs/man/libnng.3.adoc index 0a62f931..25e8b08d 100644 --- a/docs/man/libnng.3.adoc +++ b/docs/man/libnng.3.adoc @@ -356,6 +356,7 @@ These functions are intended for use with HTTP server applications. |=== |<>|allocate HTTP server handler +|<>|set HTTP handler to collect request body |<>|free HTTP server handler |<>|return extra data for HTTP handler |<>|set extra data for HTTP handler diff --git a/docs/man/nng_http_handler_alloc.3http.adoc b/docs/man/nng_http_handler_alloc.3http.adoc index 523b5328..d5861d14 100644 --- a/docs/man/nng_http_handler_alloc.3http.adoc +++ b/docs/man/nng_http_handler_alloc.3http.adoc @@ -149,6 +149,7 @@ This function returns 0 on success, and non-zero otherwise. <>, <>, <>, +<>, <>, <>, <>, diff --git a/docs/man/nng_http_handler_collect_body.3http.adoc b/docs/man/nng_http_handler_collect_body.3http.adoc new file mode 100644 index 00000000..875f32c4 --- /dev/null +++ b/docs/man/nng_http_handler_collect_body.3http.adoc @@ -0,0 +1,78 @@ += nng_http_handler_collect_body(3http) +// +// Copyright 2018 Staysail Systems, Inc. +// Copyright 2018 Capitar IT Group BV +// +// This document is supplied under the terms of the MIT License, a +// copy of which should be located in the distribution where this +// file was obtained (LICENSE.txt). A copy of the license may also be +// found online at https://opensource.org/licenses/MIT. +// + +== NAME + +nng_http_handler_collect_body - set HTTP handler to collect request body + +== SYNOPSIS + +[source, c] +---- +#include +#include + +int nng_http_handler_collect_body(nng_http_handler *handler, bool want, size_t maxsz); +---- + +== DESCRIPTION + +The `nng_http_handler_collect_data()` function causes the _handler_ to +collect any request body that was submitted with the request, and attach +it to the `nng_http_req` before the handler is called. + +Subsequently the data can be retrieved by the handler from the request with the +`<>` function. + +The collection is enabled if _want_ is true. +Furthermore, the data that the client may sent is limited by the +value of _maxsz_. +If the client attempts to send more data than _maxsz_, then the +request will be terminated with a 400 "`Bad Request`" status. + +TIP: Limiting the size of incoming request data can provide protection +against denial of service attacks, as a buffer of the client-supplied +size must be allocated to receive the data. + +In order to provide an unlimited size, use `(size_t)-1` for _maxsz_. +The value `0` for _maxsz_ can be used to prevent any data from being passed +by the client. + +The built-in handlers for files, directories, and static data limit the +_maxsz_ to zero by default. +Otherwise the default setting is to enable this capability with a default +value of _maxsz_ of 1 megabyte. + +NOTE: The handler looks for data indicated by the `Content-Length:` HTTP +header. +If this header is absent, the request is assumed not to contain any data. + +NOTE: This specifically does not support the `Chunked` transfer-encoding. +This is considered a bug, and is a deficiency for full HTTP/1.1 compliance. +However, few clients send data in this format, so in practice this should +not create few limitations. + +== RETURN VALUES + +This function returns 0 on success, and non-zero otherwise. + +== ERRORS + +[horizontal] +`NNG_ENOTSUP`:: No support for HTTP in the library. + +== SEE ALSO + +[.text-left] +<>, +<>, +<>, +<> diff --git a/src/supplemental/http/http.h b/src/supplemental/http/http.h index cffd1a8b..d9edd5c8 100644 --- a/src/supplemental/http/http.h +++ b/src/supplemental/http/http.h @@ -350,6 +350,16 @@ NNG_DECL int nng_http_handler_set_method(nng_http_handler *, const char *); // that case is not considered.) NNG_DECL int nng_http_handler_set_host(nng_http_handler *, const char *); +// nng_http_handler_collect_body is used to indicate the server should +// check for, and process, data sent by the client, which will be attached +// to the request. If this is false, then the handler will need to check +// for and process any content data. By default the server will accept +// up to 1MB. If the client attempts to send more data than requested, +// then a 400 Bad Request will be sent back to the client. To set an +// unlimited value, use (size_t)-1. To preclude the client from sending +// *any* data, use 0. (The static and file handlers use 0 by default.) +NNG_DECL int nng_http_handler_collect_body(nng_http_handler *, bool, size_t); + // nng_http_handler_set_tree indicates that the handler is being registered // for a heirarchical tree, rather than just a single path, so it will be // called for all child paths supplied. By default the handler is only diff --git a/src/supplemental/http/http_api.h b/src/supplemental/http/http_api.h index 71b24f54..a30399d2 100644 --- a/src/supplemental/http/http_api.h +++ b/src/supplemental/http/http_api.h @@ -101,6 +101,7 @@ extern int nni_http_req_copy_data(nni_http_req *, const void *, size_t); extern int nni_http_res_copy_data(nni_http_res *, const void *, size_t); extern int nni_http_req_set_data(nni_http_req *, const void *, size_t); extern int nni_http_res_set_data(nni_http_res *, const void *, size_t); +extern int nni_http_req_alloc_data(nni_http_req *, size_t); extern int nni_http_res_alloc_data(nni_http_res *, size_t); extern const char *nni_http_req_get_method(nni_http_req *); extern const char *nni_http_req_get_version(nni_http_req *); @@ -248,6 +249,11 @@ extern int nni_http_handler_init_static( // calls this for any handlers still registered with it if it is destroyed. extern void nni_http_handler_fini(nni_http_handler *); +// nni_http_handler_collect_body informs the server that it should collect +// the entitty data associated with the client request, and sets the maximum +// size to accept. +extern void nni_http_handler_collect_body(nni_http_handler *, bool, size_t); + // nni_http_handler_set_tree marks the handler as servicing the entire // tree (e.g. a directory), rather than just a leaf node. The handler // will probably need to inspect the URL of the request. diff --git a/src/supplemental/http/http_msg.c b/src/supplemental/http/http_msg.c index 6d7e9f8a..3b78a2a9 100644 --- a/src/supplemental/http/http_msg.c +++ b/src/supplemental/http/http_msg.c @@ -381,6 +381,17 @@ nni_http_req_copy_data(nni_http_req *req, const void *data, size_t size) return (0); } +int +nni_http_req_alloc_data(nni_http_req *req, size_t size) +{ + int rv; + + if ((rv = http_entity_alloc_data(&req->data, size)) != 0) { + return (rv); + } + return (0); +} + int nni_http_res_copy_data(nni_http_res *res, const void *data, size_t size) { diff --git a/src/supplemental/http/http_public.c b/src/supplemental/http/http_public.c index 389b74c8..b86521b0 100644 --- a/src/supplemental/http/http_public.c +++ b/src/supplemental/http/http_public.c @@ -586,6 +586,17 @@ nng_http_handler_set_method(nng_http_handler *h, const char *meth) #endif } +int +nng_http_handler_collect_body(nng_http_handler *h, bool want, size_t len) +{ +#ifdef NNG_SUPP_HTTP + nni_http_handler_collect_body(h, want, len); + return (0); +#else + return (NNG_ENOTSUP); +#endif +} + int nng_http_handler_set_host(nng_http_handler *h, const char *host) { diff --git a/src/supplemental/http/http_server.c b/src/supplemental/http/http_server.c index f36c941d..1a7f2c25 100644 --- a/src/supplemental/http/http_server.c +++ b/src/supplemental/http/http_server.c @@ -35,25 +35,28 @@ struct nng_http_handler { char * host; bool tree; int refcnt; + size_t maxbody; + bool getbody; void * data; nni_cb dtor; void (*cb)(nni_aio *); }; typedef struct http_sconn { - nni_list_node node; - nni_http_conn * conn; - nni_http_server *server; - nni_http_req * req; - nni_http_res * res; - bool close; - bool closed; - bool finished; - nni_aio * cbaio; - nni_aio * rxaio; - nni_aio * txaio; - nni_aio * txdataio; - nni_reap_item reap; + nni_list_node node; + nni_http_conn * conn; + nni_http_server * server; + nni_http_req * req; + nni_http_res * res; + nni_http_handler *handler; // set if we deferred to read body + bool close; + bool closed; + bool finished; + nni_aio * cbaio; + nni_aio * rxaio; + nni_aio * txaio; + nni_aio * txdataio; + nni_reap_item reap; } http_sconn; typedef struct http_error { @@ -101,13 +104,15 @@ nni_http_handler_init( return (NNG_ENOMEM); } NNI_LIST_NODE_INIT(&h->node); - h->cb = cb; - h->data = NULL; - h->dtor = NULL; - h->host = NULL; - h->tree = false; - h->refcnt = 0; - *hp = h; + h->cb = cb; + h->data = NULL; + h->dtor = NULL; + h->host = NULL; + h->tree = false; + h->refcnt = 0; + h->maxbody = 1024 * 1024; // By default we accept up to 1MB of body + h->getbody = true; + *hp = h; return (0); } @@ -126,6 +131,13 @@ nni_http_handler_fini(nni_http_handler *h) NNI_FREE_STRUCT(h); } +void +nni_http_handler_collect_body(nni_http_handler *h, bool want, size_t maxbody) +{ + h->getbody = want; + h->maxbody = maxbody; +} + int nni_http_handler_set_data(nni_http_handler *h, void *data, nni_cb dtor) { @@ -292,6 +304,7 @@ http_sconn_txdatdone(void *arg) return; } + sc->handler = NULL; nni_http_req_reset(sc->req); nni_http_read_req(sc->conn, sc->req, sc->rxaio); } @@ -316,6 +329,7 @@ http_sconn_txdone(void *arg) nni_http_res_free(sc->res); sc->res = NULL; } + sc->handler = NULL; nni_http_req_reset(sc->req); nni_http_read_req(sc->conn, sc->req, sc->rxaio); } @@ -457,12 +471,18 @@ http_sconn_rxdone(void *arg) bool badmeth = false; bool needhost = false; const char * host; + const char * cls; if ((rv = nni_aio_result(aio)) != 0) { http_sconn_close(sc); return; } + if ((h = sc->handler) != NULL) { + nni_mtx_lock(&s->mtx); + goto finish; + } + // Validate the request -- it has to at least look like HTTP // 1.x. We flatly refuse to deal with HTTP 0.9, and we can't // cope with HTTP/2. @@ -594,6 +614,35 @@ http_sconn_rxdone(void *arg) return; } + if ((h->getbody) && + ((cls = nni_http_req_get_header(req, "Content-Length")) != NULL)) { + uint64_t len; + + if ((nni_strtou64(cls, &len) != 0) || (len > h->maxbody)) { + nni_mtx_unlock(&s->mtx); + http_sconn_error(sc, NNG_HTTP_STATUS_BAD_REQUEST); + return; + } + if (len > 0) { + nng_iov iov; + if ((nni_http_req_alloc_data(req, (size_t) len)) != + 0) { + nni_mtx_unlock(&s->mtx); + http_sconn_error( + sc, NNG_HTTP_STATUS_INTERNAL_SERVER_ERROR); + return; + } + nng_http_req_get_data(req, &iov.iov_buf, &iov.iov_len); + sc->handler = h; + nni_mtx_unlock(&s->mtx); + nni_aio_set_iov(sc->rxaio, 1, &iov); + nni_http_read_full(sc->conn, aio); + return; + } + } + +finish: + sc->handler = NULL; nni_aio_set_input(sc->cbaio, 0, sc->req); nni_aio_set_input(sc->cbaio, 1, h); nni_aio_set_input(sc->cbaio, 2, sc->conn); @@ -671,6 +720,7 @@ http_sconn_cbdone(void *arg) } else { // Presumably client already sent a response. // Wait for another request. + sc->handler = NULL; nni_http_req_reset(sc->req); nni_http_read_req(sc->conn, sc->req, sc->rxaio); } @@ -747,6 +797,7 @@ http_server_acccb(void *arg) sc->server = s; nni_list_append(&s->conns, sc); + sc->handler = NULL; nni_http_read_req(sc->conn, sc->req, sc->rxaio); nni_tcp_listener_accept(s->listener, s->accaio); nni_mtx_unlock(&s->mtx); @@ -1345,6 +1396,9 @@ nni_http_handler_init_file_ctype(nni_http_handler **hpp, const char *uri, return (rv); } + // We don't permit a body for getting a file. + nni_http_handler_collect_body(h, true, 0); + *hpp = h; return (0); } @@ -1505,6 +1559,8 @@ nni_http_handler_init_directory( http_file_free(hf); return (rv); } + // We don't permit a body for getting a file. + nni_http_handler_collect_body(h, true, 0); if (((rv = nni_http_handler_set_tree(h)) != 0) || ((rv = nni_http_handler_set_data(h, hf, http_file_free)) != 0)) { @@ -1596,6 +1652,9 @@ nni_http_handler_init_static(nni_http_handler **hpp, const char *uri, return (rv); } + // We don't permit a body for getting static data. + nni_http_handler_collect_body(h, true, 0); + *hpp = h; return (0); } diff --git a/tests/httpserver.c b/tests/httpserver.c index a36901f5..21b961b8 100644 --- a/tests/httpserver.c +++ b/tests/httpserver.c @@ -158,6 +158,30 @@ fail: return (rv); } +static void +httpecho(nng_aio *aio) +{ + nng_http_req *req = nng_aio_get_input(aio, 0); + nng_http_res *res; + int rv; + void * body; + size_t len; + + nng_http_req_get_data(req, &body, &len); + + if (((rv = nng_http_res_alloc(&res)) != 0) || + ((rv = nng_http_res_copy_data(res, body, len)) != 0) || + ((rv = nng_http_res_set_header( + res, "Content-type", "text/plain")) != 0) || + ((rv = nng_http_res_set_status(res, NNG_HTTP_STATUS_OK)) != 0)) { + nng_http_res_free(res); + nng_aio_finish(aio, rv); + return; + } + nng_aio_set_output(aio, 0, res); + nng_aio_finish(aio, 0); +} + TestMain("HTTP Server", { nng_http_server * s; nng_http_handler *h; @@ -458,4 +482,72 @@ TestMain("HTTP Server", { nng_url_free(curl); }); }); + Convey("Custom POST handler works", { + char urlstr[32]; + nng_url *url; + + trantest_next_address(urlstr, "http://127.0.0.1:%u"); + So(nng_url_parse(&url, urlstr) == 0); + So(nng_http_server_hold(&s, url) == 0); + + Reset({ + nng_http_server_release(s); + nng_url_free(url); + }); + + So(nng_http_handler_alloc(&h, "/post", httpecho) == 0); + So(nng_http_handler_set_method(h, "POST") == 0); + So(nng_http_server_add_handler(s, h) == 0); + So(nng_http_server_start(s) == 0); + + nng_msleep(100); + + Convey("Echo POST works", { + char fullurl[256]; + size_t size; + nng_http_req *req; + nng_http_res *res; + nng_url * curl; + char txdata[5]; + char * rxdata; + + snprintf(txdata, sizeof(txdata), "1234"); + So(nng_http_res_alloc(&res) == 0); + snprintf(fullurl, sizeof(fullurl), "%s/post", urlstr); + So(nng_url_parse(&curl, fullurl) == 0); + So(nng_http_req_alloc(&req, curl) == 0); + nng_http_req_set_data(req, txdata, strlen(txdata)); + So(nng_http_req_set_method(req, "POST") == 0); + So(httpdo(curl, req, res, (void **) &rxdata, &size) == + 0); + So(nng_http_res_get_status(res) == NNG_HTTP_STATUS_OK); + So(size == strlen(txdata)); + So(strncmp(txdata, rxdata, size) == 0); + nng_http_req_free(req); + nng_http_res_free(res); + nng_url_free(curl); + }); + + Convey("Get method gives 405", { + char fullurl[256]; + void * data; + size_t size; + nng_http_req *req; + nng_http_res *res; + nng_url * curl; + + So(nng_http_res_alloc(&res) == 0); + snprintf(fullurl, sizeof(fullurl), "%s/post", urlstr); + So(nng_url_parse(&curl, fullurl) == 0); + So(nng_http_req_alloc(&req, curl) == 0); + So(nng_http_req_set_method(req, "GET") == 0); + + So(httpdo(curl, req, res, &data, &size) == 0); + So(nng_http_res_get_status(res) == + NNG_HTTP_STATUS_METHOD_NOT_ALLOWED); + nng_http_req_free(req); + nng_http_res_free(res); + nng_url_free(curl); + }); + }); }) -- cgit v1.2.3-70-g09d2