--- /dev/null
+/* ez-http.c: HTTP processing support routines.
+
+ Copyright (C) 2021 Michael Zucchi
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see
+ <http://www.gnu.org/licenses/>.
+*/
+
+#define _GNU_SOURCE
+
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <ctype.h>
+#include <sys/uio.h>
+#include <netdb.h>
+#include <signal.h>
+
+#include <stdlib.h>
+#include "ez-blob-io.h"
+#include "ez-list.h"
+#include "ez-set.h"
+
+#define obstack_chunk_alloc malloc
+#define obstack_chunk_free free
+#include <obstack.h>
+#include <stdio.h>
+#include <errno.h>
+#define D(x)
+
+#include "ez-http.h"
+
+static void blobio_init_read(struct ez_blobio *io, const void *data, size_t size) {
+ io->index = 0;
+ io->data = (void *)data;
+ io->size = size;
+ io->alloc = 0;
+ io->error = 0;
+ io->mode = BLOBIO_READ;
+}
+
+static void init_request(struct ez_httprequest *r, struct ez_httpconnection *c) {
+ memset(r, 0, sizeof(*r));
+
+ r->http.conn = c;
+ ez_list_init(&r->http.headers);
+ ez_list_init(&r->params);
+}
+
+static void init_response(struct ez_httpresponse *r, struct ez_httpconnection *c) {
+ memset(r, 0, sizeof(*r));
+
+ r->http.conn = c;
+ ez_list_init(&r->http.headers);
+}
+
+void httpresponse_set_response(struct ez_httpresponse *r, int code, const char *msg) {
+ r->code = code;
+ r->message = obstack_copy(&r->http.conn->os, msg, strlen(msg)+1);
+}
+
+/*
+ Call repeatedly to scan headers
+
+ This swallows all of the data in io each call, either copying it to the headers
+ or to http.data
+
+ states of return is:
+ 1* (data.size = 0)
+ 2 (data.size >= 0)
+ 1* (data.size >= 0)
+
+ return -1 error
+ return 0 finished
+ return 1 call again
+ return 2 headers complete, may include start of data in http.data
+ */
+
+enum {
+ STATE_HEADER,
+ STATE_CR,
+ STATE_CONTENT,
+};
+
+static int parse_http(struct ez_http *r, struct ez_blobio * __restrict io) {
+ struct obstack *os = &r->conn->os;
+
+ while (r->state != STATE_CONTENT) {
+ size_t size = io->size - io->index;
+ const uint8_t *s = memchr(io->data + io->index, '\n', size);
+ if (s) {
+ size_t len = s - (io->data + io->index);
+
+ if (len > 0 && s[-1] == '\r')
+ obstack_grow0(os, blobio_take(io, len + 1), len - 1);
+ else if (len == 0 && r->state == STATE_CR)
+ obstack_grow0(os, blobio_take(io, 1), 0);
+ else
+ return -1;
+
+ len = obstack_object_size(os) - 1;
+ if (len) {
+ char *value = obstack_finish(os);
+
+ if (!value)
+ return -1;
+
+ if (!r->status) {
+ r->status = value;
+ } else {
+ struct ez_pair *h = obstack_alloc(os, sizeof(*h));
+
+ if (!h)
+ return -1;
+
+ h->name = value;
+ h->value = strchr(value, ':');
+ if (h->value) {
+ *(h->value)++ = 0;
+ if (strcmp(h->name, "Content-Length") == 0)
+ r->content_length = strtoul(h->value, NULL, 10);
+ } else {
+ // error?
+ }
+ ez_list_addtail(&r->headers, h);
+ }
+ r->state = STATE_HEADER;
+ } else {
+ // loses a byte, ignore
+ obstack_finish(os);
+
+ r->state = STATE_CONTENT;
+
+ // new 'read-all-in' version
+ r->data.index = 0;
+ r->data.data = obstack_alloc(os, r->content_length);
+ if (!r->data.data)
+ return -1;
+ r->data.size = r->content_length;
+ r->data.alloc = r->content_length;
+ r->data.mode = BLOBIO_READ;
+
+ // swallow all we have up to limit
+ size_t size = io->size - io->index;
+ size_t max = r->data.size - r->data.index;
+ size_t len = size > max ? max : size;
+
+ memcpy(r->data.data + r->data.index, blobio_take(io, len), len);
+ r->data.index += len;
+ }
+ } else {
+ if (io->size > 0 && io->index < io->size && io->data[io->size-1] == '\r') {
+ r->state = STATE_CR;
+ obstack_grow(os, blobio_take(io, size), size-1);
+ } else {
+ r->state = STATE_HEADER;
+ obstack_grow(os, blobio_take(io, size), size);
+ }
+ return 1;
+ }
+ }
+
+ return 2;
+}
+
+// this assumes the nybble is a valid char, worst that happens is you get junk
+static int nybble(int c) {
+ if (c <= '9')
+ return c - '0';
+ else if (c <= 'F')
+ return c - 'A' + 10;
+ else
+ return c - 'a' + 10;
+}
+
+// url decode in-place
+static void url_decode(char *s) {
+ char *o = s;
+ char c;
+
+ while ((c = *s++)) {
+ if (c == '%' && s[0] && s[1]) {
+ c = (nybble(s[0]) << 4) | nybble(s[1]);
+ s += 2;
+ } else if (c == '+')
+ c = ' ';
+ *o++ = c;
+ }
+ *o = 0;
+}
+
+static int tohex(int nybble) {
+ return nybble < 10 ? nybble + '0' : nybble + 'a' - 10;
+}
+
+// see tables.c
+#define UCHAR 0x01
+#define HEX 0x02
+const uint8_t type[256] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01,
+ 0x00, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+static __inline__ int is_uchar(char c) {
+ return (type[(unsigned)c] & UCHAR) != 0;
+}
+
+static void url_encode(struct obstack *os, const char *s) {
+ char c;
+ // TODO: utf8?
+ while ((c = *s++)) {
+ if (is_uchar(c))
+ obstack_1grow(os, c);
+ else if (c == ' ')
+ obstack_1grow(os, '+');
+ else {
+ obstack_1grow(os, '%');
+ obstack_1grow(os, tohex((c>>4)&0x0f));
+ obstack_1grow(os, tohex(c&0x0f));
+ }
+ }
+}
+
+// parse response VERSION CODE REASON
+static int parse_response_status(struct ez_httpresponse *r) {
+ char *code, *message;
+ code = strchr(r->http.status, ' ');
+ if (code) {
+ *code++ = 0;
+ message = strchr(code, ' ');
+ if (message)
+ *message++ = 0;
+ else
+ return -1;
+ } else
+ return -1;
+
+ r->version = r->http.status;
+ r->code = atoi(code);
+ r->message = message;
+ return 0;
+}
+
+// parse request METHOD URL?params VERSION
+static int parse_request_status(struct ez_httprequest *r) {
+ char *url, *version;
+ url = strchr(r->http.status, ' ');
+ if (url) {
+ *url++ = 0;
+ version = strchr(url, ' ');
+ if (version)
+ *version++ = 0;
+ else
+ return -1;
+ } else
+ return -1;
+
+ r->method = r->http.status;
+ r->url = url;
+ r->version = version;
+
+ // parse the url parameters if there are any
+ // this overwwirtes r->url[contents]
+ char *s = strchr(url, '?'), *t, *u;
+ if (s) {
+ struct obstack *os = &r->http.conn->os;
+
+ *s++ = 0;
+ do {
+ t = strchr(s, '&');
+ if (t) *t++ = 0;
+ u = strchr(s, '=');
+ if (u) {
+ *u++ = 0;
+ url_decode(u);
+ }
+ url_decode(s);
+
+ struct ez_pair *h = obstack_alloc(os, sizeof(*h));
+ h->name = s;
+ h->value = u;
+ ez_list_addtail(&r->params, h);
+ s = t;
+ } while (t);
+ }
+ return 0;
+}
+
+static unsigned handler_hash(const void *a) {
+ return ez_hash_string(((struct ez_httphandler *)a)->path);
+}
+
+static int handler_equals(const void *a, const void *b) {
+ return strcmp(((struct ez_httphandler *)a)->path, ((struct ez_httphandler *)b)->path) == 0;
+}
+
+// read all data, data may be null (== noop)
+int httpconnection_read(struct ez_httpconnection *s, struct ez_blobio *data) {
+ if (data) {
+ ssize_t len = data->size - data->index;
+
+ while (len > 0
+ && (len = read(s->fd, data->data + data->index, len)) >= 0) {
+ printf("read:\n");
+ fwrite(data->data + data->index, 1, len, stdout);
+ printf("\n--\n");
+ data->index += len;
+ len = data->size - data->index;
+ }
+ // reset index for reading
+ data->index = 0;
+ return len;
+ } else
+ return 0;
+}
+
+// write all data, data may be null (== noop)
+// return 0 on success
+int httpconnection_write(struct ez_httpconnection *s, struct ez_blobio *data) {
+ if (data) {
+ ssize_t len = data->size - data->index;
+
+ while (len > 0
+ && (len = write(s->fd, data->data + data->index, len)) >= 0) {
+ printf("send:\n");
+ fwrite(data->data + data->index, 1, len, stdout);
+ printf("\n--\n");
+ data->index += len;
+ len = data->size - data->index;
+ }
+ return len;
+ } else
+ return 0;
+}
+
+// read headers and all data into memory
+static int read_http(struct ez_http *http) {
+ struct ez_httpconnection *s = http->conn;
+ int res;
+
+ do {
+ ssize_t len = read(s->fd, s->io.data, s->io.alloc);
+
+ if (len < 0)
+ return -1;
+ s->io.index = 0;
+ s->io.size = len;
+
+ printf("read:\n");
+ fwrite(s->io.data, 1, len, stdout);
+ printf("\n--\n");
+
+ res = parse_http(http, &s->io);
+ if (res == 2)
+ return httpconnection_read(s, &http->data);
+ } while (res > 0);
+ return -1;
+}
+
+// Format and write headers
+static int write_headers(struct ez_http *http) {
+ struct obstack *os = &http->conn->os;
+ struct ez_blobio msg = { 0 };
+ int res;
+
+ http->content_length = http->data.size - http->data.index;
+
+ for (struct ez_pair *w = ez_list_head(&http->headers), *n = ez_node_succ(w);n;w=n,n = ez_node_succ(w))
+ obstack_printf(os, "%s:%s\r\n", w->name, w->value);
+ obstack_printf(os, "Content-Length:%zd\r\n\r\n", http->content_length);
+
+ msg.size = obstack_object_size(os);
+ msg.data = obstack_finish(os);
+ if (msg.data) {
+ res = httpconnection_write(http->conn, &msg);
+ obstack_free(os, msg.data);
+ } else {
+ const char *oom = "HTTP/1.0 500 Out of Memory";
+
+ msg.size = strlen(oom);
+ msg.data = (void *)oom;
+
+ httpconnection_write(http->conn, &msg);
+
+ res = -1;
+ }
+
+ return res;
+}
+
+// Send a response
+static int httpresponse_run(struct ez_httpresponse *r) {
+ struct obstack *os = &r->http.conn->os;
+ int res;
+
+ obstack_printf(os, "HTTP/1.0 %d %s\r\n", r->code, r->message);
+ res = write_headers(&r->http);
+ if (res == 0)
+ res = httpconnection_write(r->http.conn, &r->http.data);
+
+ return res;
+}
+
+int httpserver_init(struct ez_httpserver *s, int port) {
+ memset(s, 0, sizeof(*s));
+
+ ez_set_init(&s->handlers, handler_hash, handler_equals, NULL);
+
+ memset(&s->addr, 0, sizeof(s->addr));
+ s->addr.sin_family = AF_INET;
+ s->addr.sin_addr.s_addr = INADDR_ANY;
+ s->addr.sin_port = htons(port);
+
+ return 0;
+}
+
+void httpserver_free(struct ez_httpserver *s) {
+ ez_set_clear(&s->handlers);
+}
+
+int httpconnection_init(struct ez_httpconnection *conn, struct ez_httpserver *s) {
+ memset(conn, 0, sizeof(*conn));
+
+ conn->server = s;
+ conn->io.alloc = 4096;
+ conn->io.data = malloc(conn->io.alloc);
+ conn->io.mode = BLOBIO_READ;
+
+ obstack_init(&conn->os);
+ conn->empty = obstack_alloc(&conn->os, 0);
+
+ return 0;
+}
+
+void httpconnection_clear(struct ez_httpconnection *conn) {
+ // close fd?
+ obstack_free(&conn->os, conn->empty);
+ conn->empty = obstack_alloc(&conn->os, 0);
+}
+
+void httpconnection_free(struct ez_httpconnection *conn) {
+ obstack_free(&conn->os, NULL);
+ free(conn->io.data);
+ memset(conn, 0, sizeof(*conn));
+}
+
+void httpserver_addhandlers(struct ez_httpserver *s, struct ez_httphandler *list, int count) {
+ for (int i=0;i<count;i++)
+ ez_set_put(&s->handlers, &list[i]);
+}
+
+int httpserver_run(struct ez_httpserver *s) {
+ int res;
+
+ signal(SIGPIPE, SIG_IGN);
+
+ s->fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (s->fd < 0) {
+ perror("ERROR opening socket");
+ return -1;
+ }
+
+ res = 1;
+ res = setsockopt(s->fd, SOL_SOCKET, SO_REUSEADDR, &res, sizeof(int));
+
+ res = bind(s->fd, (struct sockaddr *)&s->addr, sizeof(s->addr));
+ if (res != 0)
+ goto fail;
+
+ struct ez_httpconnection conn;
+
+ httpconnection_init(&conn, s);
+ int quit = 0;
+
+ while (!quit) {
+ socklen_t alen = sizeof(conn.addr);
+ struct ez_httprequest req;
+ struct ez_httpresponse rep;
+
+ // Wait for new connection
+ res = listen(s->fd, 16);
+ conn.fd = accept(s->fd, (struct sockaddr *)&conn.addr, &alen);
+
+ if (1) {
+ struct timeval timeout;
+ timeout.tv_sec = 5;
+ timeout.tv_usec = 0;
+
+ setsockopt(conn.fd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout));
+ setsockopt(conn.fd, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout));
+ }
+
+ // Default response
+ init_response(&rep, &conn);
+ rep.code = 500;
+ rep.message = "Server Error";
+
+ // Read headers and data, and map to handlers
+ init_request(&req, &conn);
+ res = read_http(&req.http);
+
+ if (res == 0) {
+ res = parse_request_status(&req);
+ if (res == 0) {
+ const struct ez_httphandler key = { .path = req.url };
+ const struct ez_httphandler *handler = ez_set_get(&s->handlers, &key);
+
+ if (!handler)
+ quit = handler->fn(&req, &rep);
+ else
+ httpresponse_set_response(&rep, 404, "Missing");
+ } else
+ httpresponse_set_response(&rep, 400, "Invalid");
+ } else
+ httpresponse_set_response(&rep, 400, "Invalid");
+
+ httpresponse_run(&rep);
+
+ close(conn.fd);
+
+ httpconnection_clear(&conn);
+ }
+ httpconnection_free(&conn);
+ res = 0;
+fail:
+ signal(SIGPIPE, SIG_DFL);
+
+ close(s->fd);
+ s->fd = -1;
+
+ return res;
+}
+
+int httpclient_init(struct ez_httpclient *cc, const char *host, int port) {
+ struct hostent *server;
+ struct ez_httpconnection *c = &cc->conn;
+
+ httpconnection_init(c, NULL);
+
+ server = gethostbyname(host);
+ memset(&c->addr, 0, sizeof(c->addr));
+ c->addr.sin_family = AF_INET;
+ c->addr.sin_port = htons(port);
+ memcpy(&c->addr.sin_addr.s_addr, server->h_addr, server->h_length);
+
+ init_request(&cc->request, &cc->conn);
+
+ return 0;
+}
+
+void httpclient_free(struct ez_httpclient *cc) {
+ httpconnection_free(&cc->conn);
+}
+
+void httprequest_addparams(struct ez_httprequest *h, struct ez_pair *list, int count) {
+ for (int i=0;i<count;i++)
+ ez_list_addtail(&h->params, &list[i]);
+}
+
+void http_addheaders(struct ez_http *http, struct ez_pair *list, int count) {
+ for (int i=0;i<count;i++)
+ ez_list_addtail(&http->headers, &list[i]);
+}
+
+void http_set_data(struct ez_http *http, const char *data, size_t size) {
+ blobio_init_read(&http->data, data, size);
+}
+
+// simple send/receive in-memory version
+// send request header and all data
+// read response header and all data
+int httpclient_run(struct ez_httpclient *cc, const char *method, const char *url) {
+ int res;
+ struct ez_httpconnection *c = &cc->conn;
+ struct ez_httprequest *req = &cc->request;
+ struct ez_httpresponse *rep = &cc->response;
+ struct obstack *os = &c->os;
+
+ c->fd = socket(AF_INET, SOCK_STREAM, 0);
+ res = connect(c->fd, (struct sockaddr *)&c->addr, sizeof(c->addr));
+
+ if (1) {
+ struct timeval timeout;
+ timeout.tv_sec = 5;
+ timeout.tv_usec = 0;
+
+ setsockopt(c->fd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout));
+ setsockopt(c->fd, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout));
+ }
+
+ // from request status line, method, url
+ req->method = obstack_copy(os, method, strlen(method)+1);
+ req->url = obstack_copy(os, url, strlen(url)+1);
+
+ obstack_printf(os, "%s %s", method, url);
+ char next = '?';
+
+ // add encoded parameters
+ for (struct ez_pair *w = ez_list_head(&req->params), *n = ez_node_succ(w);n;w=n,n = ez_node_succ(w)) {
+ obstack_1grow(os, next);
+ url_encode(os, w->name);
+ if (w->value) {
+ obstack_1grow(os, '=');
+ url_encode(os, w->value);
+ }
+ next = '&';
+ }
+ // and version
+ obstack_printf(os, " HTTP/1.0\r\n");
+
+ // write headers and data
+ res = write_headers(&req->http);
+ if (res == 0) {
+ res = httpconnection_write(c, &req->http.data);
+
+ if (res == 0) {
+ // fully read response
+ init_response(rep, req->http.conn);
+ res = read_http(&rep->http);
+ if (res == 0)
+ res = parse_response_status(rep);
+ }
+ }
+
+ close(c->fd);
+ c->fd = -1;
+
+ return res;
+}