// // Copyright 2020 Staysail Systems, Inc. // Copyright 2018 Capitar IT Group BV // Copyright 2018 QXSoftware // Copyright 2019 Devolutions // Copyright 2020 Dirac Research // // 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 #include #include #include #include #include "core/nng_impl.h" #include "nng/supplemental/tls/tls.h" #include "http_api.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 * uri; char * method; char * host; nng_sockaddr host_addr; bool host_ip; bool tree; bool tree_exclusive; nni_atomic_u64 ref; nni_atomic_bool busy; 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; nni_http_handler *handler; // set if we deferred to read body nni_http_handler *release; // set if we dispatched handler bool close; bool closed; bool finished; nni_aio * cbaio; nni_aio * rxaio; nni_aio * txaio; nni_aio * txdataio; nni_reap_node reap; } http_sconn; typedef struct http_error { nni_list_node node; uint16_t code; void * body; size_t len; } http_error; 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; nni_aio * accaio; nng_stream_listener *listener; int port; // native order char * hostname; nni_list errors; nni_mtx errors_mtx; nni_reap_node reap; }; static void http_sc_reap(void *); static nni_reap_list http_sc_reap_list = { .rl_offset = offsetof(http_sconn, reap), .rl_func = http_sc_reap, }; static void http_server_fini(nni_http_server *); static nni_reap_list http_server_reap_list = { .rl_offset = offsetof(nni_http_server, reap), .rl_func = (nni_cb) http_server_fini, }; int nni_http_handler_init( nni_http_handler **hp, const char *uri, void (*cb)(nni_aio *)) { nni_http_handler *h; if ((h = NNI_ALLOC_STRUCT(h)) == NULL) { return (NNG_ENOMEM); } nni_atomic_init64(&h->ref); nni_atomic_inc64(&h->ref); // Default for HTTP is /. But remap it to "" for ease of matching. if ((uri == NULL) || (strlen(uri) == 0) || (strcmp(uri, "/") == 0)) { uri = ""; } if (((h->uri = nni_strdup(uri)) == 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->tree_exclusive = false; h->maxbody = 1024 * 1024; // By default we accept up to 1MB of body h->getbody = true; *hp = h; return (0); } // nni_http_handler_fini just drops the reference count, only destroying // the handler if the reference drops to zero. void nni_http_handler_fini(nni_http_handler *h) { if (nni_atomic_dec64_nv(&h->ref) != 0) { return; } if (h->dtor != NULL) { h->dtor(h->data); } nni_strfree(h->host); nni_strfree(h->uri); nni_strfree(h->method); 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) { if (nni_atomic_get_bool(&h->busy)) { 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) { if (strlen(h->uri) == 0) { return ("/"); } return (h->uri); } int nni_http_handler_set_tree(nni_http_handler *h) { if (nni_atomic_get_bool(&h->busy) != 0) { return (NNG_EBUSY); } h->tree = true; h->tree_exclusive = false; return (0); } int nni_http_handler_set_tree_exclusive(nni_http_handler *h) { if (nni_atomic_get_bool(&h->busy) != 0) { return (NNG_EBUSY); } h->tree = true; h->tree_exclusive = true; return (0); } int nni_http_handler_set_host(nni_http_handler *h, const char *host) { char *dup; if (nni_atomic_get_bool(&h->busy) != 0) { return (NNG_EBUSY); } if ((host == NULL) || (strcmp(host, "*") == 0) || strcmp(host, "") == 0) { nni_strfree(h->host); h->host = NULL; return (0); } if (nni_parse_ip(host, &h->host_addr) == 0) { uint8_t wild[16] = { 0 }; // Check for wild card addresses. switch (h->host_addr.s_family) { case NNG_AF_INET: if (h->host_addr.s_in.sa_addr == 0) { nni_strfree(h->host); h->host = NULL; return (0); } break; case NNG_AF_INET6: if (memcmp(h->host_addr.s_in6.sa_addr, wild, 16) == 0) { nni_strfree(h->host); h->host = NULL; return (0); } break; } h->host_ip = true; } if ((dup = nni_strdup(host)) == NULL) { return (NNG_ENOMEM); } nni_strfree(h->host); h->host = dup; return (0); } int nni_http_handler_set_method(nni_http_handler *h, const char *method) { char *dup; if (nni_atomic_get_bool(&h->busy) != 0) { return (NNG_EBUSY); } if (method == NULL) { nni_strfree(h->method); h->method = NULL; return (0); } if ((dup = nni_strdup(method)) == NULL) { return (NNG_ENOMEM); } nni_strfree(h->method); h->method = dup; return (0); } static nni_list http_servers; static nni_mtx http_servers_lk; static void http_sc_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); } nni_http_req_free(sc->req); nni_http_res_free(sc->res); nni_aio_free(sc->rxaio); nni_aio_free(sc->txaio); nni_aio_free(sc->txdataio); nni_aio_free(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_sc_close_locked(http_sconn *sc) { nni_http_conn *conn; if (sc->closed) { return; } NNI_ASSERT(!sc->finished); sc->closed = true; nni_aio_close(sc->rxaio); nni_aio_close(sc->txaio); nni_aio_close(sc->txdataio); nni_aio_close(sc->cbaio); if ((conn = sc->conn) != NULL) { nni_http_conn_close(conn); } nni_reap(&http_sc_reap_list, sc); } static void http_sconn_close(http_sconn *sc) { nni_http_server *s; s = sc->server; nni_mtx_lock(&s->mtx); http_sc_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; } nni_http_res_free(sc->res); sc->res = NULL; if (sc->close) { http_sconn_close(sc); return; } sc->handler = NULL; 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; } 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); } 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'; return ((strlen(path) != 0) ? path : "/"); } static void http_sconn_error(http_sconn *sc, uint16_t err) { nni_http_res *res; if (nni_http_res_alloc(&res) != 0) { http_sconn_close(sc); return; } nni_http_res_set_status(res, err); if (nni_http_server_res_error(sc->server, res) != 0) { nni_http_res_free(res); http_sconn_close(sc); return; } if (sc->close) { if (nni_http_res_set_header(res, "Connection", "close") != 0) { nni_http_res_free(res); 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 bool http_handler_host_match(nni_http_handler *h, const char *host) { nng_sockaddr sa; size_t len; if (h->host == NULL) { return (true); } if (host == NULL) { // Virtual hosts not possible under HTTP/1.0 return (false); } if (h->host_ip) { if (nni_parse_ip_port(host, &sa) != 0) { return (false); } switch (h->host_addr.s_family) { case NNG_AF_INET: if ((sa.s_in.sa_family != NNG_AF_INET) || (sa.s_in.sa_addr != h->host_addr.s_in.sa_addr)) { return (false); } return (true); case NNG_AF_INET6: if (sa.s_in6.sa_family != NNG_AF_INET6) { return (false); } if (memcmp(sa.s_in6.sa_addr, h->host_addr.s_in6.sa_addr, 16) != 0) { return (false); } return (true); } } len = strlen(h->host); if ((nni_strncasecmp(host, h->host, len) != 0)) { return (false); } // 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'))) { return (false); } return (true); } 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; 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. 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 (!http_handler_host_match(h, host)) { continue; } len = strlen(h->uri); if (strncmp(path, h->uri, 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; } if ((h->getbody) && ((cls = nni_http_req_get_header(req, "Content-Length")) != NULL)) { uint64_t len; char * end; len = strtoull(cls, &end, 10); if ((end == NULL) || (*end != '\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->release = h; 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); // Set a reference -- this because the callback may be running // asynchronously even after it gets removed from the server. nni_atomic_inc64(&h->ref); // Documented that we call this on behalf of the callback. if (nni_aio_begin(sc->cbaio) != 0) { nni_mtx_unlock(&s->mtx); return; } 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; // Get the handler. It may be set regardless of success or // failure. Clear it, and drop our reference, since we're // done with the handler for now. if ((h = sc->release) != NULL) { sc->release = NULL; nni_http_handler_fini(h); } if (nni_aio_result(aio) != 0) { // Hard close, no further feedback. http_sconn_close(sc); return; } res = nni_aio_get_output(aio, 0); // If it's 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); } else if (nni_http_res_is_error(res)) { (void) nni_http_server_res_error(s, res); } 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. sc->handler = NULL; nni_http_req_reset(sc->req); nni_http_read_req(sc->conn, sc->req, sc->rxaio); } } static int http_sconn_init(http_sconn **scp, nng_stream *stream) { http_sconn *sc; int rv; if ((sc = NNI_ALLOC_STRUCT(sc)) == NULL) { nng_stream_free(stream); return (NNG_ENOMEM); } if (((rv = nni_http_req_alloc(&sc->req, NULL)) != 0) || ((rv = nni_aio_alloc(&sc->rxaio, http_sconn_rxdone, sc)) != 0) || ((rv = nni_aio_alloc(&sc->txaio, http_sconn_txdone, sc)) != 0) || ((rv = nni_aio_alloc(&sc->txdataio, http_sconn_txdatdone, sc)) != 0) || ((rv = nni_aio_alloc(&sc->cbaio, http_sconn_cbdone, sc)) != 0)) { // Can't even accept the incoming request. Hard close. http_sconn_close(sc); return (rv); } rv = nni_http_conn_init(&sc->conn, stream); 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; nng_stream * stream; http_sconn * sc; int rv; nni_mtx_lock(&s->mtx); if ((rv = nni_aio_result(aio)) != 0) { if (!s->closed) { // try again? nng_stream_listener_accept(s->listener, s->accaio); } nni_mtx_unlock(&s->mtx); return; } stream = nni_aio_get_output(aio, 0); if (s->closed) { // If we're closing, then reject this one. nng_stream_free(stream); nni_mtx_unlock(&s->mtx); return; } if (http_sconn_init(&sc, stream) != 0) { // The stream structure is already cleaned up. // Start another accept attempt. nng_stream_listener_accept(s->listener, s->accaio); nni_mtx_unlock(&s->mtx); return; } sc->server = s; nni_list_append(&s->conns, sc); sc->handler = NULL; nni_http_read_req(sc->conn, sc->req, sc->rxaio); nng_stream_listener_accept(s->listener, s->accaio); nni_mtx_unlock(&s->mtx); } static void http_server_fini(nni_http_server *s) { nni_http_handler *h; http_error * epage; nni_aio_stop(s->accaio); nni_mtx_lock(&s->mtx); if (!nni_list_empty(&s->conns)) { // Try to reap later, after the connections are done reaping. // (Note, connections will all have been closed already.) nni_reap(&http_server_reap_list, s); nni_mtx_unlock(&s->mtx); return; } nng_stream_listener_free(s->listener); while ((h = nni_list_first(&s->handlers)) != NULL) { nni_list_remove(&s->handlers, h); nni_http_handler_fini(h); } nni_mtx_unlock(&s->mtx); nni_mtx_lock(&s->errors_mtx); while ((epage = nni_list_first(&s->errors)) != NULL) { nni_list_remove(&s->errors, epage); nni_free(epage->body, epage->len); NNI_FREE_STRUCT(epage); } nni_mtx_unlock(&s->errors_mtx); nni_mtx_fini(&s->errors_mtx); nni_aio_free(s->accaio); nni_cv_fini(&s->cv); nni_mtx_fini(&s->mtx); nni_strfree(s->hostname); NNI_FREE_STRUCT(s); } static int http_server_init(nni_http_server **serverp, const nni_url *url) { nni_http_server *s; int rv; nng_url my_url; const char * scheme; if ((scheme = nni_http_stream_scheme(url->u_scheme)) == NULL) { return (NNG_EADDRINVAL); } // Rewrite URLs to either TLS or TCP. memcpy(&my_url, url, sizeof(my_url)); my_url.u_scheme = (char *) scheme; if ((s = NNI_ALLOC_STRUCT(s)) == NULL) { return (NNG_ENOMEM); } nni_mtx_init(&s->mtx); nni_mtx_init(&s->errors_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); nni_mtx_init(&s->errors_mtx); NNI_LIST_INIT(&s->errors, http_error, node); if ((rv = nni_aio_alloc(&s->accaio, http_server_acccb, s)) != 0) { http_server_fini(s); return (rv); } // NB: We only support number port numbers, and the URL framework // expands empty port numbers to 80 or 443 as appropriate. s->port = atoi(url->u_port); if ((s->hostname = nni_strdup(url->u_hostname)) == NULL) { http_server_fini(s); return (NNG_ENOMEM); } if ((rv = nng_stream_listener_alloc_url(&s->listener, &my_url)) != 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 ((!s->closed) && (atoi(url->u_port) == s->port) && (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; if ((rv = nng_stream_listener_listen(s->listener)) != 0) { return (rv); } if (s->port == 0) { nng_stream_listener_get_int( s->listener, NNG_OPT_TCP_BOUND_PORT, &s->port); } nng_stream_listener_accept(s->listener, 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; nni_aio_close(s->accaio); // Close the TCP endpoint that is listening. if (s->listener) { nng_stream_listener_close(s->listener); } // 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_sc_close_locked(sc); } while (!nni_list_empty(&s->conns)) { nni_cv_wait(&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); } static int http_server_set_err(nni_http_server *s, uint16_t code, void *body, size_t len) { http_error *epage; nni_mtx_lock(&s->errors_mtx); NNI_LIST_FOREACH (&s->errors, epage) { if (epage->code == code) { break; } } if (epage == NULL) { if ((epage = NNI_ALLOC_STRUCT(epage)) == NULL) { nni_mtx_unlock(&s->mtx); return (NNG_ENOMEM); } epage->code = code; nni_list_append(&s->errors, epage); } if (epage->len != 0) { nni_free(epage->body, epage->len); } epage->body = body; epage->len = len; nni_mtx_unlock(&s->errors_mtx); return (0); } int nni_http_server_set_error_page( nni_http_server *s, uint16_t code, const char *html) { char * body; int rv; size_t len; // We copy the content, without the trailing NUL. len = strlen(html); if ((body = nni_alloc(len)) == NULL) { return (NNG_ENOMEM); } memcpy(body, html, len); if ((rv = http_server_set_err(s, code, body, len)) != 0) { nni_free(body, len); } return (rv); } int nni_http_server_set_error_file( nni_http_server *s, uint16_t code, const char *path) { void * body; size_t len; int rv; if ((rv = nni_file_get(path, &body, &len)) != 0) { return (rv); } if ((rv = http_server_set_err(s, code, body, len)) != 0) { nni_free(body, len); } return (rv); } int nni_http_server_res_error(nni_http_server *s, nni_http_res *res) { http_error *epage; char * body = NULL; char * html = NULL; size_t len; uint16_t code = nni_http_res_get_status(res); int rv; nni_mtx_lock(&s->errors_mtx); NNI_LIST_FOREACH (&s->errors, epage) { if (epage->code == code) { body = epage->body; len = epage->len; break; } } nni_mtx_unlock(&s->errors_mtx); if (body == NULL) { if ((rv = nni_http_alloc_html_error(&html, code, NULL)) != 0) { return (rv); } body = html; len = strlen(body); } // NB: The server lock has to be held here to guard against the // error page being tossed or changed. if (((rv = nni_http_res_copy_data(res, body, len)) == 0) && ((rv = nni_http_res_set_header( res, "Content-Type", "text/html; charset=UTF-8")) == 0)) { nni_http_res_set_status(res, code); } nni_strfree(html); return (rv); } 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->uri)) > 0) && (h->uri[0] != '/')) || (h->cb == NULL)) { return (NNG_EINVAL); } while ((len > 0) && (h->uri[len - 1] == '/')) { len--; // ignore trailing '/' (this collapses them) } nni_mtx_lock(&s->mtx); // General rule for finding a conflict is that if either uri // string is an exact duplicate 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->uri); while ((len2 > 0) && (h2->uri[len2 - 1] == '/')) { len2--; // ignore trailing '/' } if ((h2->tree && h2->tree_exclusive) || (h->tree && h->tree_exclusive)) { // Old behavior if (strncmp(h->uri, h2->uri, len > len2 ? len2 : len) != 0) { continue; // prefixes don't match. } if (len2 > len) { if ((h2->uri[len] == '/') && (h->tree)) { nni_mtx_unlock(&s->mtx); return (NNG_EADDRINUSE); } } else if (len > len2) { if ((h->uri[len2] == '/') && (h2->tree)) { nni_mtx_unlock(&s->mtx); return (NNG_EADDRINUSE); } } else { nni_mtx_unlock(&s->mtx); return (NNG_EADDRINUSE); } } else { if (len != len2) { continue; // length mismatch } if (strcmp(h->uri, h2->uri) != 0) { continue; // not a duplicate } nni_mtx_unlock(&s->mtx); return (NNG_EADDRINUSE); } } // Maintain list of handlers in longest uri first order NNI_LIST_FOREACH (&s->handlers, h2) { size_t len2 = strlen(h2->uri); if (len > len2) { nni_list_insert_before(&s->handlers, h, h2); break; } } if (h2 == NULL) { nni_list_append(&s->handlers, h); } // Note that we have borrowed the reference count on the handler. // Thus we own it, and if the server is destroyed while we have it, // then we must finalize it it too. We do mark it busy so // that other settings cannot change. nni_atomic_set_bool(&h->busy, true); 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) { // NB: We are giving the caller our reference // on the handler. nni_list_remove(&s->handlers, h); 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); 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)) { 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); } // We don't permit a body for getting a file. nni_http_handler_collect_body(h, true, 0); *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 (base[1] != '\0' && // Allows "/" as base ((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 separator // It's never actually that bad, because we we have //. 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)) { 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; 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); } // 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_exclusive(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_redirect { uint16_t code; char * where; } http_redirect; static void http_handle_redirect(nni_aio *aio) { nni_http_res * r = NULL; char * html = NULL; char * msg = NULL; char * loc = NULL; http_redirect * hr; nni_http_handler *h; int rv; nni_http_req * req; const char * base; const char * uri; req = nni_aio_get_input(aio, 0); h = nni_aio_get_input(aio, 1); base = nni_http_handler_get_uri(h); // base uri uri = nni_http_req_get_uri(req); hr = nni_http_handler_get_data(h); // If we are doing a full tree, then include the entire suffix. if (strncmp(uri, base, strlen(base)) == 0) { rv = nni_asprintf(&loc, "%s%s", hr->where, uri + strlen(base)); if (rv != 0) { nni_aio_finish_error(aio, rv); return; } } else { loc = hr->where; } // Builtin redirect page rv = nni_asprintf(&msg, "You should be automatically redirected to %s.", loc, loc); // Build a response. We always close the connection for redirects, // because it is probably going to another server. This also // keeps us from having to consume the entity body, we can just // discard it. if ((rv != 0) || ((rv = nni_http_res_alloc(&r)) != 0) || ((rv = nni_http_alloc_html_error(&html, hr->code, msg)) != 0) || ((rv = nni_http_res_set_status(r, hr->code)) != 0) || ((rv = nni_http_res_set_header(r, "Connection", "close")) != 0) || ((rv = nni_http_res_set_header( r, "Content-Type", "text/html; charset=UTF-8")) != 0) || ((rv = nni_http_res_set_header(r, "Location", loc)) != 0) || ((rv = nni_http_res_copy_data(r, html, strlen(html))) != 0)) { if (loc != hr->where) { nni_strfree(loc); } nni_strfree(msg); nni_strfree(html); nni_http_res_free(r); nni_aio_finish_error(aio, rv); return; } if (loc != hr->where) { nni_strfree(loc); } nni_strfree(msg); nni_strfree(html); nni_aio_set_output(aio, 0, r); nni_aio_finish(aio, 0, 0); } static void http_redirect_free(void *arg) { http_redirect *hr; if ((hr = arg) != NULL) { nni_strfree(hr->where); NNI_FREE_STRUCT(hr); } } int nni_http_handler_init_redirect(nni_http_handler **hpp, const char *uri, uint16_t status, const char *where) { nni_http_handler *h; int rv; http_redirect * hr; if ((hr = NNI_ALLOC_STRUCT(hr)) == NULL) { return (NNG_ENOMEM); } if ((hr->where = nni_strdup(where)) == NULL) { NNI_FREE_STRUCT(hr); return (NNG_ENOMEM); } if (status == 0) { status = NNG_HTTP_STATUS_STATUS_MOVED_PERMANENTLY; } hr->code = status; if ((rv = nni_http_handler_init(&h, uri, http_handle_redirect)) != 0) { http_redirect_free(hr); return (rv); } if (((rv = nni_http_handler_set_method(h, NULL)) != 0) || ((rv = nni_http_handler_set_data(h, hr, http_redirect_free)) != 0)) { http_redirect_free(hr); nni_http_handler_fini(h); return (rv); } // We don't need to collect the body at all, because the handler // just discards the content and closes the connection. nni_http_handler_collect_body(h, false, 0); *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)) { 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); } // We don't permit a body for getting static data. nni_http_handler_collect_body(h, true, 0); *hpp = h; return (0); } int nni_http_server_set_tls(nni_http_server *s, nng_tls_config *tls) { return ( nng_stream_listener_set_ptr(s->listener, NNG_OPT_TLS_CONFIG, tls)); } int nni_http_server_get_tls(nni_http_server *s, nng_tls_config **tlsp) { return (nng_stream_listener_get_ptr( s->listener, NNG_OPT_TLS_CONFIG, (void **) tlsp)); } int nni_http_server_set(nni_http_server *s, const char *name, const void *buf, size_t sz, nni_type t) { // We have no local options, but we just pass them straight through. return (nni_stream_listener_set(s->listener, name, buf, sz, t)); } int nni_http_server_get( nni_http_server *s, const char *name, void *buf, size_t *szp, nni_type t) { return (nni_stream_listener_get(s->listener, name, buf, szp, t)); } void nni_http_server_fini(nni_http_server *s) { nni_mtx_lock(&http_servers_lk); s->refcnt--; if (s->refcnt == 0) { nni_mtx_lock(&s->mtx); http_server_stop(s); nni_mtx_unlock(&s->mtx); nni_list_remove(&http_servers, s); nni_reap(&http_server_reap_list, s); } nni_mtx_unlock(&http_servers_lk); } 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_reap_drain(); nni_mtx_fini(&http_servers_lk); }