start work on json api
authorJeroen van der Heijden <jeroen@transceptor.technology>
Mon, 6 Jan 2020 15:44:20 +0000 (16:44 +0100)
committerJeroen van der Heijden <jeroen@transceptor.technology>
Mon, 6 Jan 2020 15:44:20 +0000 (16:44 +0100)
include/base64/base64.h [new file with mode: 0644]
include/qpjson/qpjson.h [new file with mode: 0644]
include/siri/api.h [new file with mode: 0644]
include/siri/cfg/cfg.h
include/siri/db/users.h
src/base64/base64.c [new file with mode: 0644]
src/qpjson/qpjson.c [new file with mode: 0644]
src/siri/api.c [new file with mode: 0644]
src/siri/db/users.c
src/siri/health.c
src/siri/siri.c

diff --git a/include/base64/base64.h b/include/base64/base64.h
new file mode 100644 (file)
index 0000000..b663b2c
--- /dev/null
@@ -0,0 +1,13 @@
+/*
+ * base64.h - Base64 encoding/decoding
+ */
+#ifndef SIRI_BASE64_H_
+#define SIRI_BASE64_H_
+
+#include <stddef.h>
+
+char * base64_decode(const void * data, size_t n, size_t * size);
+char * base64_encode(const void * data, size_t n, size_t * size);
+
+
+#endif  /* SIRI_BASE64_H_ */
diff --git a/include/qpjson/qpjson.h b/include/qpjson/qpjson.h
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/include/siri/api.h b/include/siri/api.h
new file mode 100644 (file)
index 0000000..8a57d4d
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * api.h - SiriDB HTTP API.
+ */
+#ifndef SIRI_API_H_
+#define SIRI_API_H_
+
+#include <lib/http_parser.h>
+#include <uv.h>
+
+#define SIRIDB_API_FLAG 1<<29
+
+typedef enum
+{
+    SIRIDB_API_STATE_NONE,
+    SIRIDB_API_STATE_CONTENT_TYPE,
+    SIRIDB_API_STATE_AUTHORIZATION,
+} siridb_api_state_t;
+
+typedef enum
+{
+    SIRIDB_API_CT_TEXT,
+    SIRIDB_API_CT_JSON,
+} siridb_api_content_t;
+
+typedef enum
+{
+    SIRIDB_API_FLAG_IS_CLOSED       =1<<0,
+    SIRIDB_API_FLAG_IN_USE          =1<<1,
+    SIRIDB_API_FLAG_JSON_BEAUTY     =1<<2,
+    SIRIDB_API_FLAG_JSON_UTF8       =1<<3,
+} siridb_api_flags_t;
+
+typedef struct siri_api_request_s siri_api_request_t;
+
+int siri_api_init(void);
+void siri_ali_close(siri_api_request_t * web_request);
+static inline _Bool siri_api_is_handle(uv_handle_t * handle);
+
+struct siri_api_request_s
+{
+    uint32_t ref_;  /* maps to sirnet_stream_t for cleanup */
+    siridb_api_flags_t flags;
+    siridb_api_state_t state;
+    siridb_api_content_t content_type;
+    size_t content_n;
+    uv_write_t req;
+    uv_stream_t uvstream;
+    http_parser parser;
+    char * content;
+    siridb_t * siridb;
+    siridb_user_t * user;
+};
+
+static inline _Bool siri_api_is_handle(uv_handle_t * handle)
+{
+    return
+        handle->type == UV_TCP &&
+        handle->data &&
+        (((siri_api_request_t *) handle->data)->ref_ & SIRIDB_API_FLAG);
+}
+
+#endif /* SIRI_API_H_ */
index 88a5d1ed57fa0417f59c7fa47c77ec61b6cddafb..d6215401cbb047f814689ffc021b4cda3a185445 100644 (file)
@@ -27,7 +27,7 @@ struct siri_cfg_s
     uint16_t max_open_files;
 
     uint16_t http_status_port;
