diff options
Diffstat (limited to 'src/supplemental/http/http_server.c')
| -rw-r--r-- | src/supplemental/http/http_server.c | 1532 |
1 files changed, 1532 insertions, 0 deletions
diff --git a/src/supplemental/http/http_server.c b/src/supplemental/http/http_server.c new file mode 100644 index 00000000..21a88f9e --- /dev/null +++ b/src/supplemental/http/http_server.c @@ -0,0 +1,1532 @@ +// +// Copyright 2018 Staysail Systems, Inc. <info@staysail.tech> +// Copyright 2018 Capitar IT Group BV <info@capitar.com> +// +// This software 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. +// + +#include <ctype.h> +#include <stdbool.h> +#include <stdio.h> +#include <string.h> + +#include "core/nng_impl.h" +#include "supplemental/tls/tls.h" + +#include "http.h" + +static int http_server_sys_init(void); +static void http_server_sys_fini(void); + +static nni_initializer http_server_initializer = { + .i_init = http_server_sys_init, + .i_fini = http_server_sys_fini, + .i_once = 0, +}; + +struct nng_http_handler { + nni_list_node node; + char * path; + char * method; + char * host; + bool tree; + int refcnt; + void * data; + nni_cb dtor; + void (*cb)(nni_aio *); +}; + +typedef struct nni_http_ctx { + 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; +} http_sconn; + +struct nng_http_server { + nng_sockaddr addr; + nni_list_node node; + int refcnt; + int starts; + nni_list handlers; + nni_list conns; + nni_mtx mtx; + nni_cv cv; + bool closed; + nng_tls_config * tls; + nni_aio * accaio; + nni_plat_tcp_ep *tep; + char * port; + char * hostname; +}; + +int +nni_http_handler_init( + nni_http_handler **hp, const char *path, void (*cb)(nni_aio *)) +{ + nni_http_handler *h; + + if ((h = NNI_ALLOC_STRUCT(h)) == NULL) { + return (NNG_ENOMEM); + } + if (((h->path = nni_strdup(path)) == NULL) || + ((h->method = nni_strdup("GET")) == NULL)) { + nni_http_handler_fini(h); + 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; + return (0); +} + +void +nni_http_handler_fini(nni_http_handler *h) +{ + if (h->refcnt != 0) { + return; + } + if (h->dtor != NULL) { + h->dtor(h->data); + } + nni_strfree(h->host); + nni_strfree(h->path); + nni_strfree(h->method); + NNI_FREE_STRUCT(h); +} + +int +nni_http_handler_set_data(nni_http_handler *h, void *data, nni_cb dtor) +{ + if (h->refcnt != 0) { + return (NNG_EBUSY); + } + h->data = data; + h->dtor = dtor; + return (0); +} + +void * +nni_http_handler_get_data(nni_http_handler *h) +{ + return (h->data); +} + +const char * +nni_http_handler_get_uri(nni_http_handler *h) +{ + return (h->path); +} + +int +nni_http_handler_set_tree(nni_http_handler *h) +{ + if (h->refcnt != 0) { + return (NNG_EBUSY); + } + h->tree = true; + return (0); +} + +int +nni_http_handler_set_host(nni_http_handler *h, const char *host) +{ + char *duphost; + if (h->refcnt != 0) { + return (NNG_EBUSY); + } + if (host == NULL) { + nni_strfree(h->host); + h->host = NULL; + return (0); + } + if ((duphost = nni_strdup(host)) == NULL) { + return (NNG_ENOMEM); + } + nni_strfree(h->host); + h->host = duphost; + return (0); +} + +int +nni_http_handler_set_method(nni_http_handler *h, const char *method) +{ + char *dupmeth; + if (h->refcnt != 0) { + return (NNG_EBUSY); + } + if (method == NULL) { + nni_strfree(h->method); + h->method = NULL; + return (0); + } + if ((dupmeth = nni_strdup(method)) == NULL) { + return (NNG_ENOMEM); + } + nni_strfree(h->method); + h->method = dupmeth; + return (0); +} + +static nni_list http_servers; +static nni_mtx http_servers_lk; + +static void +http_sconn_reap(void *arg) +{ + http_sconn * sc = arg; + nni_http_server *s = sc->server; + NNI_ASSERT(!sc->finished); + sc->finished = true; + nni_aio_stop(sc->rxaio); + nni_aio_stop(sc->txaio); + nni_aio_stop(sc->txdataio); + nni_aio_stop(sc->cbaio); + + if (sc->conn != NULL) { + nni_http_conn_fini(sc->conn); + } + if (sc->req != NULL) { + nni_http_req_free(sc->req); + } + if (sc->res != NULL) { + nni_http_res_free(sc->res); + } + nni_aio_fini(sc->rxaio); + nni_aio_fini(sc->txaio); + nni_aio_fini(sc->txdataio); + nni_aio_fini(sc->cbaio); + + // Now it is safe to release our reference on the server. + nni_mtx_lock(&s->mtx); + if (nni_list_node_active(&sc->node)) { + nni_list_remove(&s->conns, sc); + if (nni_list_empty(&s->conns)) { + nni_cv_wake(&s->cv); + } + } + nni_mtx_unlock(&s->mtx); + + NNI_FREE_STRUCT(sc); +} + +static void +http_sconn_fini(http_sconn *sc) +{ + nni_reap(&sc->reap, http_sconn_reap, sc); +} + +static void +http_sconn_close_locked(http_sconn *sc) +{ + nni_http_conn *conn; + + if (sc->closed) { + return; + } + NNI_ASSERT(!sc->finished); + + sc->closed = true; + nni_aio_abort(sc->rxaio, NNG_ECLOSED); + nni_aio_abort(sc->txaio, NNG_ECLOSED); + nni_aio_abort(sc->txdataio, NNG_ECLOSED); + nni_aio_abort(sc->cbaio, NNG_ECLOSED); + + if ((conn = sc->conn) != NULL) { + nni_http_conn_close(conn); + } + http_sconn_fini(sc); +} + +static void +http_sconn_close(http_sconn *sc) +{ + nni_http_server *s; + s = sc->server; + + nni_mtx_lock(&s->mtx); + http_sconn_close_locked(sc); + nni_mtx_unlock(&s->mtx); +} + +static void +http_sconn_txdatdone(void *arg) +{ + http_sconn *sc = arg; + nni_aio * aio = sc->txdataio; + + if (nni_aio_result(aio) != 0) { + http_sconn_close(sc); + return; + } + + if (sc->res != NULL) { + nni_http_res_free(sc->res); + sc->res = NULL; + } + + if (sc->close) { + http_sconn_close(sc); + return; + } + + nni_http_req_reset(sc->req); + nni_http_read_req(sc->conn, sc->req, sc->rxaio); +} + +static void +http_sconn_txdone(void *arg) +{ + http_sconn *sc = arg; + nni_aio * aio = sc->txaio; + + if (nni_aio_result(aio) != 0) { + http_sconn_close(sc); + return; + } + + if (sc->close) { + http_sconn_close(sc); + return; + } + + if (sc->res != NULL) { + nni_http_res_free(sc->res); + sc->res = NULL; + } + nni_http_req_reset(sc->req); + nni_http_read_req(sc->conn, sc->req, sc->rxaio); +} + +static char +http_hexval(char c) +{ + if ((c >= '0') && (c <= '9')) { + return (c - '0'); + } + if ((c >= 'a') && (c <= 'f')) { + return ((c - 'a') + 10); + } + if ((c >= 'A') && (c <= 'F')) { + return ((c - 'A') + 10); + } + return (0); +} + +// XXX: REPLACE THIS WITH CODE USING THE URL FRAMEWORK. +static char * +http_uri_canonify(char *path) +{ + char *tmp; + char *dst; + + // Chomp off query string. + if ((tmp = strchr(path, '?')) != NULL) { + *tmp = '\0'; + } + // If the URI was absolute, make it relative. + if ((nni_strncasecmp(path, "http://", strlen("http://")) == 0) || + (nni_strncasecmp(path, "https://", strlen("https://")) == 0)) { + // Skip past the :// + path = strchr(path, ':'); + path += 3; + + // scan for the end of the host, distinguished by a / + // path delimiter. There might not be one, in which + // case the whole thing is the host and we assume the + // path is just /. + if ((path = strchr(path, '/')) == NULL) { + return ("/"); + } + } + + // Now we have to unescape things. Unescaping is a shrinking + // operation (strictly), so this is safe. This is just URL + // decode. Note that paths with an embedded NUL are going to be + // treated as though truncated. Don't be that guy that sends + // %00 in a URL. + // + // XXX: Normalizer needs to leave % encoded stuff in there if + // the characters to which they refer are reserved. See RFC 3986 + // section 6.2.2. + tmp = path; + dst = path; + while (*tmp != '\0') { + char c; + if ((c = *tmp) != '%') { + *dst++ = c; + tmp++; + continue; + } + if (isxdigit(tmp[1]) && isxdigit(tmp[2])) { + c = http_hexval(tmp[1]); + c *= 16; + c += http_hexval(tmp[2]); + *dst++ = c; + tmp += 3; + } + // garbage in, garbage out + *dst++ = c; + tmp++; + } + *dst = '\0'; + + // XXX: this is where we should also remove /./ and /../ references. + return (path); +} + +static void +http_sconn_error(http_sconn *sc, uint16_t err) +{ + nni_http_res *res; + + if (nni_http_res_alloc_error(&res, err) != 0) { + http_sconn_close(sc); + return; + } + + if (sc->close) { + if (nni_http_res_set_header(res, "Connection", "close") != 0) { + http_sconn_close(sc); + } + } + sc->res = res; + nni_http_write_res(sc->conn, res, sc->txaio); +} + +int +nni_http_hijack(nni_http_conn *conn) +{ + http_sconn *sc; + + sc = nni_http_conn_get_ctx(conn); + if (sc != NULL) { + nni_http_server *s = sc->server; + nni_http_conn_set_ctx(conn, NULL); + + nni_mtx_lock(&s->mtx); + sc->conn = NULL; + sc->req = NULL; + nni_mtx_unlock(&s->mtx); + } + return (0); +} + +static void +http_sconn_rxdone(void *arg) +{ + http_sconn * sc = arg; + nni_http_server * s = sc->server; + nni_aio * aio = sc->rxaio; + int rv; + nni_http_handler *h = NULL; + nni_http_handler *head = NULL; + const char * val; + nni_http_req * req = sc->req; + char * uri; + size_t urisz; + char * path; + bool badmeth = false; + bool needhost = false; + const char * host; + + if ((rv = nni_aio_result(aio)) != 0) { + http_sconn_close(sc); + return; + } + + // 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. + if ((val = nni_http_req_get_version(req)) == NULL) { + sc->close = true; + http_sconn_error(sc, NNG_HTTP_STATUS_BAD_REQUEST); + return; + } + if (strncmp(val, "HTTP/1.", 7) != 0) { + sc->close = true; + http_sconn_error(sc, NNG_HTTP_STATUS_HTTP_VERSION_NOT_SUPP); + return; + } + if (strcmp(val, "HTTP/1.1") != 0) { + // We treat HTTP/1.0 connections as non-persistent. + // No effort is made for non-standard "persistent" HTTP/1.0. + sc->close = true; + } else { + needhost = true; + } + + // If the connection was 1.0, or a connection: close was + // requested, then mark this close on our end. + if ((val = nni_http_req_get_header(req, "Connection")) != NULL) { + // HTTP 1.1 says these have to be case insensitive + if (nni_strcasestr(val, "close") != NULL) { + // In theory this could falsely match some other weird + // connection header with the substring close. No such + // values are defined, so anyone who does that gets + // what they deserve. (Harmless actually, since it only + // prevents persistent connections.) + sc->close = true; + } + } + + val = nni_http_req_get_uri(req); + urisz = strlen(val) + 1; + if ((uri = nni_alloc(urisz)) == NULL) { + http_sconn_close(sc); // out of memory + return; + } + strncpy(uri, val, urisz); + path = http_uri_canonify(uri); + + host = nni_http_req_get_header(req, "Host"); + if ((host == NULL) && (needhost)) { + // Per RFC 2616 14.23 we have to send 400 status here. + http_sconn_error(sc, NNG_HTTP_STATUS_BAD_REQUEST); + nni_free(uri, urisz); + return; + } + + nni_mtx_lock(&s->mtx); + NNI_LIST_FOREACH (&s->handlers, h) { + size_t len; + if (h->host != NULL) { + if (host == NULL) { + // HTTP/1.0 cannot access virtual hosts. + continue; + } + + len = strlen(h->host); + if ((nni_strncasecmp(host, h->host, len) != 0)) { + continue; + } + + // At least the first part matches. If the ending + // part is a lone "." (legal in DNS), or a port + // number, we match it. (We do not validate the + // port number.) Note that there may be false matches + // with IPv6 addresses, but addresses shouldn't be + // used with virtual hosts anyway. With both addresses + // and ports, a false match would be unlikely since + // they'd still have to *connect* using that info. + if ((host[len] != '\0') && (host[len] != ':') && + ((host[len] != '.') || (host[len + 1] != '\0'))) { + continue; + } + } + + len = strlen(h->path); + if (strncmp(path, h->path, len) != 0) { + continue; + } + switch (path[len]) { + case '\0': + break; + case '/': + if ((path[len + 1] != '\0') && (!h->tree)) { + // Trailing component and not a directory. + continue; + } + break; + default: + continue; // Some other substring, not matched. + } + + if ((h->method == NULL) || (h->method[0] == '\0')) { + // Handler wants to process *all* methods. + break; + } + // So, what about the method? + val = nni_http_req_get_method(req); + if (strcmp(val, h->method) == 0) { + break; + } + // HEAD is remapped to GET, but only if no HEAD specific + // handler registered. + if ((strcmp(val, "HEAD") == 0) && + (strcmp(h->method, "GET") == 0)) { + head = h; + continue; + } + badmeth = 1; + } + + if ((h == NULL) && (head != NULL)) { + h = head; + } + nni_free(uri, urisz); + if (h == NULL) { + nni_mtx_unlock(&s->mtx); + if (badmeth) { + http_sconn_error( + sc, NNG_HTTP_STATUS_METHOD_NOT_ALLOWED); + } else { + http_sconn_error(sc, NNG_HTTP_STATUS_NOT_FOUND); + } + return; + } + + 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); + + // Technically, probably callback should initialize this with + // start, but we do it instead. + + if (nni_aio_start(sc->cbaio, NULL, NULL) != 0) { + nni_mtx_unlock(&s->mtx); + return; + } + nni_aio_set_data(sc->cbaio, 1, h); + h->refcnt++; + nni_mtx_unlock(&s->mtx); + h->cb(sc->cbaio); +} + +static void +http_sconn_cbdone(void *arg) +{ + http_sconn * sc = arg; + nni_aio * aio = sc->cbaio; + nni_http_res * res; + nni_http_handler *h; + nni_http_server * s = sc->server; + + if (nni_aio_result(aio) != 0) { + // Hard close, no further feedback. + http_sconn_close(sc); + return; + } + + h = nni_aio_get_data(aio, 1); + res = nni_aio_get_output(aio, 0); + + nni_mtx_lock(&s->mtx); + h->refcnt--; + if (h->refcnt == 0) { + nni_http_handler_fini(h); + } + nni_mtx_unlock(&s->mtx); + + // If its an upgrader, and they didn't give us back a response, + // it means that they took over, and we should just discard + // this session, without closing the underlying channel. + if (sc->conn == NULL) { + // If this happens, then the session was hijacked. + // We close the context, but the http channel stays up. + http_sconn_close(sc); + return; + } + if (res != NULL) { + const char *val; + val = nni_http_res_get_header(res, "Connection"); + if ((val != NULL) && (strstr(val, "close") != NULL)) { + sc->close = true; + } + if (sc->close) { + nni_http_res_set_header(res, "Connection", "close"); + } + sc->res = res; + if (strcmp(nni_http_req_get_method(sc->req), "HEAD") == 0) { + void * data; + size_t size; + // prune off the data, but preserve the content-length + // header. By passing NULL here, we leave off the old + // data, but the non-zero size means we don't clobber + // the HTTP header. + nni_http_res_get_data(res, &data, &size); + nni_http_res_set_data(res, NULL, size); + } + nni_http_write_res(sc->conn, res, sc->txaio); + } else if (sc->close) { + http_sconn_close(sc); + } else { + // Presumably client already sent a response. + // Wait for another request. + nni_http_req_reset(sc->req); + nni_http_read_req(sc->conn, sc->req, sc->rxaio); + } +} + +static int +http_sconn_init(http_sconn **scp, nni_http_server *s, nni_plat_tcp_pipe *tcp) +{ + http_sconn *sc; + int rv; + + if ((sc = NNI_ALLOC_STRUCT(sc)) == NULL) { + nni_plat_tcp_pipe_fini(tcp); + return (NNG_ENOMEM); + } + + if (((rv = nni_http_req_alloc(&sc->req, NULL)) != 0) || + ((rv = nni_aio_init(&sc->rxaio, http_sconn_rxdone, sc)) != 0) || + ((rv = nni_aio_init(&sc->txaio, http_sconn_txdone, sc)) != 0) || + ((rv = nni_aio_init(&sc->txdataio, http_sconn_txdatdone, sc)) != + 0) || + ((rv = nni_aio_init(&sc->cbaio, http_sconn_cbdone, sc)) != 0)) { + // Can't even accept the incoming request. Hard close. + http_sconn_close(sc); + return (rv); + } + + if (s->tls != NULL) { + rv = nni_http_conn_init_tls(&sc->conn, s->tls, tcp); + } else { + rv = nni_http_conn_init_tcp(&sc->conn, tcp); + } + if (rv != 0) { + http_sconn_close(sc); + return (rv); + } + nni_http_conn_set_ctx(sc->conn, sc); + *scp = sc; + return (0); +} + +static void +http_server_acccb(void *arg) +{ + nni_http_server * s = arg; + nni_aio * aio = s->accaio; + nni_plat_tcp_pipe *tcp; + http_sconn * sc; + int rv; + + nni_mtx_lock(&s->mtx); + if ((rv = nni_aio_result(aio)) != 0) { + if (!s->closed) { + // try again? + nni_plat_tcp_ep_accept(s->tep, s->accaio); + } + nni_mtx_unlock(&s->mtx); + return; + } + tcp = nni_aio_get_pipe(aio); + if (s->closed) { + // If we're closing, then reject this one. + nni_plat_tcp_pipe_fini(tcp); + nni_mtx_unlock(&s->mtx); + return; + } + if (http_sconn_init(&sc, s, tcp) != 0) { + // The TCP structure is already cleaned up. + // Start another accept attempt. + nni_plat_tcp_ep_accept(s->tep, s->accaio); + nni_mtx_unlock(&s->mtx); + return; + } + sc->server = s; + nni_list_append(&s->conns, sc); + + nni_http_read_req(sc->conn, sc->req, sc->rxaio); + nni_plat_tcp_ep_accept(s->tep, s->accaio); + nni_mtx_unlock(&s->mtx); +} + +static void +http_server_fini(nni_http_server *s) +{ + nni_http_handler *h; + + nni_aio_stop(s->accaio); + + nni_mtx_lock(&s->mtx); + while (!nni_list_empty(&s->conns)) { + nni_cv_wait(&s->cv); + } + if (s->tep != NULL) { + nni_plat_tcp_ep_fini(s->tep); + } + while ((h = nni_list_first(&s->handlers)) != NULL) { + nni_list_remove(&s->handlers, h); + h->refcnt--; + nni_http_handler_fini(h); + } + nni_mtx_unlock(&s->mtx); +#ifdef NNG_SUPP_TLS + if (s->tls != NULL) { + nni_tls_config_fini(s->tls); + } +#endif + nni_aio_fini(s->accaio); + nni_cv_fini(&s->cv); + nni_mtx_fini(&s->mtx); + nni_strfree(s->hostname); + nni_strfree(s->port); + NNI_FREE_STRUCT(s); +} + +void +nni_http_server_fini(nni_http_server *s) +{ + nni_mtx_lock(&http_servers_lk); + s->refcnt--; + if (s->refcnt == 0) { + nni_list_remove(&http_servers, s); + http_server_fini(s); + } + nni_mtx_unlock(&http_servers_lk); +} + +static int +http_server_init(nni_http_server **serverp, const nni_url *url) +{ + nni_http_server *s; + int rv; + nni_aio * aio; + + if ((strcmp(url->u_scheme, "http") != 0) && +#ifdef NNG_SUPP_TLS + (strcmp(url->u_scheme, "https") != 0) && + (strcmp(url->u_scheme, "wss") != 0) && +#endif + (strcmp(url->u_scheme, "ws") != 0)) { + return (NNG_EADDRINVAL); + } + if ((s = NNI_ALLOC_STRUCT(s)) == NULL) { + return (NNG_ENOMEM); + } + nni_mtx_init(&s->mtx); + nni_cv_init(&s->cv, &s->mtx); + NNI_LIST_INIT(&s->handlers, nni_http_handler, node); + NNI_LIST_INIT(&s->conns, http_sconn, node); + if ((rv = nni_aio_init(&s->accaio, http_server_acccb, s)) != 0) { + http_server_fini(s); + return (rv); + } + + if ((strlen(url->u_port)) && + ((s->port = nni_strdup(url->u_port)) == NULL)) { + http_server_fini(s); + return (NNG_ENOMEM); + } + if ((strlen(url->u_hostname)) && + ((s->hostname = nni_strdup(url->u_hostname)) == NULL)) { + http_server_fini(s); + return (NNG_ENOMEM); + } + +#ifdef NNG_SUPP_TLS + if ((strcmp(url->u_scheme, "https") == 0) || + (strcmp(url->u_scheme, "wss") == 0)) { + rv = nni_tls_config_init(&s->tls, NNG_TLS_MODE_SERVER); + if (rv != 0) { + http_server_fini(s); + return (rv); + } + } +#endif + + // Do the DNS lookup *now*. This means that this is + // synchronous, but it should be fast, since it should either + // resolve as a number, or resolve locally, without having to + // hit up DNS. + if ((rv = nni_aio_init(&aio, NULL, NULL)) != 0) { + http_server_fini(s); + return (rv); + } + nni_aio_set_input(aio, 0, &s->addr); + nni_plat_tcp_resolv(s->hostname, s->port, NNG_AF_UNSPEC, true, aio); + nni_aio_wait(aio); + rv = nni_aio_result(aio); + nni_aio_fini(aio); + if (rv != 0) { + http_server_fini(s); + return (rv); + } + s->refcnt = 1; + *serverp = s; + return (0); +} + +int +nni_http_server_init(nni_http_server **serverp, const nni_url *url) +{ + int rv; + nni_http_server *s; + + nni_initialize(&http_server_initializer); + + nni_mtx_lock(&http_servers_lk); + NNI_LIST_FOREACH (&http_servers, s) { + if ((strcmp(url->u_port, s->port) == 0) && + (strcmp(url->u_hostname, s->hostname) == 0)) { + *serverp = s; + s->refcnt++; + nni_mtx_unlock(&http_servers_lk); + return (0); + } + } + + // We didn't find a server, try to make a new one. + if ((rv = http_server_init(&s, url)) == 0) { + nni_list_append(&http_servers, s); + *serverp = s; + } + + nni_mtx_unlock(&http_servers_lk); + return (rv); +} + +static int +http_server_start(nni_http_server *s) +{ + int rv; + rv = nni_plat_tcp_ep_init(&s->tep, &s->addr, NULL, NNI_EP_MODE_LISTEN); + if (rv != 0) { + return (rv); + } + if ((rv = nni_plat_tcp_ep_listen(s->tep)) != 0) { + nni_plat_tcp_ep_fini(s->tep); + s->tep = NULL; + return (rv); + } + nni_plat_tcp_ep_accept(s->tep, s->accaio); + return (0); +} + +int +nni_http_server_start(nni_http_server *s) +{ + int rv = 0; + + nni_mtx_lock(&s->mtx); + if (s->starts == 0) { + rv = http_server_start(s); + } + if (rv == 0) { + s->starts++; + } + nni_mtx_unlock(&s->mtx); + return (rv); +} + +static void +http_server_stop(nni_http_server *s) +{ + http_sconn *sc; + + if (s->closed) { + return; + } + + s->closed = true; + // Close the TCP endpoint that is listening. + if (s->tep) { + nni_plat_tcp_ep_close(s->tep); + } + + // Stopping the server is a hard stop -- it aborts any work + // being done by clients. (No graceful shutdown). + NNI_LIST_FOREACH (&s->conns, sc) { + http_sconn_close_locked(sc); + } + nni_cv_wake(&s->cv); +} + +void +nni_http_server_stop(nni_http_server *s) +{ + nni_mtx_lock(&s->mtx); + s->starts--; + if (s->starts == 0) { + http_server_stop(s); + } + nni_mtx_unlock(&s->mtx); +} + +int +nni_http_server_add_handler(nni_http_server *s, nni_http_handler *h) +{ + nni_http_handler *h2; + size_t len; + + // Must have a legal method (and not one that is HEAD), path, + // and handler. (The reason HEAD is verboten is that we supply + // it automatically as part of GET support.) + if (((len = strlen(h->path)) == 0) || (h->path[0] != '/') || + (h->cb == NULL)) { + return (NNG_EINVAL); + } + while ((len > 0) && (h->path[len - 1] == '/')) { + len--; // ignore trailing '/' (this collapses them) + } + + nni_mtx_lock(&s->mtx); + // General rule for finding a conflict is that if either string + // is a strict substring of the other, then we have a + // collision. (But only if the methods match, and the host + // matches.) Note that a wild card host matches both. + NNI_LIST_FOREACH (&s->handlers, h2) { + size_t len2; + + if ((h2->host != NULL) && (h->host != NULL) && + (nni_strcasecmp(h2->host, h->host) != 0)) { + // Hosts don't match, so we are safe. + continue; + } + if (((h2->host == NULL) && (h->host != NULL)) || + ((h->host == NULL) && (h2->host != NULL))) { + continue; // Host specified for just one. + } + if (((h->method == NULL) && (h2->method != NULL)) || + ((h2->method == NULL) && (h->method != NULL))) { + continue; // Method specified for just one. + } + if ((h->method != NULL) && + (strcmp(h2->method, h->method) != 0)) { + // Different methods, so again we are fine. + continue; + } + + len2 = strlen(h2->path); + + while ((len2 > 0) && (h2->path[len2 - 1] == '/')) { + len2--; // ignore trailing '/' + } + if (strncmp(h->path, h2->path, len > len2 ? len2 : len) != 0) { + continue; // prefixes don't match. + } + + if (len2 > len) { + if ((h2->path[len] == '/') && (h->tree)) { + nni_mtx_unlock(&s->mtx); + return (NNG_EADDRINUSE); + } + } else if (len > len2) { + if ((h->path[len2] == '/') && (h2->tree)) { + nni_mtx_unlock(&s->mtx); + return (NNG_EADDRINUSE); + } + } else { + nni_mtx_unlock(&s->mtx); + return (NNG_EADDRINUSE); + } + } + h->refcnt = 1; + nni_list_append(&s->handlers, h); + nni_mtx_unlock(&s->mtx); + return (0); +} + +int +nni_http_server_del_handler(nni_http_server *s, nni_http_handler *h) +{ + int rv = NNG_ENOENT; + nni_http_handler *srch; + nni_mtx_lock(&s->mtx); + NNI_LIST_FOREACH (&s->handlers, srch) { + if (srch == h) { + nni_list_remove(&s->handlers, h); + h->refcnt--; + rv = 0; + break; + } + } + nni_mtx_unlock(&s->mtx); + return (rv); +} + +// Very limited MIME type map. Used only if the handler does not +// supply it's own. +static struct content_map { + const char *ext; + const char *typ; +} content_map[] = { + // clang-format off + { ".ai", "application/postscript" }, + { ".aif", "audio/aiff" }, + { ".aiff", "audio/aiff" }, + { ".avi", "video/avi" }, + { ".au", "audio/basic" }, + { ".bin", "application/octet-stream" }, + { ".bmp", "image/bmp" }, + { ".css", "text/css" }, + { ".eps", "application/postscript" }, + { ".gif", "image/gif" }, + { ".htm", "text/html" }, + { ".html", "text/html" }, + { ".ico", "image/x-icon" }, + { ".jpeg", "image/jpeg" }, + { ".jpg", "image/jpeg" }, + { ".js", "application/javascript" }, + { ".md", "text/markdown" }, + { ".mp2", "video/mpeg" }, + { ".mp3", "audio/mpeg3" }, + { ".mpeg", "video/mpeg" }, + { ".mpg", "video/mpeg" }, + { ".pdf", "application/pdf" }, + { ".png", "image/png" }, + { ".ps", "application/postscript" }, + { ".rtf", "text/rtf" }, + { ".text", "text/plain" }, + { ".tif", "image/tiff" }, + { ".tiff", "image/tiff" }, + { ".txt", "text/plain" }, + { ".wav", "audio/wav"}, + { "README", "text/plain" }, + { NULL, NULL }, + // clang-format on +}; + +const char * +http_lookup_type(const char *path) +{ + size_t l1 = strlen(path); + for (int i = 0; content_map[i].ext != NULL; i++) { + size_t l2 = strlen(content_map[i].ext); + if (l2 > l1) { + continue; + } + if (nni_strcasecmp(&path[l1 - l2], content_map[i].ext) == 0) { + return (content_map[i].typ); + } + } + return (NULL); +} + +typedef struct http_file { + char *path; + char *ctype; +} http_file; + +static void +http_handle_file(nni_aio *aio) +{ + nni_http_handler *h = nni_aio_get_input(aio, 1); + nni_http_res * res = NULL; + void * data; + size_t size; + int rv; + http_file * hf = nni_http_handler_get_data(h); + char * path = nni_http_handler_get_data(h); + const char * ctype; + + if ((ctype = hf->ctype) == NULL) { + ctype = "application/octet-stream"; + } + + // This is a very simplistic file server, suitable only for small + // files. In the future we can use an AIO based file read, where + // we read files a bit at a time, or even mmap them, and serve + // them up chunkwise. Applications could even come up with their own + // caching version of the http handler. + if ((rv = nni_file_get(hf->path, &data, &size)) != 0) { + uint16_t status; + switch (rv) { + case NNG_ENOMEM: + status = NNG_HTTP_STATUS_INTERNAL_SERVER_ERROR; + break; + case NNG_ENOENT: + status = NNG_HTTP_STATUS_NOT_FOUND; + break; + case NNG_EPERM: + status = NNG_HTTP_STATUS_FORBIDDEN; + break; + default: + status = NNG_HTTP_STATUS_INTERNAL_SERVER_ERROR; + break; + } + if ((rv = nni_http_res_alloc_error(&res, status)) != 0) { + nni_aio_finish_error(aio, rv); + return; + } + nni_aio_set_output(aio, 0, res); + nni_aio_finish(aio, 0, 0); + return; + } + if (((rv = nni_http_res_alloc(&res)) != 0) || + ((rv = nni_http_res_set_status(res, NNG_HTTP_STATUS_OK)) != 0) || + ((rv = nni_http_res_set_header(res, "Content-Type", ctype)) != + 0) || + ((rv = nni_http_res_copy_data(res, data, size)) != 0)) { + if (res != NULL) { + nni_http_res_free(res); + } + nni_free(data, size); + nni_aio_finish_error(aio, rv); + return; + } + + nni_free(data, size); + nni_aio_set_output(aio, 0, res); + nni_aio_finish(aio, 0, 0); +} + +static void +http_file_free(void *arg) +{ + http_file *hf; + if ((hf = arg) != NULL) { + nni_strfree(hf->path); + nni_strfree(hf->ctype); + NNI_FREE_STRUCT(hf); + } +} + +int +nni_http_handler_init_file_ctype(nni_http_handler **hpp, const char *uri, + const char *path, const char *ctype) +{ + nni_http_handler *h; + http_file * hf; + int rv; + + if ((hf = NNI_ALLOC_STRUCT(hf)) == NULL) { + return (NNG_ENOMEM); + } + + // Later we might want to do this in the server side, if we support + // custom media type lists on a per-server basis. For now doing this + // here ensures that we don't have to lookup the type every time. + if (ctype == NULL) { + if ((ctype = http_lookup_type(path)) == NULL) { + ctype = "application/octet-stream"; + } + } + if (((hf->path = nni_strdup(path)) == NULL) || + ((hf->ctype = nni_strdup(ctype)) == NULL)) { + http_file_free(hf); + return (NNG_ENOMEM); + } + + if ((rv = nni_http_handler_init(&h, uri, http_handle_file)) != 0) { + http_file_free(hf); + return (rv); + } + + if ((rv = nni_http_handler_set_data(h, hf, http_file_free)) != 0) { + http_file_free(hf); + nni_http_handler_fini(h); + return (rv); + } + + *hpp = h; + return (0); +} + +int +nni_http_handler_init_file( + nni_http_handler **hpp, const char *uri, const char *path) +{ + return (nni_http_handler_init_file_ctype(hpp, uri, path, NULL)); +} + +static void +http_handle_dir(nni_aio *aio) +{ + nni_http_req * req = nni_aio_get_input(aio, 0); + nni_http_handler *h = nni_aio_get_input(aio, 1); + nni_http_res * res = NULL; + void * data; + size_t size; + int rv; + http_file * hf = nni_http_handler_get_data(h); + const char * path = hf->path; + const char * base = nni_http_handler_get_uri(h); // base uri + const char * uri = nni_http_req_get_uri(req); + const char * ctype; + char * dst; + size_t len; + size_t pnsz; + char * pn; + + len = strlen(base); + if ((strncmp(uri, base, len) != 0) || + ((uri[len] != 0) && (uri[len] != '/'))) { + // This should never happen! + nni_aio_finish_error(aio, NNG_EINVAL); + return; + } + + // simple worst case is every character in path is a seperator + // It's never actually that bad, because we we have /<something>/. + pnsz = (strlen(path) + strlen(uri) + 2) * strlen(NNG_PLATFORM_DIR_SEP); + pnsz += strlen("index.html") + 1; // +1 for term nul + + if ((pn = nni_alloc(pnsz)) == NULL) { + nni_aio_finish_error(aio, NNG_ENOMEM); + return; + } + + // make sure we have a "/" present. + strcpy(pn, path); + dst = pn + strlen(pn); + + if ((dst == pn) || (dst[-1] != '/')) { + *dst++ = '/'; + } + + for (uri = uri + len; *uri != '\0'; uri++) { + if (*uri == '/') { + strcpy(dst, NNG_PLATFORM_DIR_SEP); + dst += sizeof(NNG_PLATFORM_DIR_SEP) - 1; + } else { + *dst++ = *uri; + } + } + + *dst = '\0'; + + // This is a very simplistic file server, suitable only for small + // files. In the future we can use an AIO based file read, where + // we read files a bit at a time, or even mmap them, and serve + // them up chunkwise. Applications could even come up with their + // own caching version of the http handler. + + rv = 0; + if (nni_file_is_dir(pn)) { + sprintf(dst, "%s%s", NNG_PLATFORM_DIR_SEP, "index.html"); + if (!nni_file_is_file(pn)) { + pn[strlen(pn) - 1] = '\0'; // index.html -> index.htm + } + if (!nni_file_is_file(pn)) { + rv = NNG_ENOENT; + } + } + + if (rv == 0) { + rv = nni_file_get(pn, &data, &size); + } else { + data = NULL; + size = 0; + } + ctype = http_lookup_type(pn); + if (ctype == NULL) { + ctype = "application/octet-stream"; + } + + nni_free(pn, pnsz); + if (rv != 0) { + uint16_t status; + + switch (rv) { + case NNG_ENOMEM: + status = NNG_HTTP_STATUS_INTERNAL_SERVER_ERROR; + break; + case NNG_ENOENT: + status = NNG_HTTP_STATUS_NOT_FOUND; + break; + case NNG_EPERM: + status = NNG_HTTP_STATUS_FORBIDDEN; + break; + default: + status = NNG_HTTP_STATUS_INTERNAL_SERVER_ERROR; + break; + } + if ((rv = nni_http_res_alloc_error(&res, status)) != 0) { + nni_aio_finish_error(aio, rv); + return; + } + nni_aio_set_output(aio, 0, res); + nni_aio_finish(aio, 0, 0); + return; + } + + if (((rv = nni_http_res_alloc(&res)) != 0) || + ((rv = nni_http_res_set_status(res, NNG_HTTP_STATUS_OK)) != 0) || + ((rv = nni_http_res_set_header(res, "Content-Type", ctype)) != + 0) || + ((rv = nni_http_res_copy_data(res, data, size)) != 0)) { + if (res != NULL) { + nni_http_res_free(res); + } + nni_free(data, size); + nni_aio_finish_error(aio, rv); + return; + } + + nni_free(data, size); + nni_aio_set_output(aio, 0, res); + nni_aio_finish(aio, 0, 0); +} + +int +nni_http_handler_init_directory( + nni_http_handler **hpp, const char *uri, const char *path) +{ + http_file * hf; + nni_http_handler *h; + int rv; + char * p; + + if ((hf = NNI_ALLOC_STRUCT(hf)) == NULL) { + return (NNG_ENOMEM); + } + if ((hf->path = nni_strdup(path)) == NULL) { + NNI_FREE_STRUCT(hf); + return (NNG_ENOMEM); + } + + if ((rv = nni_http_handler_init(&h, uri, http_handle_dir)) != 0) { + http_file_free(hf); + return (rv); + } + + if (((rv = nni_http_handler_set_tree(h)) != 0) || + ((rv = nni_http_handler_set_data(h, hf, http_file_free)) != 0)) { + http_file_free(hf); + nni_http_handler_fini(h); + return (rv); + } + + *hpp = h; + return (0); +} + +typedef struct http_static { + void * data; + size_t size; + char * ctype; +} http_static; + +static void +http_handle_static(nni_aio *aio) +{ + http_static * hs; + const char * ctype; + nni_http_handler *h; + nni_http_res * r = NULL; + int rv; + + h = nni_aio_get_input(aio, 1); + hs = nni_http_handler_get_data(h); + + if ((ctype = hs->ctype) == NULL) { + ctype = "application/octet-stream"; + } + + if (((rv = nni_http_res_alloc(&r)) != 0) || + ((rv = nni_http_res_set_header(r, "Content-Type", ctype)) != 0) || + ((rv = nni_http_res_set_status(r, NNG_HTTP_STATUS_OK)) != 0) || + ((rv = nni_http_res_set_data(r, hs->data, hs->size)) != 0)) { + if (r != NULL) { + nni_http_res_free(r); + } + nni_aio_finish_error(aio, rv); + return; + } + + nni_aio_set_output(aio, 0, r); + nni_aio_finish(aio, 0, 0); +} + +static void +http_static_free(void *arg) +{ + http_static *hs; + + if ((hs = arg) != NULL) { + nni_free(hs->data, hs->size); + nni_strfree(hs->ctype); + NNI_FREE_STRUCT(hs); + } +} + +int +nni_http_handler_init_static(nni_http_handler **hpp, const char *uri, + const void *data, size_t size, const char *ctype) +{ + nni_http_handler *h; + int rv; + http_static * hs; + + if ((hs = NNI_ALLOC_STRUCT(hs)) == NULL) { + return (NNG_ENOMEM); + } + if (((hs->ctype = nni_strdup(ctype)) == NULL) || + ((size > 0) && ((hs->data = nni_alloc(size)) == NULL))) { + http_static_free(hs); + return (NNG_ENOMEM); + } + hs->size = size; + memcpy(hs->data, data, size); + + if ((rv = nni_http_handler_init(&h, uri, http_handle_static)) != 0) { + http_static_free(hs); + return (rv); + } + + if ((rv = nni_http_handler_set_data(h, hs, http_static_free)) != 0) { + http_static_free(hs); + nni_http_handler_fini(h); + return (rv); + } + + *hpp = h; + return (0); +} + +int +nni_http_server_set_tls(nni_http_server *s, nng_tls_config *tcfg) +{ +#ifdef NNG_SUPP_TLS + nng_tls_config *old; + nni_mtx_lock(&s->mtx); + if (s->starts) { + nni_mtx_unlock(&s->mtx); + return (NNG_EBUSY); + } + old = s->tls; + s->tls = tcfg; + if (tcfg) { + nni_tls_config_hold(tcfg); + } + nni_mtx_unlock(&s->mtx); + if (old) { + nni_tls_config_fini(old); + } + return (0); +#else + return (NNG_ENOTSUP); +#endif +} + +int +nni_http_server_get_tls(nni_http_server *s, nng_tls_config **tp) +{ +#ifdef NNG_SUPP_TLS + nni_mtx_lock(&s->mtx); + if (s->tls == NULL) { + nni_mtx_unlock(&s->mtx); + return (NNG_EINVAL); + } + *tp = s->tls; + nni_mtx_unlock(&s->mtx); + return (0); +#else + return (NNG_ENOTSUP); +#endif +} + +static int +http_server_sys_init(void) +{ + NNI_LIST_INIT(&http_servers, nni_http_server, node); + nni_mtx_init(&http_servers_lk); + return (0); +} + +static void +http_server_sys_fini(void) +{ + nni_mtx_fini(&http_servers_lk); +} |
