diff options
Diffstat (limited to 'src/supplemental/http/http_msg.c')
| -rw-r--r-- | src/supplemental/http/http_msg.c | 956 |
1 files changed, 956 insertions, 0 deletions
diff --git a/src/supplemental/http/http_msg.c b/src/supplemental/http/http_msg.c new file mode 100644 index 00000000..2e245868 --- /dev/null +++ b/src/supplemental/http/http_msg.c @@ -0,0 +1,956 @@ +// +// Copyright 2017 Staysail Systems, Inc. <info@staysail.tech> +// Copyright 2017 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 <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "core/nng_impl.h" +#include "http.h" + +// Note that as we parse headers, the rule is that if a header is already +// present, then we can append it to the existing header, separated by +// a comma. From experience, for example, Firefox uses a Connection: +// header with two values, "keepalive", and "upgrade". +typedef struct http_header { + char * name; + char * value; + nni_list_node node; +} http_header; + +struct nni_http_entity { + char * data; + size_t size; // allocated/expected size + size_t len; // current length + bool own; // if true, data is "ours", and should be freed +}; + +struct nni_http_req { + nni_list hdrs; + nni_http_entity data; + char * meth; + char * uri; + char * vers; + char * buf; + size_t bufsz; +}; + +struct nni_http_res { + nni_list hdrs; + nni_http_entity data; + int code; + char * rsn; + char * vers; + char * buf; + size_t bufsz; +}; + +static int +http_set_string(char **strp, const char *val) +{ + char *news; + if ((news = nni_strdup(val)) == NULL) { + return (NNG_ENOMEM); + } + nni_strfree(*strp); + *strp = news; + return (0); +} + +static void +http_headers_reset(nni_list *hdrs) +{ + http_header *h; + while ((h = nni_list_first(hdrs)) != NULL) { + nni_list_remove(hdrs, h); + if (h->name != NULL) { + nni_strfree(h->name); + } + if (h->value != NULL) { + nni_free(h->value, strlen(h->value) + 1); + } + NNI_FREE_STRUCT(h); + } +} + +static void +http_entity_reset(nni_http_entity *entity) +{ + if (entity->own && entity->size) { + nni_free(entity->data, entity->size); + } + entity->data = NULL; + entity->size = 0; + entity->own = false; +} + +void +nni_http_req_reset(nni_http_req *req) +{ + http_headers_reset(&req->hdrs); + http_entity_reset(&req->data); + nni_strfree(req->vers); + nni_strfree(req->meth); + nni_strfree(req->uri); + req->vers = req->meth = req->uri = NULL; + if (req->bufsz) { + req->buf[0] = '\0'; + } +} + +void +nni_http_res_reset(nni_http_res *res) +{ + http_headers_reset(&res->hdrs); + http_entity_reset(&res->data); + nni_strfree(res->rsn); + nni_strfree(res->vers); + res->code = 0; + if (res->bufsz) { + res->buf[0] = '\0'; + } +} + +void +nni_http_req_fini(nni_http_req *req) +{ + nni_http_req_reset(req); + if (req->bufsz) { + nni_free(req->buf, req->bufsz); + } + NNI_FREE_STRUCT(req); +} + +void +nni_http_res_fini(nni_http_res *res) +{ + nni_http_res_reset(res); + if (res->bufsz) { + nni_free(res->buf, res->bufsz); + } + NNI_FREE_STRUCT(res); +} + +static int +http_del_header(nni_list *hdrs, const char *key) +{ + http_header *h; + NNI_LIST_FOREACH (hdrs, h) { + if (strcasecmp(key, h->name) == 0) { + nni_list_remove(hdrs, h); + nni_strfree(h->name); + nni_free(h->value, strlen(h->value) + 1); + NNI_FREE_STRUCT(h); + return (0); + } + } + return (NNG_ENOENT); +} + +int +nni_req_del_header(nni_http_req *req, const char *key) +{ + return (http_del_header(&req->hdrs, key)); +} + +int +nni_res_del_header(nni_http_res *res, const char *key) +{ + return (http_del_header(&res->hdrs, key)); +} + +static int +http_set_header(nni_list *hdrs, const char *key, const char *val) +{ + http_header *h; + NNI_LIST_FOREACH (hdrs, h) { + if (strcasecmp(key, h->name) == 0) { + char * news; + size_t len = strlen(val) + 1; + if ((news = nni_alloc(len)) == NULL) { + return (NNG_ENOMEM); + } + snprintf(news, len, "%s", val); + nni_free(h->value, strlen(h->value) + 1); + h->value = news; + return (0); + } + } + + if ((h = NNI_ALLOC_STRUCT(h)) == NULL) { + return (NNG_ENOMEM); + } + if ((h->name = nni_strdup(key)) == NULL) { + NNI_FREE_STRUCT(h); + return (NNG_ENOMEM); + } + if ((h->value = nni_alloc(strlen(val) + 1)) == NULL) { + nni_strfree(h->name); + NNI_FREE_STRUCT(h); + return (NNG_ENOMEM); + } + strncpy(h->value, val, strlen(val) + 1); + nni_list_append(hdrs, h); + return (0); +} + +int +nni_http_req_set_header(nni_http_req *req, const char *key, const char *val) +{ + return (http_set_header(&req->hdrs, key, val)); +} + +int +nni_http_res_set_header(nni_http_res *res, const char *key, const char *val) +{ + return (http_set_header(&res->hdrs, key, val)); +} + +static int +http_add_header(nni_list *hdrs, const char *key, const char *val) +{ + http_header *h; + NNI_LIST_FOREACH (hdrs, h) { + if (strcasecmp(key, h->name) == 0) { + char * news; + size_t len = strlen(h->value) + strlen(val) + 3; + if ((news = nni_alloc(len)) == NULL) { + return (NNG_ENOMEM); + } + snprintf(news, len, "%s, %s", h->value, val); + nni_free(h->value, strlen(h->value) + 1); + h->value = news; + return (0); + } + } + + if ((h = NNI_ALLOC_STRUCT(h)) == NULL) { + return (NNG_ENOMEM); + } + if ((h->name = nni_strdup(key)) == NULL) { + NNI_FREE_STRUCT(h); + return (NNG_ENOMEM); + } + if ((h->value = nni_alloc(strlen(val) + 1)) == NULL) { + nni_strfree(h->name); + NNI_FREE_STRUCT(h); + return (NNG_ENOMEM); + } + strncpy(h->value, val, strlen(val) + 1); + nni_list_append(hdrs, h); + return (0); +} + +int +nni_http_req_add_header(nni_http_req *req, const char *key, const char *val) +{ + return (http_add_header(&req->hdrs, key, val)); +} + +int +nni_http_res_add_header(nni_http_res *res, const char *key, const char *val) +{ + return (http_add_header(&res->hdrs, key, val)); +} + +static const char * +http_get_header(nni_list *hdrs, const char *key) +{ + http_header *h; + NNI_LIST_FOREACH (hdrs, h) { + if (strcasecmp(h->name, key) == 0) { + return (h->value); + } + } + return (NULL); +} + +const char * +nni_http_req_get_header(nni_http_req *req, const char *key) +{ + return (http_get_header(&req->hdrs, key)); +} + +const char * +nni_http_res_get_header(nni_http_res *res, const char *key) +{ + return (http_get_header(&res->hdrs, key)); +} + +// http_entity_set_data sets the entity, but does not update the +// content-length header. +static void +http_entity_set_data(nni_http_entity *entity, const void *data, size_t size) +{ + if (entity->own) { + nni_free(entity->data, entity->size); + } + entity->data = (void *) data; + entity->size = size; + entity->own = false; +} + +static int +http_entity_alloc_data(nni_http_entity *entity, size_t size) +{ + void *newdata; + if ((newdata = nni_alloc(size)) == NULL) { + return (NNG_ENOMEM); + } + http_entity_set_data(entity, newdata, size); + entity->own = true; + return (0); +} + +static int +http_entity_copy_data(nni_http_entity *entity, const void *data, size_t size) +{ + int rv; + if ((rv = http_entity_alloc_data(entity, size)) == 0) { + memcpy(entity->data, data, size); + } + return (rv); +} + +static int +http_set_content_length(nni_http_entity *entity, nni_list *hdrs) +{ + char buf[16]; + (void) snprintf(buf, sizeof(buf), "%u", (unsigned) entity->size); + return (http_set_header(hdrs, "Content-Length", buf)); +} + +static void +http_entity_get_data(nni_http_entity *entity, void **datap, size_t *sizep) +{ + *datap = entity->data; + *sizep = entity->size; +} + +void +nni_http_req_get_data(nni_http_req *req, void **datap, size_t *sizep) +{ + http_entity_get_data(&req->data, datap, sizep); +} + +void +nni_http_res_get_data(nni_http_res *res, void **datap, size_t *sizep) +{ + http_entity_get_data(&res->data, datap, sizep); +} + +int +nni_http_req_set_data(nni_http_req *req, const void *data, size_t size) +{ + int rv; + + http_entity_set_data(&req->data, data, size); + if ((rv = http_set_content_length(&req->data, &req->hdrs)) != 0) { + http_entity_set_data(&req->data, NULL, 0); + } + return (rv); +} + +int +nni_http_res_set_data(nni_http_res *res, const void *data, size_t size) +{ + int rv; + + http_entity_set_data(&res->data, data, size); + if ((rv = http_set_content_length(&res->data, &res->hdrs)) != 0) { + http_entity_set_data(&res->data, NULL, 0); + } + return (rv); +} + +int +nni_http_req_copy_data(nni_http_req *req, const void *data, size_t size) +{ + int rv; + + if (((rv = http_entity_copy_data(&req->data, data, size)) != 0) || + ((rv = http_set_content_length(&req->data, &req->hdrs)) != 0)) { + http_entity_set_data(&req->data, NULL, 0); + return (rv); + } + return (0); +} + +int +nni_http_res_copy_data(nni_http_res *res, const void *data, size_t size) +{ + int rv; + + if (((rv = http_entity_copy_data(&res->data, data, size)) != 0) || + ((rv = http_set_content_length(&res->data, &res->hdrs)) != 0)) { + http_entity_set_data(&res->data, NULL, 0); + return (rv); + } + return (0); +} + +int +nni_http_res_alloc_data(nni_http_res *res, size_t size) +{ + return (http_entity_alloc_data(&res->data, size)); +} + +static int +http_parse_header(nni_list *hdrs, void *line) +{ + http_header *h; + char * key = line; + char * val; + char * end; + + // Find separation between key and value + if ((val = strchr(key, ':')) == NULL) { + return (NNG_EPROTO); + } + + // Trim leading and trailing whitespace from header + *val = '\0'; + val++; + while (*val == ' ' || *val == '\t') { + val++; + } + end = val + strlen(val); + end--; + while ((end > val) && (*end == ' ' || *end == '\t')) { + *end = '\0'; + end--; + } + + return (http_add_header(hdrs, key, val)); +} + +// http_sprintf_headers makes headers for an HTTP request or an HTTP response +// object. Each header is dumped from the list. If the buf is NULL, +// or the sz is 0, then a dryrun is done, in order to allow the caller to +// determine how much space is needed. Returns the size of the space needed, +// not including the terminating NULL byte. Truncation occurs if the size +// returned is >= the requested size. +static size_t +http_sprintf_headers(char *buf, size_t sz, nni_list *list) +{ + size_t rv = 0; + http_header *h; + + if (buf == NULL) { + sz = 0; + } + + NNI_LIST_FOREACH (list, h) { + size_t l; + l = snprintf(buf, sz, "%s: %s\r\n", h->name, h->value); + if (buf != NULL) { + buf += l; + } + sz = (sz > l) ? sz - l : 0; + rv += l; + } + return (rv); +} + +static int +http_asprintf(char **bufp, size_t *szp, nni_list *hdrs, const char *fmt, ...) +{ + va_list ap; + size_t len; + size_t n; + char * buf; + + va_start(ap, fmt); + len = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + + len += http_sprintf_headers(NULL, 0, hdrs); + len += 5; // \r\n\r\n\0 + + if (len <= *szp) { + buf = *bufp; + } else { + if ((buf = nni_alloc(len)) == NULL) { + return (NNG_ENOMEM); + } + nni_free(*bufp, *szp); + *bufp = buf; + *szp = len; + } + va_start(ap, fmt); + n = vsnprintf(buf, len, fmt, ap); + va_end(ap); + buf += n; + len -= n; + n = http_sprintf_headers(buf, len, hdrs); + buf += n; + len -= n; + snprintf(buf, len, "\r\n"); + return (0); +} + +static int +http_req_prepare(nni_http_req *req) +{ + int rv; + if ((req->uri == NULL) || (req->meth == NULL)) { + return (NNG_EINVAL); + } + rv = http_asprintf(&req->buf, &req->bufsz, &req->hdrs, "%s %s %s\r\n", + req->meth, req->uri, req->vers != NULL ? req->vers : "HTTP/1.1"); + return (rv); +} + +static int +http_res_prepare(nni_http_res *res) +{ + int rv; + rv = http_asprintf(&res->buf, &res->bufsz, &res->hdrs, "%s %d %s\r\n", + res->vers != NULL ? res->vers : "HTTP/1.1", res->code, + res->rsn != NULL ? res->rsn : "Unknown Error"); + return (rv); +} + +int +nni_http_req_get_buf(nni_http_req *req, void **data, size_t *szp) +{ + int rv; + + if ((req->buf == NULL) && (rv = http_req_prepare(req)) != 0) { + return (rv); + } + *data = req->buf; + *szp = strlen(req->buf); + return (0); +} + +int +nni_http_res_get_buf(nni_http_res *res, void **data, size_t *szp) +{ + int rv; + + if ((res->buf == NULL) && (rv = http_res_prepare(res)) != 0) { + return (rv); + } + *data = res->buf; + *szp = strlen(res->buf); + return (0); +} + +int +nni_http_req_init(nni_http_req **reqp) +{ + nni_http_req *req; + if ((req = NNI_ALLOC_STRUCT(req)) == NULL) { + return (NNG_ENOMEM); + } + NNI_LIST_INIT(&req->hdrs, http_header, node); + req->buf = NULL; + req->bufsz = 0; + req->data.data = NULL; + req->data.size = 0; + req->data.own = false; + req->vers = NULL; + req->meth = NULL; + req->uri = NULL; + *reqp = req; + return (0); +} + +int +nni_http_res_init(nni_http_res **resp) +{ + nni_http_res *res; + if ((res = NNI_ALLOC_STRUCT(res)) == NULL) { + return (NNG_ENOMEM); + } + NNI_LIST_INIT(&res->hdrs, http_header, node); + res->buf = NULL; + res->bufsz = 0; + res->data.data = NULL; + res->data.size = 0; + res->data.own = false; + res->vers = NULL; + res->rsn = NULL; + res->code = 0; + *resp = res; + return (0); +} + +const char * +nni_http_req_get_method(nni_http_req *req) +{ + return (req->meth); +} + +const char * +nni_http_req_get_uri(nni_http_req *req) +{ + return (req->uri); +} + +const char * +nni_http_req_get_version(nni_http_req *req) +{ + return (req->vers); +} + +const char * +nni_http_res_get_version(nni_http_res *res) +{ + return (res->vers); +} + +int +nni_http_req_set_version(nni_http_req *req, const char *vers) +{ + return (http_set_string(&req->vers, vers)); +} + +int +nni_http_res_set_version(nni_http_res *res, const char *vers) +{ + return (http_set_string(&res->vers, vers)); +} + +int +nni_http_req_set_uri(nni_http_req *req, const char *uri) +{ + return (http_set_string(&req->uri, uri)); +} + +int +nni_http_req_set_method(nni_http_req *req, const char *meth) +{ + return (http_set_string(&req->meth, meth)); +} + +int +nni_http_res_set_status(nni_http_res *res, int status, const char *reason) +{ + int rv; + if ((rv = http_set_string(&res->rsn, reason)) == 0) { + res->code = status; + } + return (rv); +} + +int +nni_http_res_get_status(nni_http_res *res) +{ + return (res->code); +} + +const char * +nni_http_res_get_reason(nni_http_res *res) +{ + return (res->rsn); +} + +static int +http_scan_line(void *vbuf, size_t n, size_t *lenp) +{ + size_t len; + char lc; + char * buf = vbuf; + + lc = 0; + for (len = 0; len < n; len++) { + char c = buf[len]; + if (c == '\n') { + // Technically we should be receiving CRLF, but + // debugging is easier with just LF, so we behave + // following Postel's Law. + if (lc != '\r') { + buf[len] = '\0'; + } else { + buf[len - 1] = '\0'; + } + *lenp = len + 1; + return (0); + } + // If we have a control character (other than CR), or a CR + // followed by anything other than LF, then its an error. + if (((c < ' ') && (c != '\r')) || (lc == '\r')) { + return (NNG_EPROTO); + } + lc = c; + } + // Scanned the entire content, but did not find a line. + return (NNG_EAGAIN); +} + +static int +http_req_parse_line(nni_http_req *req, void *line) +{ + int rv; + char *method; + char *uri; + char *version; + + method = line; + if ((uri = strchr(method, ' ')) == NULL) { + return (NNG_EPROTO); + } + *uri = '\0'; + uri++; + + if ((version = strchr(uri, ' ')) == NULL) { + return (NNG_EPROTO); + } + *version = '\0'; + version++; + + if (((rv = nni_http_req_set_method(req, method)) != 0) || + ((rv = nni_http_req_set_uri(req, uri)) != 0) || + ((rv = nni_http_req_set_version(req, version)) != 0)) { + return (rv); + } + return (0); +} + +static int +http_res_parse_line(nni_http_res *res, uint8_t *line) +{ + int rv; + char *reason; + char *codestr; + char *version; + int status; + + version = (char *) line; + if ((codestr = strchr(version, ' ')) == NULL) { + return (NNG_EPROTO); + } + *codestr = '\0'; + codestr++; + + if ((reason = strchr(codestr, ' ')) == NULL) { + return (NNG_EPROTO); + } + *reason = '\0'; + reason++; + + status = atoi(codestr); + if ((status < 100) || (status > 999)) { + return (NNG_EPROTO); + } + + if (((rv = nni_http_res_set_status(res, status, reason)) != 0) || + ((rv = nni_http_res_set_version(res, version)) != 0)) { + return (rv); + } + return (0); +} + +// nni_http_req_parse parses a request (but not any attached entity data). +// The amount of data consumed is returned in lenp. Returns zero on +// success, NNG_EPROTO on parse failure, NNG_EAGAIN if more data is +// required, or NNG_ENOMEM on memory exhaustion. Note that lenp may +// be updated even in the face of errors (esp. NNG_EAGAIN, which is +// not an error so much as a request for more data.) +int +nni_http_req_parse(nni_http_req *req, void *buf, size_t n, size_t *lenp) +{ + + size_t len = 0; + size_t cnt; + int rv = 0; + + for (;;) { + uint8_t *line; + if ((rv = http_scan_line(buf, n, &cnt)) != 0) { + break; + } + + len += cnt; + line = buf; + buf = line + cnt; + n -= cnt; + + if (*line == '\0') { + break; + } + + if (req->vers != NULL) { + rv = http_parse_header(&req->hdrs, line); + } else { + rv = http_req_parse_line(req, line); + } + + if (rv != 0) { + break; + } + } + + *lenp = len; + return (rv); +} + +int +nni_http_res_parse(nni_http_res *res, void *buf, size_t n, size_t *lenp) +{ + + size_t len = 0; + size_t cnt; + int rv = 0; + for (;;) { + uint8_t *line; + if ((rv = http_scan_line(buf, n, &cnt)) != 0) { + break; + } + + len += cnt; + line = buf; + buf = line + cnt; + n -= cnt; + + if (*line == '\0') { + break; + } + + if (res->vers != NULL) { + rv = http_parse_header(&res->hdrs, line); + } else { + rv = http_res_parse_line(res, line); + } + + if (rv != 0) { + break; + } + } + + *lenp = len; + return (rv); +} + +int +nni_http_res_init_error(nni_http_res **resp, uint16_t err) +{ + char * rsn; + char rsnbuf[80]; + char html[1024]; + nni_http_res *res; + + if ((nni_http_res_init(&res)) != 0) { + return (NNG_ENOMEM); + } + + // Note that it is expected that redirect URIs will update the + // payload to reflect the target location. + switch (err) { + case NNI_HTTP_STATUS_STATUS_MOVED_PERMANENTLY: + rsn = "Moved Permanently"; + break; + case NNI_HTTP_STATUS_MULTIPLE_CHOICES: + rsn = "Multiple Choices"; + break; + case NNI_HTTP_STATUS_FOUND: + rsn = "Found"; + break; + case NNI_HTTP_STATUS_SEE_OTHER: + rsn = "See Other"; + break; + case NNI_HTTP_STATUS_TEMPORARY_REDIRECT: + rsn = "Temporary Redirect"; + break; + case NNI_HTTP_STATUS_BAD_REQUEST: + rsn = "Bad Request"; + break; + case NNI_HTTP_STATUS_UNAUTHORIZED: + rsn = "Unauthorized"; + break; + case NNI_HTTP_STATUS_PAYMENT_REQUIRED: + rsn = "Payment Required"; + break; + case NNI_HTTP_STATUS_NOT_FOUND: + rsn = "Not Found"; + break; + case NNI_HTTP_STATUS_METHOD_NOT_ALLOWED: + // Caller must also supply an Allow: header + rsn = "Method Not Allowed"; + break; + case NNI_HTTP_STATUS_NOT_ACCEPTABLE: + rsn = "Not Acceptable"; + break; + case NNI_HTTP_STATUS_REQUEST_TIMEOUT: + rsn = "Request Timeout"; + break; + case NNI_HTTP_STATUS_CONFLICT: + rsn = "Conflict"; + break; + case NNI_HTTP_STATUS_GONE: + rsn = "Gone"; + break; + case NNI_HTTP_STATUS_LENGTH_REQUIRED: + rsn = "Length Required"; + break; + case NNI_HTTP_STATUS_PAYLOAD_TOO_LARGE: + rsn = "Payload Too Large"; + break; + case NNI_HTTP_STATUS_FORBIDDEN: + rsn = "Forbidden"; + break; + case NNI_HTTP_STATUS_URI_TOO_LONG: + rsn = "URI Too Long"; + break; + case NNI_HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE: + rsn = "Unsupported Media Type"; + break; + case NNI_HTTP_STATUS_EXPECTATION_FAILED: + rsn = "Expectation Failed"; + break; + case NNI_HTTP_STATUS_UPGRADE_REQUIRED: + // Caller must add "Upgrade:" header. + rsn = "Upgrade Required"; + break; + case NNI_HTTP_STATUS_INTERNAL_SERVER_ERROR: + rsn = "Internal Server Error"; + break; + case NNI_HTTP_STATUS_HTTP_VERSION_NOT_SUPP: + rsn = "HTTP version not supported"; + break; + case NNI_HTTP_STATUS_NOT_IMPLEMENTED: + rsn = "Not Implemented"; + break; + case NNI_HTTP_STATUS_SERVICE_UNAVAILABLE: + rsn = "Service Unavailable"; + break; + default: + snprintf(rsnbuf, sizeof(rsnbuf), "HTTP error code %d", err); + rsn = rsnbuf; + break; + } + + // very simple builtin error page + snprintf(html, sizeof(html), + "<head><title>%d %s</title></head>" + "<body><p/><h1 align=\"center\">" + "<span style=\"font-size: 36px; border-radius: 5px; " + "background-color: black; color: white; padding: 7px; " + "font-family: Arial, sans serif;\">%d</span></h1>" + "<p align=\"center\">" + "<span style=\"font-size: 24px; font-family: Arial, sans serif;\">" + "%s</span></p></body>", + err, rsn, err, rsn); + + nni_http_res_set_status(res, err, rsn); + nni_http_res_copy_data(res, html, strlen(html)); + nni_http_res_set_version(res, "HTTP/1.1"); + nni_http_res_set_header( + res, "Content-Type", "text/html; charset=UTF-8"); + // We could set the date, but we don't necessarily have a portable + // way to get the time of day. + + *resp = res; + return (0); +} |