-    uint16_t pad0_;
+    uint16_t http_api_port;
     uint8_t pipe_support;
     uint8_t ip_support;
     uint8_t shard_compression;
index db1a958f7b82d483637f13bc49fcefbad7c2d746..aae5818dc5bf2ce85acb2fad7060dabd2bb4a2ef 100644 (file)
@@ -23,6 +23,10 @@ siridb_user_t * siridb_users_get_user(
         siridb_t * siridb,
         const char * username,
         const char * password);
+siridb_user_t * siridb_users_get_user_from_basic(
+        siridb_t * siridb,
+        const char * data,
+        size_t n);
 int siridb_users_save(siridb_t * siridb);
 ssize_t siridb_users_get_file(char ** buffer, siridb_t * siridb);
 
diff --git a/src/base64/base64.c b/src/base64/base64.c
new file mode 100644 (file)
index 0000000..c64b9f9
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * base64.c - Base64 encoding/decoding
+ */
+#include <stdlib.h>
+
+
+static const int base64__idx[256] = {
+        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 0,  0,  0,  0,  0,  0,
+        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 0,  0,  0,  0,
+        0,  0,  0,  0,  0,  0,  0, 62, 63, 62, 62, 63, 52, 53, 54, 55, 56, 57,
+        58, 59, 60, 61,  0,  0,  0,  0,  0,  0,  0,  0,  1,  2,  3,  4,  5,  6,
+        7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
+        25,  0, 0,  0,  0, 63,  0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
+        37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
+};
+
+static const unsigned char base64__table[65] =
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+char * base64_decode(const void * data, size_t n, size_t * size)
+{
+    const unsigned char * p = data;
+    int pad = n > 0 && (n % 4 || p[n - 1] == '=');
+    size_t i, j, L = ((n + 3) / 4 - pad) * 4;
+    char * out = malloc((L / 4 * 3) + (pad
+            ? (n > 1 && (n % 4 == 3 || p[n - 2] != '=')) + 2
+            : 1));
+    if (!out)
+        return NULL;
+
+    for (i = 0, j = 0; i < L; i += 4)
+    {
+        int nn = base64__idx[p[i]] << 18 | \
+                base64__idx[p[i + 1]] << 12 | \
+                base64__idx[p[i + 2]] << 6 | \
+                base64__idx[p[i + 3]];
+        out[j++] = nn >> 16;
+        out[j++] = nn >> 8 & 0xFF;
+        out[j++] = nn & 0xFF;
+    }
+
+    if (pad)
+    {
+        int nn = base64__idx[p[L]] << 18 | base64__idx[p[L + 1]] << 12;
+        out[j++] = nn >> 16;
+
+        if (n > L + 2 && p[L + 2] != '=')
+        {
+            nn |= base64__idx[p[L + 2]] << 6;
+            out[j++] = nn >> 8 & 0xFF;
+        }
+    }
+
+    *size = j;
+    out[*size] = '\0';
+    return out;
+}
+
+
+char * base64_encode(const void * data, size_t n, size_t * size)
+{
+    unsigned char * out;
+    unsigned char * pos;
+    const unsigned char * end, * in;
+    size_t olen = 4 * ((n + 2) / 3); /* 3-byte blocks to 4-byte */
+
+    if (olen < n || !(out = malloc(olen + 1)))
+        /* integer overflow or allocation error */
+        return NULL;
+
+    end = data + n;
+    in = data;
+    pos = out;
+
+    while (end - in >= 3)
+    {
+        *pos++ = base64__table[in[0] >> 2];
+        *pos++ = base64__table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
+        *pos++ = base64__table[((in[1] & 0x0f) << 2) | (in[2] >> 6)];
+        *pos++ = base64__table[in[2] & 0x3f];
+        in += 3;
+    }
+
+    if (end - in)
+    {
+        *pos++ = base64__table[in[0] >> 2];
+        if (end - in == 1)
+        {
+            *pos++ = base64__table[(in[0] & 0x03) << 4];
+            *pos++ = '=';
+        }
+        else
+        {
+            *pos++ = base64__table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
+            *pos++ = base64__table[(in[1] & 0x0f) << 2];
+        }
+        *pos++ = '=';
+    }
+
+    *size = pos - out;
+    out[*size] = '\0';
+    return (char *) out;
+}
+
+
diff --git a/src/qpjson/qpjson.c b/src/qpjson/qpjson.c
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/siri/api.c b/src/siri/api.c
new file mode 100644 (file)
index 0000000..8e92dc8
--- /dev/null
@@ -0,0 +1,415 @@
+/*
+ * api.c
+ */
+#include <siri/api.h>
+#include <siri/siri.h>
+
+#define API__HEADER_MAX_SZ 256
+#define CONTENT_TYPE_JSON "application/json"
+
+static uv_tcp_t api__uv_server;
+static http_parser_settings api__settings;
+
+static const char api__content_type[2][20] = {
+        "text/plain",
+        "application/json",
+};
+
+static const char api__html_header[8][32] = {
+        "200 OK",
+        "400 Bad Request",
+        "401 Unauthorized",
+        "403 Forbidden",
+        "404 Not Found",
+        "415 Unsupported Media Type",
+        "500 Internal Server Error",
+        "503 Service Unavailable",
+};
+
+static const char api__default_body[8][30] = {
+        "OK\r\n",
+        "BAD REQUEST\r\n",
+        "UNAUTHORIZED\r\n",
+        "FORBIDDEN\r\n",
+        "NOT FOUND\r\n",
+        "UNSUPPORTED MEDIA TYPE\r\n",
+        "INTERNAL SERVER ERROR\r\n",
+        "SERVICE UNAVAILABLE\r\n",
+};
+
+typedef enum
+{
+    E200_OK,
+    E400_BAD_REQUEST,
+    E401_UNAUTHORIZED,
+    E403_FORBIDDEN,
+    E404_NOT_FOUND,
+    E415_UNSUPPORTED_MEDIA_TYPE,
+    E500_INTERNAL_SERVER_ERROR,
+    E503_SERVICE_UNAVAILABLE
+} api__header_t;
+
+static inline _Bool api__starts_with(
+        const char ** str,
+        size_t * strn,
+        const char * with,
+        size_t withn)
+{
+    if (*strn < withn || strncasecmp(*str, with, withn))
+        return false;
+    *str += withn;
+    *strn -= withn;
+    return true;
+}
+
+static void api__close_cb(uv_handle_t * handle)
+{
+    siri_api_request_t * ar = handle->data;
+    if (ar->siridb)
+        siridb_decref(ar->siridb);
+    if (ar->user)
+        siridb_user_decref(ar->user);
+    free(ar->content);
+    free(ar);
+}
+
+static void api__alloc_cb(
+        uv_handle_t * UNUSED(handle),
+        size_t UNUSED(sugsz),
+        uv_buf_t * buf)
+{
+    buf->base = malloc(HTTP_MAX_HEADER_SIZE);
+    buf->len = buf->base ? HTTP_MAX_HEADER_SIZE-1 : 0;
+}
+
+static void api__data_cb(
+        uv_stream_t * uvstream,
+        ssize_t n,
+        const uv_buf_t * buf)
+{
+    size_t parsed;
+    siri_api_request_t * ar = uvstream->data;
+
+    if (ar->flags & SIRIDB_API_FLAG_IS_CLOSED)
+        goto done;
+
+    if (n < 0)
+    {
+        if (n != UV_EOF)
+            log_error(uv_strerror(n));
+
+        ti_api_close(ar);
+        goto done;
+    }
+
+    buf->base[HTTP_MAX_HEADER_SIZE-1] = '\0';
+
+    parsed = http_parser_execute(
+            &ar->parser,
+            &api__settings,
+            buf->base, n);
+
+    if (ar->parser.upgrade)
+    {
+        /* TODO: do we need to do something? */
+        log_debug("upgrade to a new protocol");
+    }
+    else if (parsed != (size_t) n)
+    {
+        log_warning("error parsing HTTP API request");
+        ti_api_close(ar);
+    }
+
+done:
+     free(buf->base);
+}
+
+static int api__headers_complete_cb(http_parser * parser)
+{
+    siri_api_request_t * ar = parser->data;
+
+    assert (!ar->content);
+
+    ar->content = malloc(parser->content_length);
+    if (ar->content)
+        ar->content_n = parser->content_length;
+
+    return 0;
+}
+
+static int api__url_cb(http_parser * parser, const char * at, size_t n)
+{
+    siri_api_request_t * ar = parser->data;
+
+
+
+    return 0;
+}
+
+static void api__connection_cb(uv_stream_t * server, int status)
+{
+    int rc;
+    siri_api_request_t * ar;
+
+    if (status < 0)
+    {
+        log_error("HTTP API connection error: `%s`", uv_strerror(status));
+        return;
+    }
+
+    log_debug("received a HTTP API call");
+
+    ar = calloc(1, sizeof(siri_api_request_t));
+    if (!ar)
+    {
+        ERR_ALLOC
+        return;
+    }
+
+    (void) uv_tcp_init(siri.loop, (uv_tcp_t *) &ar->uvstream);
+
+    ar->flags |= SIRIDB_API_FLAG;
+    ar->uvstream.data = ar;
+    ar->parser.data = ar;
+
+    rc = uv_accept(server, &ar->uvstream);
+    if (rc)
+    {
+        log_error("cannot accept HTTP API request: `%s`", uv_strerror(rc));
+        ti_api_close(ar);
+        return;
+    }
+
+    http_parser_init(&ar->parser, HTTP_REQUEST);
+
+    rc = uv_read_start(&ar->uvstream, api__alloc_cb, api__data_cb);
+    if (rc)
+    {
+        log_error("cannot read HTTP API request: `%s`", uv_strerror(rc));
+        ti_api_close(ar);
+        return;
+    }
+}
+
+static int api__header_field_cb(http_parser * parser, const char * at, size_t n)
+{
+    siri_api_request_t * ar = parser->data;
+
+    if (API__ICMP_WITH(at, n, "content-type"))
+    {
+        ar->state = SIRIDB_API_STATE_CONTENT_TYPE;
+        return 0;
+    }
+
+    if (API__ICMP_WITH(at, n, "authorization"))
+    {
+        ar->state = SIRIDB_API_STATE_AUTHORIZATION;
+        return 0;
+    }
+
+    ar->state = SIRIDB_API_STATE_NONE;
+    return 0;
+}
+
+static int api__header_value_cb(http_parser * parser, const char * at, size_t n)
+{
+    siri_api_request_t * ar = parser->data;
+
+    switch (ar->state)
+    {
+    case SIRIDB_API_STATE_NONE:
+        break;
+
+    case SIRIDB_API_STATE_CONTENT_TYPE:
+        if (API__ICMP_WITH(at, n, CONTENT_TYPE_JSON))
+        {
+            ar->content_type = SIRIDB_API_CT_JSON;
+            break;
+        }
+        if (API__ICMP_WITH(at, n, "text/json"))
+        {
+            ar->content_type = SIRIDB_API_CT_JSON;
+            break;
+        }
+
+        /* invalid content type */
+        log_debug("unsupported content-type: %.*s", (int) n, at);
+        break;
+
+    case SIRIDB_API_STATE_AUTHORIZATION:
+        if (api__starts_with(&at, &n, "token ", strlen("token ")))
+        {
+            log_debug("token authorization is not supported yet");
+        }
+
+        if (api__starts_with(&at, &n, "basic ", strlen("basic ")))
+        {
+            ar->user = siridb_users_get_user_from_basic(ar->siridb, at, n);
+
+            if (ar->user)
+                siridb_user_incref(ar->user);
+
+            break;
+        }
+
+        log_debug("invalid authorization type: %.*s", (int) n, at);
+        break;
+    }
+    return 0;
+}
+
+static int api__body_cb(http_parser * parser, const char * at, size_t n)
+{
+    size_t offset;
+    siri_api_request_t * ar = parser->data;
+
+    if (!n || !ar->content_n)
+        return 0;
+
+    offset = ar->content_n - (parser->content_length + n);
+    assert (offset + n <= ar->content_n);
+    memcpy(ar->content + offset, at, n);
+
+    return 0;
+}
+
+static void api__write_cb(uv_write_t * req, int status)
+{
+    if (status)
+        log_error(
+                "error writing HTTP API response: `%s`",
+                uv_strerror(status));
+
+    ti_api_close((siri_api_request_t *) req->handle->data);
+}
+
+static int api__plain_response(siri_api_request_t * ar, const api__header_t ht)
+{
+    const char * body = api__default_body[ht];
+    char header[API__HEADER_MAX_SZ];
+    size_t body_size;
+    int header_size;
+
+    body_size = strlen(body);
+    header_size = api__header(header, ht, SIRIDB_API_CT_TEXT, body_size);
+    if (header_size > 0)
+    {
+        uv_buf_t uvbufs[2] = {
+                uv_buf_init(header, (size_t) header_size),
+                uv_buf_init((char *) body, body_size),
+        };
+
+        (void) uv_write(
+                &ar->req,
+                &ar->uvstream,
+                uvbufs, 2,
+                api__write_cb);
+        return 0;
+    }
+    return -1;
+}
+
+static int api__message_complete_cb(http_parser * parser)
+{
+    siri_api_request_t * ar = parser->data;
+
+    if (!ar->user)
+        return api__plain_response(ar, E401_UNAUTHORIZED);
+
+    switch (ar->content_type)
+    {
+    case SIRIDB_API_CT_JSON:
+    {
+        char * data;
+        size_t size;
+        if (qpjson_json_to_qp(ar->content, ar->content_n, &data, &size))
+            return api__plain_response(ar, E400_BAD_REQUEST);
+
+        free(ar->content);
+        ar->content = data;
+        ar->content_n = size;
+    }
+    }
+
+    return api__plain_response(ar, E500_INTERNAL_SERVER_ERROR);
+}
+
+static int api__chunk_header_cb(http_parser * parser)
+{
+    LOGC("Chunk header\n Content-Length: %zu", parser->content_length);
+    return 0;
+}
+
+static int api__chunk_complete_cb(http_parser * parser)
+{
+    LOGC("Chunk complete\n Content-Length: %zu", parser->content_length);
+    return 0;
+}
+
+int siri_api_init(void)
+{
+    int rc;
+    struct sockaddr_storage addr = {0};
+    uint16_t port = siri.cfg->http_api_port;
+
+    if (port == 0)
+        return 0;
+
+    (void) uv_ip6_addr("::", (int) port, (struct sockaddr_in6 *) &addr);
+
+    api__settings.on_url = api__url_cb;
+    api__settings.on_header_field = api__header_field_cb;
+    api__settings.on_header_value = api__header_value_cb;
+    api__settings.on_message_complete = api__message_complete_cb;
+    api__settings.on_body = api__body_cb;
+    api__settings.on_chunk_header = api__chunk_header_cb;
+    api__settings.on_chunk_complete = api__chunk_complete_cb;
+    api__settings.on_headers_complete = api__headers_complete_cb;
+
+    if (
+        (rc = uv_tcp_init(siri.loop, &api__uv_server)) ||
+        (rc = uv_tcp_bind(
+                &api__uv_server,
+                (const struct sockaddr *) &addr,
+                0)) ||
+        (rc = uv_listen(
+                (uv_stream_t *) &api__uv_server,
+                128,
+                api__connection_cb)))
+    {
+        log_error("error initializing HTTP API on port %u: `%s`",
+                port,
+                uv_strerror(rc));
+        return -1;
+    }
+
+    log_info("start listening for HTTP API requests on TCP port %u", port);
+    return 0;
+}
+
+siri_api_request_t * siri_api_acquire(siri_api_request_t * ar)
+{
+    ar->flags |= SIRIDB_API_FLAG_IN_USE;
+    return ar;
+}
+
+void ti_api_release(siri_api_request_t * ar)
+{
+    ar->flags &= ~SIRIDB_API_FLAG_IN_USE;
+
+    if (ar->flags & SIRIDB_API_FLAG_IS_CLOSED)
+        uv_close((uv_handle_t *) &ar->uvstream, api__close_cb);
+}
+
+void ti_api_close(siri_api_request_t * ar)
+{
+    if (!ar || (ar->flags & SIRIDB_API_FLAG_IS_CLOSED))
+        return;
+
+    ar->flags |= SIRIDB_API_FLAG_IS_CLOSED;
+
+    if (ar->flags & SIRIDB_API_FLAG_IN_USE)
+        return;
+
+    uv_close((uv_handle_t *) &ar->uvstream, api__close_cb);
+}
index 8a8d663e6a1f06f6d3c720d587386de7c4b284f9..85e5de45297e7640270935a30073754ac4f6dd1c 100644 (file)
@@ -194,7 +194,7 @@ int siridb_users_add_user(
 }
 
 /*
- * Returns NULL when the user is not found of when the given password is
+ * Returns NULL when the user is not found or when the given password is
  * incorrect. When *password is NULL the password will NOT be checked and
  * the user will be returned when found.
  */
