about summary refs log tree commit diff stats
path: root/lua/sandborb/src
diff options
context:
space:
mode:
Diffstat (limited to 'lua/sandborb/src')
-rw-r--r--lua/sandborb/src/app.c95
-rw-r--r--lua/sandborb/src/sandbird.c979
-rw-r--r--lua/sandborb/src/sandbird.h91
3 files changed, 1165 insertions, 0 deletions
diff --git a/lua/sandborb/src/app.c b/lua/sandborb/src/app.c
new file mode 100644
index 0000000..1cfa6d0
--- /dev/null
+++ b/lua/sandborb/src/app.c
@@ -0,0 +1,95 @@
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+
+#include "sandbird.h"
+
+/* Functions to be called from Lua */
+static int l_sb_send_header(lua_State *L) {
+    sb_Event *e = lua_touserdata(L, 1);
+    const char *key = lua_tostring(L, 2);
+    const char *value = lua_tostring(L, 3);
+    sb_send_header(e->stream, key, value);
+    return 0;  /* Number of return values */
+}
+
+static int l_sb_send_status(lua_State *L) {
+    sb_Event *e = lua_touserdata(L, 1);
+    int status = lua_tointeger(L, 2);
+    const char *message = lua_tostring(L, 3);
+    sb_send_status(e->stream, status, message);
+    return 0;  /* Number of return values */
+}
+
+static int event_handler(sb_Event *e) {
+    if (e->type == SB_EV_REQUEST) {
+        printf("%s - %s %s\n", e->address, e->method, e->path);
+
+        lua_State *L = luaL_newstate();  /* create a new Lua state */
+        luaL_openlibs(L);  /* load Lua libraries */
+
+        /* Expose our functions to Lua */
+        lua_pushcfunction(L, l_sb_send_header);
+        lua_setglobal(L, "sb_send_header");
+        lua_pushcfunction(L, l_sb_send_status);
+        lua_setglobal(L, "sb_send_status");
+
+        /* load and run the Lua script */
+        if (luaL_dofile(L, "route_handler.lua") != LUA_OK) {
+            fprintf(stderr, "Failed to load Lua script: %s\n", lua_tostring(L, -1));
+            lua_close(L);
+            return SB_RES_OK;
+        }
+
+        /* call the Lua function */
+        lua_getglobal(L, "HANDLE_ROUTE"); /* all caps because it is a global function */
+        lua_pushlightuserdata(L, e); /* Pass the event as userdata to Lua */
+        lua_pushstring(L, e->path);
+        if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
+            fprintf(stderr, "Failed to call Lua function: %s\n", lua_tostring(L, -1));
+            lua_close(L);
+            return SB_RES_OK;
+        }
+
+        /* get the result from the Lua function */
+        const char *response = lua_tostring(L, -1);
+        lua_pop(L, 1);
+
+        sb_writef(e->stream, response);
+
+        lua_close(L);  /* close the Lua state */
+    }
+    return SB_RES_OK;
+}
+
+
+int main(void) {
+    sb_Options opt;
+    sb_Server *server;
+
+    memset(&opt, 0, sizeof(opt));
+    opt.port = "8000";
+    opt.handler = event_handler;
+
+    server = sb_new_server(&opt);
+
+    if (!server) {
+        fprintf(stderr, "failed to initialize server\n");
+        exit(EXIT_FAILURE);
+    }
+
+    printf("Server running at http://localhost:%s\n", opt.port);
+
+    for (;;) {
+        sb_poll_server(server, 1000);
+    }
+
+    sb_close_server(server);
+    return EXIT_SUCCESS;
+}
+
+
+
diff --git a/lua/sandborb/src/sandbird.c b/lua/sandborb/src/sandbird.c
new file mode 100644
index 0000000..e08c287
--- /dev/null
+++ b/lua/sandborb/src/sandbird.c
@@ -0,0 +1,979 @@
+/**
+ * Copyright (c) 2016 rxi
+ *
+ * This library is free software; you can redistribute it and/or modify it
+ * under the terms of the MIT license. See LICENSE for details.
+ */
+
+
+#ifdef _WIN32
+  #ifndef _WIN32_WINNT
+    #define _WIN32_WINNT 0x501
+  #endif
+  #ifndef _CRT_SECURE_NO_WARNINGS
+    #define _CRT_SECURE_NO_WARNINGS
+  #endif
+  #ifndef FD_SETSIZE
+    #define FD_SETSIZE 2048
+  #endif
+  #include <winsock2.h>
+  #include <ws2tcpip.h>
+  #include <windows.h>
+#else
+  #ifndef _POSIX_C_SOURCE
+    #define _POSIX_C_SOURCE 200809L
+  #endif
+  #include <unistd.h>
+  #include <fcntl.h>
+  #include <netdb.h>
+  #include <sys/types.h>
+  #include <sys/socket.h>
+  #include <sys/select.h>
+  #include <arpa/inet.h>
+  #include <netinet/in.h>
+#endif
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <time.h>
+#include <signal.h>
+#include <errno.h>
+
+#include "sandbird.h"
+
+
+#ifdef _WIN32
+  #define close(a) closesocket(a)
+  #define setsockopt(a, b, c, d, e) setsockopt(a, b, c, (char*)(d), e)
+
+  #undef  errno
+  #define errno WSAGetLastError()
+
+  #undef  EWOULDBLOCK
+  #define EWOULDBLOCK WSAEWOULDBLOCK
+
+  const char *inet_ntop(int af, const void *src, char *dst, socklen_t size) {
+    union { struct sockaddr sa; struct sockaddr_in sai;
+            struct sockaddr_in6 sai6; } addr;
+    int res;
+    memset(&addr, 0, sizeof(addr));
+    addr.sa.sa_family = af;
+    if (af == AF_INET6) {
+      memcpy(&addr.sai6.sin6_addr, src, sizeof(addr.sai6.sin6_addr));
+    } else {
+      memcpy(&addr.sai.sin_addr, src, sizeof(addr.sai.sin_addr));
+    }
+    res = WSAAddressToStringA(&addr.sa, sizeof(addr), 0, dst, (LPDWORD) &size);
+    if (res != 0) return NULL;
+    return dst;
+  }
+#endif
+
+#ifdef _WIN32
+  typedef SOCKET sb_Socket;
+#else
+  typedef int sb_Socket;
+  #define INVALID_SOCKET -1
+#endif
+
+typedef struct sb_Buffer sb_Buffer;
+
+struct sb_Buffer { char *s; size_t len, cap; };
+
+struct sb_Stream {
+  int state;                  /* Current state of the stream */
+  sb_Server *server;          /* The server object which owns this stream */
+  char address[46];           /* Remote IP address */
+  time_t init_time;           /* Time the stream was created */
+  time_t last_activity;       /* Time of Last I/O activity on the stream */
+  size_t expected_recv_len;   /* Expected length of the stream's request */
+  size_t data_idx;            /* Index of data section in recv_buf */
+  sb_Socket sockfd;           /* Socket for this streams connection */
+  sb_Buffer recv_buf;         /* Data received from client */
+  sb_Buffer send_buf;         /* Data waiting to be sent to client */
+  FILE *send_fp;              /* File currently being sent to client */
+  sb_Stream *next;            /* Next stream in linked list */
+};
+
+struct sb_Server {
+  sb_Stream *streams;         /* Linked list of all streams */
+  sb_Handler handler;         /* Event handler callback function */
+  sb_Socket sockfd;           /* Listeneing server socket */
+  void *udata;                /* User data value passed to all events */
+  time_t now;                 /* The current time */
+  time_t timeout;             /* Stream no-activity timeout */
+  time_t max_lifetime;        /* Maximum time a stream can exist */
+  size_t max_request_size;    /* Maximum request size in bytes */
+};
+
+enum {
+  STATE_RECEIVING_HEADER,
+  STATE_RECEIVING_REQUEST,
+  STATE_SENDING_STATUS,
+  STATE_SENDING_HEADER,
+  STATE_SENDING_DATA,
+  STATE_SENDING_FILE,
+  STATE_CLOSING
+};
+
+
+/*===========================================================================
+ * Utility
+ *===========================================================================*/
+
+static void set_socket_non_blocking(sb_Socket sockfd) {
+#ifdef _WIN32
+  u_long mode = 1;
+  ioctlsocket(sockfd, FIONBIO, &mode);
+#else
+  int flags = fcntl(sockfd, F_GETFL);
+  fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
+#endif
+}
+
+
+static int get_socket_address(sb_Socket sockfd, char *dst) {
+  int err;
+  union { struct sockaddr sa; struct sockaddr_storage sas;
+          struct sockaddr_in sai; struct sockaddr_in6 sai6; } addr;
+  socklen_t sz = sizeof(addr);
+  err = getpeername(sockfd, &addr.sa, &sz);
+  if (err == -1) {
+    *dst = '\0';
+    return SB_EFAILURE;
+  }
+  if (addr.sas.ss_family == AF_INET6) {
+    inet_ntop(AF_INET6, &addr.sai6.sin6_addr, dst, INET6_ADDRSTRLEN);
+  } else {
+    inet_ntop(AF_INET, &addr.sai.sin_addr, dst, INET_ADDRSTRLEN);
+  }
+  return SB_ESUCCESS;
+}
+
+
+static unsigned str_to_uint(const char *str) {
+  unsigned n;
+  if (!str || sscanf(str, "%u", &n) != 1) return 0;
+  return n;
+}
+
+
+static int hex_to_int(int chr) {
+  return isdigit(chr) ? (chr - '0') : (tolower(chr) - 'a' + 10);
+}
+
+
+static int url_decode(char *dst, const char *src, size_t len) {
+  len--;
+  while (*src && !strchr("?& \t\r\n", *src) && len) {
+    if (src[0] == '%' && src[1] && src[2]) {
+      *dst = (hex_to_int(src[1]) << 4) | hex_to_int(src[2]);
+      src += 2;
+    } else if (*src == '+') {
+      *dst = ' ';
+    } else {
+      *dst = *src;
+    }
+    dst++, src++, len--;
+  }
+  *dst = '\0';
+  return (len == 0) ? SB_ETRUNCATED : SB_ESUCCESS;
+}
+
+
+static int mem_equal(const void *a, const void *b, size_t len) {
+  const char *p = a, *q = b;
+  while (len) {
+    if (*p != *q) return 0;
+    p++, q++, len--;
+  }
+  return 1;
+}
+
+
+static int mem_case_equal(const void *a, const void *b, size_t len) {
+  const char *p = a, *q = b;
+  while (len) {
+    if (tolower(*p) != tolower(*q)) return 0;
+    p++, q++, len--;
+  }
+  return 1;
+}
+
+
+static const char *find_header_value(const char *str, const char *field) {
+  size_t len = strlen(field);
+  while (*str && !mem_equal(str, "\r\n", 2)) {
+    if (mem_case_equal(str, field, len) && str[len] == ':') {
+      str += len + 1;
+      return str + strspn(str, " \t");
+    }
+    str += strcspn(str, "\r");
+    str += mem_equal(str, "\r\n", 2) ? 2 : 0;
+  }
+  return NULL;
+}
+
+
+static const char *find_var_value(const char *str, const char *name) {
+  size_t len = strlen(name);
+  for (;;) {
+    if (mem_equal(str, name, len) && str[len] == '=') {
+      return str + len + 1;
+    }
+    str += strcspn(str, "& \t\r\n");
+    if (*str != '&') break;
+    str++;
+  }
+  return NULL;
+}
+
+
+const char *sb_error_str(int code) {
+  switch (code) {
+    case SB_ESUCCESS    : return "success";
+    case SB_EFAILURE    : return "failure";
+    case SB_EOUTOFMEM   : return "out of memory";
+    case SB_ETRUNCATED  : return "result truncated";
+    case SB_EBADSTATE   : return "bad stream state for this operation";
+    case SB_EBADRESULT  : return "bad result code from event handler";
+    case SB_ECANTOPEN   : return "cannot open file";
+    case SB_ENOTFOUND   : return "not found";
+    case SB_EFDTOOBIG   : return "got socket fd larger than FD_SETSIZE";
+    default             : return "unknown";
+  }
+}
+
+
+/*===========================================================================
+ * Buffer
+ *===========================================================================*/
+
+static void sb_buffer_init(sb_Buffer *buf) {
+  memset(buf, 0, sizeof(*buf));
+}
+
+
+static void sb_buffer_deinit(sb_Buffer *buf) {
+  free(buf->s);
+}
+
+
+static void sb_buffer_shift(sb_Buffer *buf, size_t n) {
+  buf->len -= n;
+  memmove(buf->s, buf->s + n, buf->len);
+}
+
+
+static int sb_buffer_reserve(sb_Buffer *buf, size_t n) {
+  void *p;
+  if (buf->cap >= n) return SB_ESUCCESS;
+  p = realloc(buf->s, n);
+  if (!p) return SB_EOUTOFMEM;
+  buf->s = p;
+  buf->cap = n;
+  return SB_ESUCCESS;
+}
+
+
+static int sb_buffer_push_char(sb_Buffer *buf, char chr) {
+  if (buf->len == buf->cap) {
+    int err = sb_buffer_reserve(buf, (buf->cap << 1) | (!buf->cap));
+    if (err) return err;
+  }
+  buf->s[buf->len++] = chr;
+  return SB_ESUCCESS;
+}
+
+
+static int sb_buffer_push_str(sb_Buffer *buf, const char *p, size_t len) {
+  int err;
+  size_t orig_len = buf->len;
+  while (len) {
+    err = sb_buffer_push_char(buf, *p);
+    if (err) {
+      buf->len = orig_len;
+      return err;
+    }
+    p++, len--;
+  }
+  return SB_ESUCCESS;
+}
+
+
+static int sb_buffer_vwritef(sb_Buffer *buf, const char *fmt, va_list args) {
+  int err;
+  size_t orig_len = buf->len;
+  char fbuf[64];
+  char lbuf[512];
+  char *s;
+
+  while (*fmt) {
+    if (*fmt == '%') {
+      switch (*++fmt) {
+
+        case 's':
+          s = va_arg(args, char*);
+          if (s == NULL) s = "(null)";
+          err = sb_buffer_push_str(buf, s, strlen(s));
+          if (err) goto fail;
+          break;
+
+        default:
+          fbuf[0] = '%';
+          s = fbuf + 1;
+          while ( !isalpha(*fmt) && *fmt != '%' ) *s++ = *fmt++;
+          s[0] = *fmt, s[1] = '\0';
+          switch (*fmt) {
+            case 'f':
+            case 'g': sprintf(lbuf, fbuf, va_arg(args, double));    break;
+            case 'c':
+            case 'd':
+            case 'i': sprintf(lbuf, fbuf, va_arg(args, int));       break;
+            case 'u':
+            case 'x':
+            case 'X': sprintf(lbuf, fbuf, va_arg(args, unsigned));  break;
+            case 'p': sprintf(lbuf, fbuf, va_arg(args, void*));     break;
+            default : lbuf[0] = *fmt, lbuf[1] = '\0';
+          }
+          err = sb_buffer_push_str(buf, lbuf, strlen(lbuf));
+          if (err) goto fail;
+      }
+    } else {
+      err = sb_buffer_push_char(buf, *fmt);
+      if (err) goto fail;
+    }
+    fmt++;
+  }
+
+  return SB_ESUCCESS;
+
+fail:
+  buf->len = orig_len;
+  return err;
+}
+
+
+static int sb_buffer_writef(sb_Buffer *buf, const char *fmt, ...) {
+  int err;
+  va_list args;
+  va_start(args, fmt);
+  err = sb_buffer_vwritef(buf, fmt, args);
+  va_end(args);
+  return err;
+}
+
+
+static int sb_buffer_null_terminate(sb_Buffer *buf) {
+  int err = sb_buffer_push_char(buf, '\0');
+  if (err) return err;
+  buf->len--;
+  return SB_ESUCCESS;
+}
+
+
+/*===========================================================================
+ * Stream
+ *===========================================================================*/
+
+static sb_Stream *sb_stream_new(sb_Server *srv, sb_Socket sockfd) {
+  sb_Stream *st = malloc( sizeof(*st) );
+  if (!st) return NULL;
+  memset(st, 0, sizeof(*st));
+  sb_buffer_init(&st->recv_buf);
+  sb_buffer_init(&st->send_buf);
+  st->sockfd = sockfd;
+  st->server = srv;
+  st->init_time = srv->now;
+  st->last_activity = srv->now;
+  set_socket_non_blocking(sockfd);
+  get_socket_address(sockfd, st->address);
+  return st;
+}
+
+
+static void sb_stream_close(sb_Stream *st) {
+  st->state = STATE_CLOSING;
+}
+
+
+static int sb_stream_emit(sb_Stream *st, sb_Event *e) {
+  int res;
+  e->stream = st;
+  e->udata = st->server->udata;
+  e->server = st->server;
+  e->address = st->address;
+  res = e->server->handler(e);
+  if (res < 0) return res;
+  switch (res) {
+    case SB_RES_CLOSE : sb_stream_close(st); /* Fall through */
+    case SB_RES_OK    : return SB_ESUCCESS;
+    default           : return SB_EBADRESULT;
+  }
+}
+
+
+static void sb_stream_destroy(sb_Stream *st) {
+  sb_Event e;
+  /* Emit close event */
+  e.type = SB_EV_CLOSE;
+  sb_stream_emit(st, &e);
+  /* Clean up */
+  close(st->sockfd);
+  if (st->send_fp) fclose(st->send_fp);
+  sb_buffer_deinit(&st->recv_buf);
+  sb_buffer_deinit(&st->send_buf);
+  free(st);
+}
+
+
+static int sb_stream_recv(sb_Stream *st) {
+  for (;;) {
+    char buf[4096];
+    int err, i, sz;
+
+    /* Receive data */
+    sz = recv(st->sockfd, buf, sizeof(buf) - 1, 0);
+    if (sz <= 0) {
+      /* Disconnected? */
+      if (sz == 0 || errno != EWOULDBLOCK) {
+        sb_stream_close(st);
+      }
+      return SB_ESUCCESS;
+    }
+
+    /* Update last_activity */
+    st->last_activity = st->server->now;
+
+    /* Write to recv_buf */
+    for (i = 0; i < sz; i++) {
+      err = sb_buffer_push_char(&st->recv_buf, buf[i]);
+      if (err) return err;
+
+      /* Have we received the whole header? */
+      if (
+        st->state == STATE_RECEIVING_HEADER &&
+        st->recv_buf.len >= 4 &&
+        mem_equal(st->recv_buf.s + st->recv_buf.len - 4, "\r\n\r\n", 4)
+      ) {
+        const char *s;
+        /* Update stream's current state */
+        st->state = STATE_RECEIVING_REQUEST;
+        /* Assure recv_buf is null-terminated */
+        err = sb_buffer_null_terminate(&st->recv_buf);
+        if (err) return err;
+        /* If the header contains the Content-Length field we set the
+         * expected_recv_len and continue writing to the recv_buf, otherwise we
+         * assume the request is complete */
+        s = find_header_value(st->recv_buf.s, "Content-Length");
+        if (s) {
+          st->expected_recv_len = st->recv_buf.len + str_to_uint(s);
+          st->data_idx = st->recv_buf.len;
+        } else {
+          goto handle_request;
+        }
+      }
+
+      /* Have we received all the data we're expecting? */
+      if (st->expected_recv_len == st->recv_buf.len) {
+        /* Handle request */
+        sb_Event e;
+        int n, path_idx;
+        char method[16], path[512], ver[16];
+handle_request:
+        st->state = STATE_SENDING_STATUS;
+        /* Assure recv_buf string is NULL-terminated */
+        err = sb_buffer_null_terminate(&st->recv_buf);
+        if (err) return err;
+        /* Get method, path, version */
+        n = sscanf(st->recv_buf.s, "%15s %n%*s %15s", method, &path_idx, ver);
+        /* Is request line invalid? */
+        if (n != 2 || !mem_equal(ver, "HTTP", 4)) {
+          sb_stream_close(st);
+          return SB_ESUCCESS;
+        }
+        /* Build and emit `request` event */
+        url_decode(path, st->recv_buf.s + path_idx, sizeof(path));
+        e.type = SB_EV_REQUEST;
+        e.method = method;
+        e.path = path;
+        err = sb_stream_emit(st, &e);
+        if (err) return err;
+        /* No more data needs to be received (nor should it exist) */
+        return SB_ESUCCESS;
+      }
+    }
+  }
+
+  return SB_ESUCCESS;
+}
+
+
+static int sb_stream_send(sb_Stream *st) {
+  if (st->send_buf.len > 0) {
+    int sz;
+
+    /* Send data */
+send_data:
+    sz = send(st->sockfd, st->send_buf.s, st->send_buf.len, 0);
+    if (sz <= 0) {
+      /* Disconnected? */
+      if (errno != EWOULDBLOCK) {
+        sb_stream_close(st);
+      }
+      return SB_ESUCCESS;
+    }
+
+    /* Remove sent bytes from buffer */
+    sb_buffer_shift(&st->send_buf, sz);
+
+    /* Update last_activity */
+    st->last_activity = st->server->now;
+
+  } else if (st->send_fp) {
+    /* Read chunk, write to stream and continue sending */
+    int err = sb_buffer_reserve(&st->send_buf, 8192);
+    if (err) return err;
+    st->send_buf.len = fread(st->send_buf.s, 1, st->send_buf.cap, st->send_fp);
+    if (st->send_buf.len > 0) goto send_data;
+
+    /* Reached end of file */
+    fclose(st->send_fp);
+    st->send_fp = NULL;
+
+  } else {
+    /* No more data left -- disconnect */
+    sb_stream_close(st);
+  }
+
+  return SB_ESUCCESS;
+}
+
+
+static int sb_stream_finalize_header(sb_Stream *st) {
+  int err;
+  if (st->state < STATE_SENDING_HEADER) {
+    err = sb_send_status(st, 200, "OK");
+    if (err) return err;
+  }
+  err = sb_buffer_push_str(&st->send_buf, "\r\n", 2);
+  if (err) return err;
+  st->state = STATE_SENDING_DATA;
+  return SB_ESUCCESS;
+}
+
+
+int sb_send_status(sb_Stream *st, int code, const char *msg) {
+  int err;
+  if (st->state != STATE_SENDING_STATUS) {
+    return SB_EBADSTATE;
+  }
+  err = sb_buffer_writef(&st->send_buf, "HTTP/1.1 %d %s\r\n", code, msg);
+  if (err) return err;
+  st->state = STATE_SENDING_HEADER;
+  return SB_ESUCCESS;
+}
+
+
+int sb_send_header(sb_Stream *st, const char *field, const char *val) {
+  int err;
+  if (st->state > STATE_SENDING_HEADER) {
+    return SB_EBADSTATE;
+  }
+  if (st->state < STATE_SENDING_HEADER) {
+    err = sb_send_status(st, 200, "OK");
+    if (err) return err;
+  }
+  err = sb_buffer_writef(&st->send_buf, "%s: %s\r\n", field, val);
+  if (err) return err;
+  return SB_ESUCCESS;
+}
+
+
+int sb_send_file(sb_Stream *st, const char *filename) {
+  int err;
+  char buf[32];
+  size_t sz;
+  FILE *fp = NULL;
+  if (st->state > STATE_SENDING_HEADER) {
+    return SB_EBADSTATE;
+  }
+  /* Try to open file */
+  fp = fopen(filename, "rb");
+  if (!fp) return SB_ECANTOPEN;
+
+  /* Get file size and write headers */
+  fseek(fp, 0, SEEK_END);
+  sz = ftell(fp);
+  sprintf(buf, "%u", (unsigned) sz);
+  err = sb_send_header(st, "Content-Length", buf);
+  if (err) goto fail;
+  err = sb_stream_finalize_header(st);
+  if (err) goto fail;
+
+  /* Rewind file, set stream's fp and state */
+  fseek(fp, 0, SEEK_SET);
+  st->send_fp = fp;
+  st->state = STATE_SENDING_FILE;
+  return SB_ESUCCESS;
+
+fail:
+  if (fp) fclose(fp);
+  return err;
+}
+
+
+int sb_write(sb_Stream *st, const void *data, size_t len) {
+  if (st->state < STATE_SENDING_DATA) {
+    int err = sb_stream_finalize_header(st);
+    if (err) return err;
+  }
+  if (st->state != STATE_SENDING_DATA) return SB_EBADSTATE;
+  return sb_buffer_push_str(&st->send_buf, data, len);
+}
+
+
+int sb_vwritef(sb_Stream *st, const char *fmt, va_list args) {
+  if (st->state < STATE_SENDING_DATA) {
+    int err = sb_stream_finalize_header(st);
+    if (err) return err;
+  }
+  if (st->state != STATE_SENDING_DATA) return SB_EBADSTATE;
+  return sb_buffer_vwritef(&st->send_buf, fmt, args);
+}
+
+
+int sb_writef(sb_Stream *st, const char *fmt, ...) {
+  int err;
+  va_list args;
+  va_start(args, fmt);
+  err = sb_vwritef(st, fmt, args);
+  va_end(args);
+  return err;
+}
+
+
+int sb_get_header(sb_Stream *st, const char *field, char *dst, size_t len) {
+  size_t n;
+  int res = SB_ESUCCESS;
+  const char *s = find_header_value(st->recv_buf.s, field);
+  if (!s) {
+    *dst = '\0';
+    return SB_ENOTFOUND;
+  }
+  n = strchr(s, '\r') - s;
+  while (n > 1 && strchr(" \t", s[n-1])) n--; /* trim whitespace from end */
+  if (n > len - 1) {
+    n = len - 1;
+    res = SB_ETRUNCATED;
+  }
+  memcpy(dst, s, n);
+  dst[n] = '\0';
+  return res;
+}
+
+
+int sb_get_var(sb_Stream *st, const char *name, char *dst, size_t len) {
+  const char *q, *s = NULL;
+
+  /* Find beginning of query string */
+  q = st->recv_buf.s + strcspn(st->recv_buf.s, "?\r");
+  q = (*q == '?') ? (q + 1) : NULL;
+
+  /* Try to get var from query string, then data string */
+  if (q) s = find_var_value(q, name);
+  if (!s && st->data_idx) {
+    s = find_var_value(st->recv_buf.s + st->data_idx, name);
+  }
+  if (!s) {
+    *dst = '\0';
+    return SB_ENOTFOUND;
+  }
+  return url_decode(dst, s, len);
+}
+
+
+int sb_get_cookie(sb_Stream *st, const char *name, char *dst, size_t len) {
+  size_t n;
+  const char *s = st->recv_buf.s;
+  int res = SB_ESUCCESS;
+  size_t name_len = strlen(name);
+
+  /* Get cookie header */
+  s = find_header_value(st->recv_buf.s, "Cookie");
+  if (!s) goto fail;
+
+  /* Find var */
+  while (*s) {
+    s += strspn(s, " \t");
+    /* Found var? find value, get len, copy value and return */
+    if ( mem_case_equal(s, name, name_len) && strchr(" =", s[name_len]) ) {
+      s += name_len;
+      s += strspn(s, "= \t\r");
+      n = strcspn(s, ";\r");
+      if (n >= len - 1) {
+        n = len - 1;
+        res = SB_ETRUNCATED;
+      }
+      memcpy(dst, s, n);
+      dst[n] = '\0';
+      return res;
+    }
+    s += strcspn(s, ";\r");
+    if (*s != ';') goto fail;
+    s++;
+  }
+
+fail:
+  *dst = '\0';
+  return SB_ENOTFOUND;
+}
+
+
+#define P_ATCHK(x)      do { if (!(p = (x))) goto fail; } while (0)
+#define P_AFTERL(x, l)  do {\
+                          size_t len__ = (l);\
+                          for (;; p++) {\
+                            if (p == end - len__) goto fail;\
+                            if (mem_equal(p, x, len__)) break;\
+                          }\
+                          p += len__;\
+                        } while (0)
+#define P_AFTER(s)      P_AFTERL(s, strlen(s))
+
+const void *sb_get_multipart(sb_Stream *st, const char *name, size_t *len) {
+  const char *boundary;
+  size_t boundary_len;
+  size_t name_len = strlen(name);
+  const char *p = st->recv_buf.s;
+  char *end = st->recv_buf.s + st->recv_buf.len;
+
+  /* Get boundary string */
+  P_ATCHK( find_header_value(p, "Content-Type") );
+  P_AFTER( "boundary=" );
+  boundary = p;
+  P_AFTER( "\r\n" );
+  boundary_len = p - boundary - 2;
+
+next:
+  /* Move to after first boundary, then to start of name */
+  P_AFTERL( boundary, boundary_len );
+  P_AFTER( "\r\n" );
+  P_ATCHK( find_header_value(p, "Content-Disposition") );
+  P_AFTER( "name=\"" );
+
+  /* Does the name match what we were looking for? */
+  if (mem_equal(p, name, name_len) && p[name_len] == '"') {
+    const char *res;
+    /* Move to start of data */
+    P_AFTER( "\r\n\r\n" );
+    res = p;
+    /* Find boundary, set length and return result */
+    P_AFTERL( boundary, boundary_len );
+    *len = p - res - boundary_len - 4;
+    return res;
+  }
+
+  /* Try the next part */
+  goto next;
+
+fail:
+  *len = 0;
+  return NULL;
+}
+
+
+/*===========================================================================
+ * Server
+ *===========================================================================*/
+
+sb_Server *sb_new_server(const sb_Options *opt) {
+  sb_Server *srv;
+  struct addrinfo hints, *ai = NULL;
+  int err, optval;
+
+#ifdef _WIN32
+  { WSADATA dat; WSAStartup(MAKEWORD(2, 2), &dat); }
+#else
+  /* Stops the SIGPIPE signal being raised when writing to a closed socket */
+  signal(SIGPIPE, SIG_IGN);
+#endif
+
+  /* Create server object */
+  srv = malloc( sizeof(*srv) );
+  if (!srv) goto fail;
+  memset(srv, 0, sizeof(*srv));
+  srv->sockfd = INVALID_SOCKET;
+  srv->handler = opt->handler;
+  srv->udata = opt->udata;
+  srv->timeout = opt->timeout ? str_to_uint(opt->timeout) : 30000;
+  srv->max_request_size = str_to_uint(opt->max_request_size);
+  srv->max_lifetime = str_to_uint(opt->max_lifetime);
+
+  /* Get addrinfo */
+  memset(&hints, 0, sizeof(hints));
+  hints.ai_family = AF_UNSPEC;
+  hints.ai_socktype = SOCK_STREAM;
+  hints.ai_flags = AI_PASSIVE;
+  err = getaddrinfo(opt->host, opt->port, &hints, &ai);
+  if (err) goto fail;
+
+  /* Init socket */
+  srv->sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+  if (srv->sockfd == INVALID_SOCKET) goto fail;
+  set_socket_non_blocking(srv->sockfd);
+
+  /* Set SO_REUSEADDR so that the socket can be immediately bound without
+   * having to wait for any closed socket on the same port to timeout */
+  optval = 1;
+  setsockopt(srv->sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
+
+  /* Bind and listen */
+  err = bind(srv->sockfd, ai->ai_addr, ai->ai_addrlen);
+  if (err) goto fail;
+  err = listen(srv->sockfd, 1023);
+  if (err) goto fail;
+
+  /* Clean up */
+  freeaddrinfo(ai);
+  ai = NULL;
+
+  return srv;
+
+fail:
+  if (ai) freeaddrinfo(ai);
+  if (srv) sb_close_server(srv);
+  return NULL;
+}
+
+
+void sb_close_server(sb_Server *srv) {
+  /* Destroy all streams */
+  while (srv->streams) {
+    sb_Stream *st = srv->streams;
+    srv->streams = st->next;
+    sb_stream_destroy(st);
+  }
+
+  /* Clean up */
+  if (srv->sockfd != INVALID_SOCKET) {
+    close(srv->sockfd);
+  }
+  free(srv);
+}
+
+
+int sb_poll_server(sb_Server *srv, int timeout) {
+  sb_Stream *st, **st_next;
+  fd_set fds_read, fds_write;
+  sb_Socket max_fd = srv->sockfd;
+  struct timeval tv;
+  int err;
+
+  /* Init fd_sets */
+  FD_ZERO(&fds_read);
+  FD_ZERO(&fds_write);
+
+  /* Add server sockfd to fd_set */
+  FD_SET(srv->sockfd, &fds_read);
+
+  /* Add streams to fd_sets */
+  for (st = srv->streams; st; st = st->next) {
+    if (st->state >= STATE_SENDING_STATUS) {
+      FD_SET(st->sockfd, &fds_write);
+    } else {
+      FD_SET(st->sockfd, &fds_read);
+    }
+    if (st->sockfd > max_fd) max_fd = st->sockfd;
+  }
+
+  /* Init timeout timeval */
+  tv.tv_sec = timeout / 1000;
+  tv.tv_usec = (timeout % 1000) * 1000;
+
+  /* Do select */
+  select(max_fd + 1, &fds_read, &fds_write, NULL, &tv);
+
+  /* Get and store current time */
+  srv->now = time(NULL);
+
+  /* Handle existing streams */
+  st_next = &srv->streams;
+  while (*st_next) {
+    st = *st_next;
+
+    /* Receive data */
+    if (FD_ISSET(st->sockfd, &fds_read)) {
+      err = sb_stream_recv(st);
+      if (err) return err;
+    }
+
+    /* Send data */
+    if (FD_ISSET(st->sockfd, &fds_write)) {
+      err = sb_stream_send(st);
+      if (err) return err;
+    }
+
+    /* Check stream against timeout, max request length and max lifetime */
+    if (
+      (srv->timeout && srv->now - st->last_activity > srv->timeout / 1000) ||
+      (srv->max_lifetime &&
+       srv->now - st->init_time > srv->max_lifetime / 1000) ||
+      (srv->max_request_size && st->recv_buf.len >= srv->max_request_size)
+    ) {
+      sb_stream_close(st);
+    }
+
+    /* Handle disconnect -- destroy stream */
+    if (st->state == STATE_CLOSING) {
+      *st_next = st->next;
+      sb_stream_destroy(st);
+      continue;
+    }
+
+    /* Next */
+    st_next = &(*st_next)->next;
+  }
+
+  /* Handle new streams */
+  if (FD_ISSET(srv->sockfd, &fds_read)) {
+    sb_Event e;
+    sb_Socket sockfd;
+
+    /* Accept connections */
+    while ( (sockfd = accept(srv->sockfd, NULL, NULL)) != INVALID_SOCKET ) {
+
+#ifdef _WIN32
+      /* As the fd_set on windows is an array rather than a bitset, an fd
+       * value can never be too large for it; thus this check is omitted */
+#else
+      /* Check FD size, error if it is larger than FD_SETSIZE */
+      if (sockfd > FD_SETSIZE) {
+        close(sockfd);
+        return SB_EFDTOOBIG;
+      }
+#endif
+
+      /* Init new stream */
+      st = sb_stream_new(srv, sockfd);
+      if (!st) {
+        close(sockfd);
+        return SB_EOUTOFMEM;
+      }
+
+      /* Push stream to list */
+      st->next = srv->streams;
+      srv->streams = st;
+
+      /* Do `connect` event */
+      e.type = SB_EV_CONNECT;
+      err = sb_stream_emit(st, &e);
+      if (err) return err;
+    }
+  }
+
+  return SB_ESUCCESS;
+}
+
+
diff --git a/lua/sandborb/src/sandbird.h b/lua/sandborb/src/sandbird.h
new file mode 100644
index 0000000..e1c0e7c
--- /dev/null
+++ b/lua/sandborb/src/sandbird.h
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2016 rxi
+ *
+ * This library is free software; you can redistribute it and/or modify it
+ * under the terms of the MIT license. See LICENSE for details.
+ */
+
+
+#ifndef SANDBIRD_H
+#define SANDBIRD_H
+
+#include <stddef.h>
+#include <stdarg.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define SB_VERSION "0.1.3"
+
+typedef struct sb_Server  sb_Server;
+typedef struct sb_Stream  sb_Stream;
+typedef struct sb_Event   sb_Event;
+typedef struct sb_Options sb_Options;
+typedef int (*sb_Handler)(sb_Event*);
+
+struct sb_Event {
+  int type;
+  void *udata;
+  sb_Server *server;
+  sb_Stream *stream;
+  const char *address;
+  const char *method;
+  const char *path;
+};
+
+struct sb_Options {
+  sb_Handler handler;
+  void *udata;
+  const char *host;
+  const char *port;
+  const char *timeout;
+  const char *max_lifetime;
+  const char *max_request_size;
+};
+
+enum {
+  SB_ESUCCESS     =  0,
+  SB_EFAILURE     = -1,
+  SB_EOUTOFMEM    = -2,
+  SB_ETRUNCATED   = -3,
+  SB_EBADSTATE    = -4,
+  SB_EBADRESULT   = -5,
+  SB_ECANTOPEN    = -6,
+  SB_ENOTFOUND    = -7,
+  SB_EFDTOOBIG    = -8
+};
+
+enum {
+  SB_EV_CONNECT,
+  SB_EV_CLOSE,
+  SB_EV_REQUEST
+};
+
+enum {
+  SB_RES_OK,
+  SB_RES_CLOSE
+};
+
+const char *sb_error_str(int code);
+sb_Server *sb_new_server(const sb_Options *opt);
+void sb_close_server(sb_Server *srv);
+int sb_poll_server(sb_Server *srv, int timeout);
+int sb_send_status(sb_Stream *st, int code, const char *msg);
+int sb_send_header(sb_Stream *st, const char *field, const char *val);
+int sb_send_file(sb_Stream *st, const char *filename);
+int sb_write(sb_Stream *st, const void *data, size_t len);
+int sb_vwritef(sb_Stream *st, const char *fmt, va_list args);
+int sb_writef(sb_Stream *st, const char *fmt, ...);
+int sb_get_header(sb_Stream *st, const char *field, char *dst, size_t len);
+int sb_get_var(sb_Stream *st, const char *name, char *dst, size_t len);
+int sb_get_cookie(sb_Stream *st, const char *name, char *dst, size_t len);
+const void *sb_get_multipart(sb_Stream *st, const char *name, size_t *len);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif
+
+