//
|
// Copyright 2020 Staysail Systems, Inc. <info@staysail.tech>
|
// Copyright 2018 Capitar IT Group BV <info@capitar.com>
|
// Copyright 2018 QXSoftware <lh563566994@126.com>
|
// Copyright 2019 Devolutions <info@devolutions.net>
|
// Copyright 2020 Dirac Research <robert.bielik@dirac.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 <stdlib.h>
|
#include <string.h>
|
|
#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 /<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)) {
|
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 <a href=\"%s\">%s</a>.",
|
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);
|
}
|