@@ -249,6 +249,28 @@ siridb_user_t * siridb_users_get_user(
     return NULL;
 }
 
+siridb_user_t * siridb_users_get_user_from_basic(
+        siridb_t * siridb,
+        const char * data,
+        size_t n)
+{
+    size_t size;
+    char * b64 = base64_decode(data, n, &size);
+
+    for (size_t nn = 0, end = size; n < end; ++nn)
+    {
+        if (b64[nn] == ':')
+        {
+            b64[nn] = '\0';
+
+            if (++nn > end)
+                return NULL;
+
+            return siridb_users_get_user(siridb, b64, b64 + nn);
+        }
+    }
+}
+
 /*
  * We get and remove the user in one code block so we do not need a dropped
  * flag on the user object.
index 79d6e1523873773ba440a912ff9a25784dce865d..b32744dd738b02cf2159ecd3291354dfafc15e3b 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * health.h
+ * health.c
  */
 #include <siri/health.h>
 #include <siri/siri.h>
index 2b521798f3ce73127a96ac8d012518ee911b0ec4..6049816a5d9fe83f4620cd4ba4a9416b80285362 100644 (file)
@@ -30,6 +30,7 @@
 #include <siri/db/server.h>
 #include <siri/db/servers.h>
 #include <siri/db/users.h>
+#include <siri/api.h>
 #include <siri/err.h>
 #include <siri/health.h>
 #include <siri/help/help.h>
@@ -167,6 +168,7 @@ int siri_start(void)
 
     /* initialize the back-end-, client- server and load databases */
     if (    (siri.cfg->http_status_port && (rc = siri_health_init())) ||
+            (siri.cfg->http_api_port && (rc = siri_api_init())) ||
             (rc = siri_service_account_init(&siri)) ||
             (rc = siri_service_request_init()) ||
             (rc = sirinet_bserver_init(&siri)) ||
@@ -512,6 +514,10 @@ static void SIRI_walk_close_handlers(
             {
                 siri_health_close((siri_health_request_t *) handle->data);
             }
+            else if (siri_api_is_handle(handle))
+            {
+                siri_api_close((siri_api_request_t *) handle->data);
+            }
             else
             {
                 sirinet_stream_decref((sirinet_stream_t *) handle->data);