From 0cc96c69b3fce09a9c4a8d467d17cfbda76bc02f Mon Sep 17 00:00:00 2001 From: Garrett D'Amore Date: Thu, 30 Aug 2018 14:23:39 -0700 Subject: [PATCH] fixes #681 HTTP convenience GET method desired... This adds a couple of new methods, and related documentation and test cases. --- docs/man/CMakeLists.txt | 4 + docs/man/libnng.3.adoc | 16 +- docs/man/nng_http_client_transact.3http.adoc | 89 +++++++ docs/man/nng_http_conn_read_res.3http.adoc | 8 + docs/man/nng_http_conn_transact.3http.adoc | 98 +++++++ docs/man/nng_http_conn_write_req.3http.adoc | 8 + docs/man/nng_http_conn_write_res.3http.adoc | 8 + docs/man/nng_http_req_get_data.3http.adoc | 50 ++++ docs/man/nng_http_res_get_data.3http.adoc | 50 ++++ src/supplemental/http/http.h | 22 ++ src/supplemental/http/http_api.h | 17 ++ src/supplemental/http/http_client.c | 254 +++++++++++++++++++ src/supplemental/http/http_msg.c | 14 + src/supplemental/http/http_public.c | 56 ++++ tests/httpclient.c | 107 +++++++- 15 files changed, 793 insertions(+), 8 deletions(-) create mode 100644 docs/man/nng_http_client_transact.3http.adoc create mode 100644 docs/man/nng_http_conn_transact.3http.adoc create mode 100644 docs/man/nng_http_req_get_data.3http.adoc create mode 100644 docs/man/nng_http_res_get_data.3http.adoc diff --git a/docs/man/CMakeLists.txt b/docs/man/CMakeLists.txt index af644a626..21651ba5b 100644 --- a/docs/man/CMakeLists.txt +++ b/docs/man/CMakeLists.txt @@ -198,11 +198,13 @@ if (NNG_ENABLE_DOC) nng_http_client_free nng_http_client_get_tls nng_http_client_set_tls + nng_http_client_transact nng_http_conn_close nng_http_conn_read nng_http_conn_read_all nng_http_conn_read_req nng_http_conn_read_res + nng_http_conn_transact nng_http_conn_write nng_http_conn_write_all nng_http_conn_write_req @@ -220,6 +222,7 @@ if (NNG_ENABLE_DOC) nng_http_req_copy_data nng_http_req_del_header nng_http_req_free + nng_http_req_get_data nng_http_req_get_header nng_http_req_get_method nng_http_req_get_uri @@ -235,6 +238,7 @@ if (NNG_ENABLE_DOC) nng_http_res_copy_data nng_http_res_del_header nng_http_res_free + nng_http_res_get_data nng_http_res_get_header nng_http_res_get_reason nng_http_res_get_status diff --git a/docs/man/libnng.3.adoc b/docs/man/libnng.3.adoc index 233069979..3e3037f90 100644 --- a/docs/man/libnng.3.adoc +++ b/docs/man/libnng.3.adoc @@ -287,6 +287,7 @@ and connections. |<>|copy HTTP request body |<>|delete HTTP request header |<>|free HTTP request structure +|<>|get HTTP request body |<>|return HTTP request header |<>|return HTTP request method |<>|return HTTP request URI @@ -302,11 +303,12 @@ and connections. |<>|copy HTTP response body |<>|delete HTTP response header |<>|free HTTP response structure -|<>|set HTTP response body +|<>|get HTTP response body |<>|return HTTP response header |<>|return HTTP response reason |<>|return HTTP response status |<>|return HTTP response protocol version +|<>|set HTTP response body |<>|set HTTP response header |<>|set HTTP response reason |<>|set HTTP response status @@ -318,11 +320,13 @@ and connections. These functions are intended for use with HTTP client applications. |=== -| <>|allocate HTTP client -| <>|establish HTTP client connection -| <>|free HTTP client -| <>|get HTTP client TLS configuration -| <>|set HTTP client TLS configuration +|<>|allocate HTTP client +|<>|establish HTTP client connection +|<>|free HTTP client +|<>|get HTTP client TLS configuration +|<>|set HTTP client TLS configuration +|<>|perform one HTTP transaction +|<>|perform one HTTP transaction on connection |=== ==== HTTP Server Functions diff --git a/docs/man/nng_http_client_transact.3http.adoc b/docs/man/nng_http_client_transact.3http.adoc new file mode 100644 index 000000000..a469dc508 --- /dev/null +++ b/docs/man/nng_http_client_transact.3http.adoc @@ -0,0 +1,89 @@ += nng_http_client_transact(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_client_transact - perform one HTTP transaction + +== SYNOPSIS + +[source, c] +---- +#include +#include + +void nng_http_client_transact(nng_http_client *client, nng_http_req *req, + nng_http_res *res, nng_aio *aio); +---- + +== DESCRIPTION + +The `nng_http_client_transact()` function is used to perform a complete +HTTP exchange. +It creates a new connection using _client_, performs the transaction by +sending the request _req_ +(and attached body data) to the remote server, then reading the response +_res_, and finally closes the connection that it created. +The entire response is read, including any associated body, which can +subsequently be obtained using +`<>`. + +This function is intended to make creation of client applications easier, +by performing multiple asynchronous operations required to complete an +entire HTTP transaction. + +A similar function, +`<>`, +exists. +That function behaves similarily, but uses an existing connection, which +can be reused. + +NOTE: This function does not support reading data sent using chunked +transfer encoding, and if the server attempts to do so, the underlying +connection will be closed and an `NNG_ENOTSUP` error will be returned. +This limitation is considered a bug, and a fix is planned for the future. + +WARNING: If the remote server tries to send an extremely large buffer, +then a corresponding allocation will be made, which can lead to denial +of service attacks. +Client applications should take care to use this only with reasonably +trust-worthy servers. + +This function returns immediately, with no return value. +Completion of the operation is signaled via the _aio_, and the final result +may be obtained via `<>`. +That result will either be zero or an error code. + +== RETURN VALUES + +None. + +== ERRORS + +[horizontal] +`NNG_ECANCELED`:: The operation was canceled. +`NNG_ECLOSED`:: The connection was closed. +`NNG_ECONNRESET`:: The peer closed the connection. +`NNG_ENOMEM`:: Insufficient free memory to perform the operation. +`NNG_ENOTSUP`:: HTTP operations are not supported, or peer sent chunked encoding. +`NNG_EPROTO`:: An HTTP protocol error occurred. +`NNG_ETIMEDOUT`:: Timeout waiting for data from the connection. + +== SEE ALSO + +[.text-left] +<>, +<>, +<>, +<>, +<>, +<>, +<> diff --git a/docs/man/nng_http_conn_read_res.3http.adoc b/docs/man/nng_http_conn_read_res.3http.adoc index 4e2f2713e..96e64ed6f 100644 --- a/docs/man/nng_http_conn_read_res.3http.adoc +++ b/docs/man/nng_http_conn_read_res.3http.adoc @@ -42,6 +42,12 @@ the operation is signaled via the _aio_, and the final result may be obtained via `<>`. That result will either be zero or an error code. +NOTE: Consider using the +`<>` or +`<>` functions, +which provide a simpler interface for performing a complete HTTP client +transaction. + == RETURN VALUES None. @@ -63,5 +69,7 @@ None. <>, <>, <>, +<>, +<>, <>, <> diff --git a/docs/man/nng_http_conn_transact.3http.adoc b/docs/man/nng_http_conn_transact.3http.adoc new file mode 100644 index 000000000..bd41f659d --- /dev/null +++ b/docs/man/nng_http_conn_transact.3http.adoc @@ -0,0 +1,98 @@ += nng_http_conn_transact(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_conn_transact - perform one HTTP transaction on connection + +== SYNOPSIS + +[source, c] +---- +#include +#include + +void nng_http_conn_transact(nng_http_conn *conn, nng_http_req *req, + nng_http_res *res, nng_aio *aio); +---- + +== DESCRIPTION + +The `nng_http_conn_transact()` function is used to perform a complete +HTTP exchange over the connection _conn_, sending the request _req_ +(and attached body data) to the remote server, and reading the response +_res_. +The entire response is read, including any associated body, which can +subsequently be obtained using +`<>`. + +This function is intended to make creation of client applications easier, +by performing multiple asynchronous operations required to complete an +entire HTTP transaction. + +If an error occurs, the caller should close _conn_ with +`<>`, as it may not +necessarily be usable with other transactions. + +A similar function, +`<>`, +exists. +That function behaves similarily, but creates a connection on demand +for the transaction, and disposes of it when finished. + +NOTE: This function does not support reading data sent using chunked +transfer encoding, and if the server attempts to do so, the underlying +connection will be closed and an `NNG_ENOTSUP` error will be returned. +This limitation is considered a bug, and a fix is planned for the future. + +WARNING: If the remote server tries to send an extremely large buffer, +then a corresponding allocation will be made, which can lead to denial +of service attacks. +Client applications should take care to use this only with reasonably +trust-worthy servers. + +WARNING: A given connection _conn_ should be used with only one +operation or transaction at a time as HTTP/1.1 has no support for +request interleaving. + +This function returns immediately, with no return value. +Completion of the operation is signaled via the _aio_, and the final result +may be obtained via `<>`. +That result will either be zero or an error code. + +== RETURN VALUES + +None. + +== ERRORS + +[horizontal] +`NNG_ECANCELED`:: The operation was canceled. +`NNG_ECLOSED`:: The connection was closed. +`NNG_ECONNRESET`:: The peer closed the connection. +`NNG_ENOMEM`:: Insufficient free memory to perform the operation. +`NNG_ENOTSUP`:: HTTP operations are not supported, or peer sent chunked encoding. +`NNG_EPROTO`:: An HTTP protocol error occurred. +`NNG_ETIMEDOUT`:: Timeout waiting for data from the connection. + +== SEE ALSO + +[.text-left] +<>, +<>, +<>, +<>, +<>, +<>, +<>, +<>, +<>, +<> diff --git a/docs/man/nng_http_conn_write_req.3http.adoc b/docs/man/nng_http_conn_write_req.3http.adoc index 0384e7b19..338ab26d9 100644 --- a/docs/man/nng_http_conn_write_req.3http.adoc +++ b/docs/man/nng_http_conn_write_req.3http.adoc @@ -39,6 +39,12 @@ Completion of the operation is signaled via the _aio_, and the final result may be obtained via `<>`. That result will either be zero or an error code. +NOTE: Consider using the +`<>` or +`<>` functions, +which provide a simpler interface for performing a complete HTTP client +transaction. + == RETURN VALUES None. @@ -59,6 +65,8 @@ None. <>, <>, <>, +<>, <>, +<>, <>, <> diff --git a/docs/man/nng_http_conn_write_res.3http.adoc b/docs/man/nng_http_conn_write_res.3http.adoc index 2bab9a311..62d7a3d12 100644 --- a/docs/man/nng_http_conn_write_res.3http.adoc +++ b/docs/man/nng_http_conn_write_res.3http.adoc @@ -48,6 +48,12 @@ If however the _res_ contains a header of `Connection:` with a value of `Close` (case-insensitive) or the response corresponds to `HTTP/1.0`, then the connection is immediately after sending the response. +NOTE: Consider using the +`<>` or +`<>` functions, +which provide a simpler interface for performing a complete HTTP client +transaction. + == RETURN VALUES None. @@ -68,6 +74,8 @@ None. <>, <>, <>, +<>, <>, +<>, <>, <> diff --git a/docs/man/nng_http_req_get_data.3http.adoc b/docs/man/nng_http_req_get_data.3http.adoc new file mode 100644 index 000000000..cd5c076ef --- /dev/null +++ b/docs/man/nng_http_req_get_data.3http.adoc @@ -0,0 +1,50 @@ += nng_http_req_get_data(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_req_get_data - get HTTP request body + +== SYNOPSIS + +[source, c] +---- +#include +#include + +void nng_http_req_get_data(nng_http_req *req, void **bodyp, size_t *sizep); +---- + +== DESCRIPTION + +The `nng_http_req_get_data()` gets the HTTP body associated with +the request _req_, storing a pointer to the buffer at the location referenced +by _bodyp_, and the length of the associated buffer at the location referenced +by _sizep_. + +NOTE: The buffer returned is owned by _req_, and will automatically freed +when the request is freed. + +== RETURN VALUES + +None. + +== ERRORS + +None. + +== SEE ALSO + +[.text-left] +<>, +<>, +<>, +<> diff --git a/docs/man/nng_http_res_get_data.3http.adoc b/docs/man/nng_http_res_get_data.3http.adoc new file mode 100644 index 000000000..44f98c1fe --- /dev/null +++ b/docs/man/nng_http_res_get_data.3http.adoc @@ -0,0 +1,50 @@ += nng_http_res_get_data(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_res_get_data - get HTTP response body + +== SYNOPSIS + +[source, c] +---- +#include +#include + +void nng_http_res_get_data(nng_http_res *res, void **bodyp, size_t *sizep); +---- + +== DESCRIPTION + +The `nng_http_res_get_data()` gets the HTTP body associated with +the request _res_, storing a pointer to the buffer at the location referenced +by _bodyp_, and the length of the associated buffer at the location referenced +by _sizep_. + +NOTE: The buffer returned is owned by _res_, and will automatically freed +when the request is freed. + +== RETURN VALUES + +None. + +== ERRORS + +None. + +== SEE ALSO + +[.text-left] +<>, +<>, +<>, +<> diff --git a/src/supplemental/http/http.h b/src/supplemental/http/http.h index 1991da1b0..f6656fcec 100644 --- a/src/supplemental/http/http.h +++ b/src/supplemental/http/http.h @@ -154,6 +154,9 @@ NNG_DECL int nng_http_req_set_data(nng_http_req *, const void *, size_t); // probably set the content-type header. NNG_DECL int nng_http_req_copy_data(nng_http_req *, const void *, size_t); +// nng_http_req_get_data gets the data for the response. +NNG_DECL void nng_http_req_get_data(nng_http_req *, void **, size_t *); + // nng_http_res represents an HTTP response. typedef struct nng_http_res nng_http_res; @@ -208,6 +211,9 @@ NNG_DECL int nng_http_res_set_version(nng_http_res *, const char *); // nng_http_res_get_version returns the version, usually HTTP/1.1. NNG_DECL const char *nng_http_res_get_version(nng_http_res *); +// nng_http_res_get_data gets the data for the response. +NNG_DECL void nng_http_res_get_data(nng_http_res *, void **, size_t *); + // nng_http_res_set_data adds entity data to the response. The // data object must persist (so only really useful for static data). // The content-length header is updated as well, but the caller should @@ -477,6 +483,22 @@ NNG_DECL int nng_http_client_get_tls( // in the first (index 0) output for the aio. NNG_DECL void nng_http_client_connect(nng_http_client *, nng_aio *); +// nng_http_conn_transact is used to perform a round-trip exchange (i.e. a +// single HTTP transaction). It will not automatically close the connection, +// unless some kind of significant error occurs. The caller should close +// the connection if the aio does not complete successfully. +// Note that this will fail with NNG_ENOTSUP if the server attempts to reply +// with a chunked transfer encoding. +NNG_DECL void nng_http_conn_transact( + nng_http_conn *, nng_http_req *, nng_http_res *, nng_aio *); + +// nng_http_client_transact is used to execute a single transaction to a +// server. The connection is opened, and will be closed when the transaction is +// complete. Note that this will fail with NNG_ENOTSUP if the server attempts +// to reply with a chunked transfer encoding. +NNG_DECL void nng_http_client_transact( + nng_http_client *, nng_http_req *, nng_http_res *, nng_aio *); + #ifdef __cplusplus } #endif diff --git a/src/supplemental/http/http_api.h b/src/supplemental/http/http_api.h index cf2c78bf9..71b24f548 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_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 *); extern const char *nni_http_req_get_uri(nni_http_req *); @@ -306,4 +307,20 @@ extern int nni_http_client_get_tls( extern void nni_http_client_connect(nni_http_client *, nni_aio *); +// nni_http_transact_conn is used to perform a round-trip exchange (i.e. a +// single HTTP transaction). It will not automatically close the connection, +// unless some kind of significant error occurs. The caller should dispose +// of the connection if the aio does not complete successfully. +// Note that this will fail with NNG_ENOTSUP if the server attempts to reply +// with a chunked transfer encoding. +extern void nni_http_transact_conn( + nni_http_conn *, nni_http_req *, nni_http_res *, nni_aio *); + +// nni_http_transact is used to execute a single transaction to a server. +// The connection is opened, and will be closed when the transaction is +// complete. Note that this will fail with NNG_ENOTSUP if the server attempts +// to reply with a chunked transfer encoding. +extern void nni_http_transact( + nni_http_client *, nni_http_req *, nni_http_res *, nni_aio *); + #endif // NNG_SUPPLEMENTAL_HTTP_HTTP_API_H diff --git a/src/supplemental/http/http_client.c b/src/supplemental/http/http_client.c index 1639b3ece..f8b1c8ab4 100644 --- a/src/supplemental/http/http_client.c +++ b/src/supplemental/http/http_client.c @@ -19,6 +19,8 @@ #include "http_api.h" +static nni_mtx http_txn_lk; + struct nng_http_client { nni_list aios; nni_mtx mtx; @@ -265,3 +267,255 @@ nni_http_client_connect(nni_http_client *c, nni_aio *aio) } nni_mtx_unlock(&c->mtx); } + +static int http_client_sys_init(void); +static void http_client_sys_fini(void); + +static nni_initializer http_client_initializer = { + .i_init = http_client_sys_init, + .i_fini = http_client_sys_fini, + .i_once = 0, +}; + +typedef enum http_txn_state { + HTTP_CONNECTING, + HTTP_SENDING, + HTTP_RECVING, + HTTP_RECVING_BODY, +} http_txn_state; + +typedef struct http_txn { + nni_aio * aio; // lower level aio + nni_list aios; // upper level aio(s) -- maximum one + nni_http_client *client; + nni_http_conn * conn; + nni_http_req * req; + nni_http_res * res; + http_txn_state state; + nni_reap_item reap; +} http_txn; + +static void +http_txn_reap(void *arg) +{ + http_txn *txn = arg; + if (txn->client != NULL) { + // We only close the connection if we created it. + if (txn->conn != NULL) { + nni_http_conn_fini(txn->conn); + } + } + nni_aio_fini(txn->aio); + NNI_FREE_STRUCT(txn); +} + +static void +http_txn_cb(void *arg) +{ + http_txn * txn = arg; + const char *str; + nni_aio * aio; + int rv; + uint64_t len; + nni_iov iov; + + nni_mtx_lock(&http_txn_lk); + if ((rv = nni_aio_result(txn->aio)) != 0) { + while ((aio = nni_list_first(&txn->aios)) != NULL) { + nni_list_remove(&txn->aios, aio); + nni_aio_finish_error(aio, rv); + } + nni_mtx_unlock(&http_txn_lk); + nni_reap(&txn->reap, http_txn_reap, txn); + return; + } + switch (txn->state) { + case HTTP_CONNECTING: + txn->conn = nni_aio_get_output(txn->aio, 0); + txn->state = HTTP_SENDING; + nni_http_write_req(txn->conn, txn->req, txn->aio); + nni_mtx_unlock(&http_txn_lk); + return; + + case HTTP_SENDING: + txn->state = HTTP_RECVING; + nni_http_read_res(txn->conn, txn->res, txn->aio); + nni_mtx_unlock(&http_txn_lk); + return; + + case HTTP_RECVING: + if (((str = nni_http_res_get_header( + txn->res, "Transfer-Encoding")) != NULL) && + (strstr(str, "chunked") != NULL)) { + // We refuse to receive chunked encoding data. + // This is an implementation limitation, but as HTTP/2 + // has eliminated this encoding, maybe it's not that + // big of a deal. We forcibly close this. + while ((aio = nni_list_first(&txn->aios)) != NULL) { + nni_list_remove(&txn->aios, aio); + nni_aio_finish_error(aio, NNG_ENOTSUP); + } + nni_http_conn_close(txn->conn); + nni_mtx_unlock(&http_txn_lk); + nni_reap(&txn->reap, http_txn_reap, txn); + return; + } + str = nni_http_req_get_method(txn->req); + if ((nni_strcasecmp(str, "HEAD") == 0) || + ((str = nni_http_res_get_header( + txn->res, "Content-Length")) == NULL) || + (nni_strtou64(str, &len) != 0) || (len == 0)) { + // If no content-length, or HEAD (which per RFC + // never transfers data), then we are done. + while ((aio = nni_list_first(&txn->aios)) != NULL) { + nni_list_remove(&txn->aios, aio); + nni_aio_finish(aio, 0, 0); + } + nni_mtx_unlock(&http_txn_lk); + nni_reap(&txn->reap, http_txn_reap, txn); + return; + } + + nni_http_res_alloc_data(txn->res, (size_t) len); + nni_http_res_get_data(txn->res, &iov.iov_buf, &iov.iov_len); + nni_aio_set_iov(txn->aio, 1, &iov); + txn->state = HTTP_RECVING_BODY; + nni_http_read_full(txn->conn, txn->aio); + nni_mtx_unlock(&http_txn_lk); + return; + + case HTTP_RECVING_BODY: + // All done! + while ((aio = nni_list_first(&txn->aios)) != NULL) { + nni_list_remove(&txn->aios, aio); + nni_aio_finish(aio, 0, 0); + } + nni_mtx_unlock(&http_txn_lk); + nni_reap(&txn->reap, http_txn_reap, txn); + return; + } + + NNI_ASSERT(0); // Unknown state! +} + +static void +http_txn_cancel(nni_aio *aio, void *arg, int rv) +{ + http_txn *txn = arg; + nni_mtx_lock(&http_txn_lk); + if (nni_aio_list_active(aio)) { + nni_aio_abort(txn->aio, rv); + } + nni_mtx_unlock(&http_txn_lk); +} + +// nni_http_transact_conn sends a request to an HTTP server, and reads the +// response. It also attempts to read any associated data. Note that +// at present it can only read data that comes in normally, as support +// for Chunked Transfer Encoding is missing. Note that cancelling the aio +// is generally fatal to the connection. +void +nni_http_transact_conn( + nni_http_conn *conn, nni_http_req *req, nni_http_res *res, nni_aio *aio) +{ + http_txn *txn; + int rv; + + nni_initialize(&http_client_initializer); + + if (nni_aio_begin(aio) != 0) { + return; + } + if ((txn = NNI_ALLOC_STRUCT(txn)) == NULL) { + nni_aio_finish_error(aio, NNG_ENOMEM); + return; + } + if ((rv = nni_aio_init(&txn->aio, http_txn_cb, txn)) != 0) { + NNI_FREE_STRUCT(txn); + nni_aio_finish_error(aio, rv); + return; + } + nni_aio_list_init(&txn->aios); + txn->client = NULL; + txn->conn = conn; + txn->req = req; + txn->res = res; + txn->state = HTTP_SENDING; + + nni_mtx_lock(&http_txn_lk); + if ((rv = nni_aio_schedule(aio, http_txn_cancel, txn)) != 0) { + nni_mtx_unlock(&http_txn_lk); + nni_aio_finish_error(aio, rv); + nni_reap(&txn->reap, http_txn_reap, txn); + return; + } + nni_http_res_reset(txn->res); + nni_list_append(&txn->aios, aio); + nni_http_write_req(conn, req, txn->aio); + nni_mtx_unlock(&http_txn_lk); +} + +// nni_http_transact_simple does a single transaction, creating a connection +// just for the purpose, and closing it when done. (No connection caching.) +// The reason we require a client to be created first is to deal with TLS +// settings. A single global client (per server) may be used. +void +nni_http_transact(nni_http_client *client, nni_http_req *req, + nni_http_res *res, nni_aio *aio) +{ + http_txn *txn; + int rv; + + nni_initialize(&http_client_initializer); + + if (nni_aio_begin(aio) != 0) { + return; + } + if ((txn = NNI_ALLOC_STRUCT(txn)) == NULL) { + nni_aio_finish_error(aio, NNG_ENOMEM); + return; + } + if ((rv = nni_aio_init(&txn->aio, http_txn_cb, txn)) != 0) { + NNI_FREE_STRUCT(txn); + nni_aio_finish_error(aio, rv); + return; + } + + if ((rv = nni_http_req_set_header(req, "Connection", "close")) != 0) { + nni_aio_finish_error(aio, rv); + nni_reap(&txn->reap, http_txn_reap, txn); + return; + } + + nni_aio_list_init(&txn->aios); + txn->client = NULL; + txn->conn = NULL; + txn->req = req; + txn->res = res; + txn->state = HTTP_CONNECTING; + + nni_mtx_lock(&http_txn_lk); + if ((rv = nni_aio_schedule(aio, http_txn_cancel, txn)) != 0) { + nni_mtx_unlock(&http_txn_lk); + nni_aio_finish_error(aio, rv); + nni_reap(&txn->reap, http_txn_reap, txn); + return; + } + nni_http_res_reset(txn->res); + nni_list_append(&txn->aios, aio); + nni_http_client_connect(client, txn->aio); + nni_mtx_unlock(&http_txn_lk); +} + +static int +http_client_sys_init(void) +{ + nni_mtx_init(&http_txn_lk); + return (0); +} + +static void +http_client_sys_fini(void) +{ + nni_mtx_fini(&http_txn_lk); +} diff --git a/src/supplemental/http/http_msg.c b/src/supplemental/http/http_msg.c index d6ab862e9..dcd842c50 100644 --- a/src/supplemental/http/http_msg.c +++ b/src/supplemental/http/http_msg.c @@ -399,6 +399,20 @@ nni_http_res_copy_data(nni_http_res *res, const void *data, size_t size) return (0); } +// nni_http_res_alloc_data allocates the data region, but does not update any +// headers. The intended use is for client implementations that want to +// allocate a buffer to receive the entity into. +int +nni_http_res_alloc_data(nni_http_res *res, size_t size) +{ + int rv; + + if ((rv = http_entity_alloc_data(&res->data, size)) != 0) { + return (rv); + } + return (0); +} + bool nni_http_res_is_error(nni_http_res *res) { diff --git a/src/supplemental/http/http_public.c b/src/supplemental/http/http_public.c index f275db28a..84811e548 100644 --- a/src/supplemental/http/http_public.c +++ b/src/supplemental/http/http_public.c @@ -226,6 +226,30 @@ nng_http_res_set_data(nng_http_res *res, const void *data, size_t sz) #endif } +void +nng_http_req_get_data(nng_http_req *req, void **datap, size_t *lenp) +{ +#ifdef NNG_SUPP_HTTP + nni_http_req_get_data(req, datap, lenp); +#else + NNI_ARG_UNUSED(req); + *datap = NULL; + *lenp = 0; +#endif +} + +void +nng_http_res_get_data(nng_http_res *res, void **datap, size_t *lenp) +{ +#ifdef NNG_SUPP_HTTP + nni_http_res_get_data(res, datap, lenp); +#else + NNI_ARG_UNUSED(res); + *datap = NULL; + *lenp = 0; +#endif +} + const char * nng_http_req_get_method(nng_http_req *req) { @@ -811,3 +835,35 @@ nng_http_client_connect(nng_http_client *cli, nng_aio *aio) } #endif } + +void +nng_http_client_transact( + nng_http_client *cli, nng_http_req *req, nng_http_res *res, nng_aio *aio) +{ +#ifdef NNG_SUPP_HTTP + nni_http_transact(cli, req, res, aio); +#else + NNI_ARG_UNUSED(cli); + NNI_ARG_UNUSED(req); + NNI_ARG_UNUSED(res); + if (nni_aio_begin(aio) == 0) { + nni_aio_finish_error(aio, NNG_ENOTSUP); + } +#endif +} + +void +nng_http_conn_transact( + nng_http_conn *conn, nng_http_req *req, nng_http_res *res, nng_aio *aio) +{ +#ifdef NNG_SUPP_HTTP + nni_http_transact_conn(conn, req, res, aio); +#else + NNI_ARG_UNUSED(conn); + NNI_ARG_UNUSED(req); + NNI_ARG_UNUSED(res); + if (nni_aio_begin(aio) == 0) { + nni_aio_finish_error(aio, NNG_ENOTSUP); + } +#endif +} diff --git a/tests/httpclient.c b/tests/httpclient.c index 96597d0cf..6964bcc2f 100644 --- a/tests/httpclient.c +++ b/tests/httpclient.c @@ -29,7 +29,7 @@ const uint8_t example_sum[20] = { 0x0e, 0x97, 0x3b, 0x59, 0xf4, 0x76, 0x00, TestMain("HTTP Client", { atexit(nng_fini); - Convey("Given a TCP connection to httpbin.org", { + Convey("Given a TCP connection to example.com", { nng_aio * aio; nng_http_client *cli; nng_http_conn * http; @@ -37,8 +37,9 @@ TestMain("HTTP Client", { So(nng_aio_alloc(&aio, NULL, NULL) == 0); - So(nng_url_parse(&url, "http://example.org/") == 0); + So(nng_url_parse(&url, "http://example.com/") == 0); + nng_aio_set_timeout(aio, 10000); So(nng_http_client_alloc(&cli, url) == 0); nng_http_client_connect(cli, aio); nng_aio_wait(aio); @@ -105,4 +106,106 @@ TestMain("HTTP Client", { }); }); }); + + Convey("Given a client", { + nng_aio * aio; + nng_http_client *cli; + nng_url * url; + + So(nng_aio_alloc(&aio, NULL, NULL) == 0); + + So(nng_url_parse(&url, "http://example.com/") == 0); + + So(nng_http_client_alloc(&cli, url) == 0); + nng_aio_set_timeout(aio, 10000); // 10 sec timeout + + Reset({ + nng_http_client_free(cli); + nng_url_free(url); + nng_aio_free(aio); + }); + + Convey("One off exchange works", { + nng_http_req *req; + nng_http_res *res; + void * data; + size_t len; + uint8_t digest[20]; + + So(nng_http_req_alloc(&req, url) == 0); + So(nng_http_res_alloc(&res) == 0); + Reset({ + nng_http_req_free(req); + nng_http_res_free(res); + }); + + nng_http_client_transact(cli, req, res, aio); + nng_aio_wait(aio); + So(nng_aio_result(aio) == 0); + So(nng_http_res_get_status(res) == 200); + nng_http_res_get_data(res, &data, &len); + nni_sha1(data, len, digest); + So(memcmp(digest, example_sum, 20) == 0); + }); + + Convey("Timeout works", { + nng_http_req *req; + nng_http_res *res; + + So(nng_http_req_alloc(&req, url) == 0); + So(nng_http_res_alloc(&res) == 0); + Reset({ + nng_http_req_free(req); + nng_http_res_free(res); + }); + + nng_aio_set_timeout(aio, 1); // 1 ms, should timeout! + nng_http_client_transact(cli, req, res, aio); + nng_aio_wait(aio); + So(nng_aio_result(aio) == NNG_ETIMEDOUT); + }); + + Convey("Connection reuse works", { + nng_http_req * req; + nng_http_res * res1; + nng_http_res * res2; + void * data; + size_t len; + uint8_t digest[20]; + nng_http_conn *conn = NULL; + + So(nng_http_req_alloc(&req, url) == 0); + So(nng_http_res_alloc(&res1) == 0); + So(nng_http_res_alloc(&res2) == 0); + Reset({ + nng_http_req_free(req); + nng_http_res_free(res1); + nng_http_res_free(res2); + if (conn != NULL) { + nng_http_conn_close(conn); + } + }); + + nng_http_client_connect(cli, aio); + nng_aio_wait(aio); + So(nng_aio_result(aio) == 0); + conn = nng_aio_get_output(aio, 0); + + nng_http_conn_transact(conn, req, res1, aio); + nng_aio_wait(aio); + So(nng_aio_result(aio) == 0); + So(nng_http_res_get_status(res1) == 200); + nng_http_res_get_data(res1, &data, &len); + nni_sha1(data, len, digest); + So(memcmp(digest, example_sum, 20) == 0); + + nng_http_conn_transact(conn, req, res2, aio); + nng_aio_wait(aio); + So(nng_aio_result(aio) == 0); + So(nng_http_res_get_status(res2) == 200); + nng_http_res_get_data(res2, &data, &len); + nni_sha1(data, len, digest); + So(memcmp(digest, example_sum, 20) == 0); + }); + }); })