about summary refs log tree commit diff stats
path: root/lua/sandborb
diff options
context:
space:
mode:
authorelioat <elioat@tilde.institute>2024-06-05 21:10:36 -0400
committerelioat <elioat@tilde.institute>2024-06-05 21:10:36 -0400
commitaa5a5558856b1e8fa73db0d747d95e143a613c76 (patch)
tree4b13da47cfe41cca463618e675860d403511b031 /lua/sandborb
parent5ee6ec873890886971555fcba8b3850cd0ab9c57 (diff)
downloadtour-aa5a5558856b1e8fa73db0d747d95e143a613c76.tar.gz
cooking with fire
Diffstat (limited to 'lua/sandborb')
-rw-r--r--lua/sandborb/.gitignore3
-rw-r--r--lua/sandborb/app.c95
-rwxr-xr-xlua/sandborb/build.sh13
-rw-r--r--lua/sandborb/json.lua388
-rw-r--r--lua/sandborb/route_handler.lua37
-rw-r--r--lua/sandborb/sandbird.c979
-rw-r--r--lua/sandborb/sandbird.h91
7 files changed, 1606 insertions, 0 deletions
diff --git a/lua/sandborb/.gitignore b/lua/sandborb/.gitignore
new file mode 100644
index 0000000..92e5c23
--- /dev/null
+++ b/lua/sandborb/.gitignore
@@ -0,0 +1,3 @@
+/a.out
+/.vscode
+*.gch
\ No newline at end of file
diff --git a/lua/sandborb/app.c b/lua/sandborb/app.c
new file mode 100644
index 0000000..1cfa6d0
--- /dev/null
+++ b/lua/sandborb/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/build.sh b/lua/sandborb/build.sh
new file mode 100755
index 0000000..28f1724
--- /dev/null
+++ b/lua/sandborb/build.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env sh
+
+if [ -f a.out ]; then
+    rm a.out
+fi
+
+cc app.c sandbird.c -I /opt/homebrew/include/lua5.4 sandbird.h -std=c99 -pedantic -Wall -Wextra -llua
+
+if [ -f a.out ]; then
+    ./a.out
+else
+    echo "Build failed"
+fi
\ No newline at end of file
diff --git a/lua/sandborb/json.lua b/lua/sandborb/json.lua
new file mode 100644
index 0000000..711ef78
--- /dev/null
+++ b/lua/sandborb/json.lua
@@ -0,0 +1,388 @@
+--
+-- json.lua
+--
+-- Copyright (c) 2020 rxi
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a copy of
+-- this software and associated documentation files (the "Software"), to deal in
+-- the Software without restriction, including without limitation the rights to
+-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+-- of the Software, and to permit persons to whom the Software is furnished to do
+-- so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in all
+-- copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+-- SOFTWARE.
+--
+
+local json = { _version = "0.1.2" }
+
+-------------------------------------------------------------------------------
+-- Encode
+-------------------------------------------------------------------------------
+
+local encode
+
+local escape_char_map = {
+  [ "\\" ] = "\\",
+  [ "\"" ] = "\"",
+  [ "\b" ] = "b",
+  [ "\f" ] = "f",
+  [ "\n" ] = "n",
+  [ "\r" ] = "r",
+  [ "\t" ] = "t",
+}
+
+local escape_char_map_inv = { [ "/" ] = "/" }
+for k, v in pairs(escape_char_map) do
+  escape_char_map_inv[v] = k
+end
+
+
+local function escape_char(c)
+  return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
+end
+
+
+local function encode_nil(val)
+  return "null"
+end
+
+
+local function encode_table(val, stack)
+  local res = {}
+  stack = stack or {}
+
+  -- Circular reference?
+  if stack[val] then error("circular reference") end
+
+  stack[val] = true
+
+  if rawget(val, 1) ~= nil or next(val) == nil then
+    -- Treat as array -- check keys are valid and it is not sparse
+    local n = 0
+    for k in pairs(val) do
+      if type(k) ~= "number" then
+        error("invalid table: mixed or invalid key types")
+      end
+      n = n + 1
+    end
+    if n ~= #val then
+      error("invalid table: sparse array")
+    end
+    -- Encode
+    for i, v in ipairs(val) do
+      table.insert(res, encode(v, stack))
+    end
+    stack[val] = nil
+    return "[" .. table.concat(res, ",") .. "]"
+
+  else
+    -- Treat as an object
+    for k, v in pairs(val) do
+      if type(k) ~= "string" then
+        error("invalid table: mixed or invalid key types")
+      end
+      table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
+    end
+    stack[val] = nil
+    return "{" .. table.concat(res, ",") .. "}"
+  end
+end
+
+
+local function encode_string(val)
+  return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
+end
+
+
+local function encode_number(val)
+  -- Check for NaN, -inf and inf
+  if val ~= val or val <= -math.huge or val >= math.huge then
+    error("unexpected number value '" .. tostring(val) .. "'")
+  end
+  return string.format("%.14g", val)
+end
+
+
+local type_func_map = {
+  [ "nil"     ] = encode_nil,
+  [ "table"   ] = encode_table,
+  [ "string"  ] = encode_string,
+  [ "number"  ] = encode_number,
+  [ "boolean" ] = tostring,
+}
+
+
+encode = function(val, stack)
+  local t = type(val)
+  local f = type_func_map[t]
+  if f then
+    return f(val, stack)
+  end
+  error("unexpected type '" .. t .. "'")
+end
+
+
+function json.encode(val)
+  return ( encode(val) )
+end
+
+
+-------------------------------------------------------------------------------
+-- Decode
+-------------------------------------------------------------------------------
+
+local parse
+
+local function create_set(...)
+  local res = {}
+  for i = 1, select("#", ...) do
+    res[ select(i, ...) ] = true
+  end
+  return res
+end
+
+local space_chars   = create_set(" ", "\t", "\r", "\n")
+local delim_chars   = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
+local escape_chars  = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
+local literals      = create_set("true", "false", "null")
+
+local literal_map = {
+  [ "true"  ] = true,
+  [ "false" ] = false,
+  [ "null"  ] = nil,
+}
+
+
+local function next_char(str, idx, set, negate)
+  for i = idx, #str do
+    if set[str:sub(i, i)] ~= negate then
+      return i
+    end
+  end
+  return #str + 1
+end
+
+
+local function decode_error(str, idx, msg)
+  local line_count = 1
+  local col_count = 1
+  for i = 1, idx - 1 do
+    col_count = col_count + 1
+    if str:sub(i, i) == "\n" then
+      line_count = line_count + 1
+      col_count = 1
+    end
+  end
+  error( string.format("%s at line %d col %d", msg, line_count, col_count) )
+end
+
+
+local function codepoint_to_utf8(n)
+  -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
+  local f = math.floor
+  if n <= 0x7f then
+    return string.char(n)
+  elseif n <= 0x7ff then
+    return string.char(f(n / 64) + 192, n % 64 + 128)
+  elseif n <= 0xffff then
+    return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
+  elseif n <= 0x10ffff then
+    return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
+                       f(n % 4096 / 64) + 128, n % 64 + 128)
+  end
+  error( string.format("invalid unicode codepoint '%x'", n) )
+end
+
+
+local function parse_unicode_escape(s)
+  local n1 = tonumber( s:sub(1, 4),  16 )
+  local n2 = tonumber( s:sub(7, 10), 16 )
+   -- Surrogate pair?
+  if n2 then
+    return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
+  else
+    return codepoint_to_utf8(n1)
+  end
+end
+
+
+local function parse_string(str, i)
+  local res = ""
+  local j = i + 1
+  local k = j
+
+  while j <= #str do
+    local x = str:byte(j)
+
+    if x < 32 then
+      decode_error(str, j, "control character in string")
+
+    elseif x == 92 then -- `\`: Escape
+      res = res .. str:sub(k, j - 1)
+      j = j + 1
+      local c = str:sub(j, j)
+      if c == "u" then
+        local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
+                 or str:match("^%x%x%x%x", j + 1)
+                 or decode_error(str, j - 1, "invalid unicode escape in string")
+        res = res .. parse_unicode_escape(hex)
+        j = j + #hex
+      else
+        if not escape_chars[c] then
+          decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
+        end
+        res = res .. escape_char_map_inv[c]
+      end
+      k = j + 1
+
+    elseif x == 34 then -- `"`: End of string
+      res = res .. str:sub(k, j - 1)
+      return res, j + 1
+    end
+
+    j = j + 1
+  end
+
+  decode_error(str, i, "expected closing quote for string")
+end
+
+
+local function parse_number(str, i)
+  local x = next_char(str, i, delim_chars)
+  local s = str:sub(i, x - 1)
+  local n = tonumber(s)
+  if not n then
+    decode_error(str, i, "invalid number '" .. s .. "'")
+  end
+  return n, x
+end
+
+
+local function parse_literal(str, i)
+  local x = next_char(str, i, delim_chars)
+  local word = str:sub(i, x - 1)
+  if not literals[word] then
+    decode_error(str, i, "invalid literal '" .. word .. "'")
+  end
+  return literal_map[word], x
+end
+
+
+local function parse_array(str, i)
+  local res = {}
+  local n = 1
+  i = i + 1
+  while 1 do
+    local x
+    i = next_char(str, i, space_chars, true)
+    -- Empty / end of array?
+    if str:sub(i, i) == "]" then
+      i = i + 1
+      break
+    end
+    -- Read token
+    x, i = parse(str, i)
+    res[n] = x
+    n = n + 1
+    -- Next token
+    i = next_char(str, i, space_chars, true)
+    local chr = str:sub(i, i)
+    i = i + 1
+    if chr == "]" then break end
+    if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
+  end
+  return res, i
+end
+
+
+local function parse_object(str, i)
+  local res = {}
+  i = i + 1
+  while 1 do
+    local key, val
+    i = next_char(str, i, space_chars, true)
+    -- Empty / end of object?
+    if str:sub(i, i) == "}" then
+      i = i + 1
+      break
+    end
+    -- Read key
+    if str:sub(i, i) ~= '"' then
+      decode_error(str, i, "expected string for key")
+    end
+    key, i = parse(str, i)
+    -- Read ':' delimiter
+    i = next_char(str, i, space_chars, true)
+    if str:sub(i, i) ~= ":" then
+      decode_error(str, i, "expected ':' after key")
+    end
+    i = next_char(str, i + 1, space_chars, true)
+    -- Read value
+    val, i = parse(str, i)
+    -- Set
+    res[key] = val
+    -- Next token
+    i = next_char(str, i, space_chars, true)
+    local chr = str:sub(i, i)
+    i = i + 1
+    if chr == "}" then break end
+    if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
+  end
+  return res, i
+end
+
+
+local char_func_map = {
+  [ '"' ] = parse_string,
+  [ "0" ] = parse_number,
+  [ "1" ] = parse_number,
+  [ "2" ] = parse_number,
+  [ "3" ] = parse_number,
+  [ "4" ] = parse_number,
+  [ "5" ] = parse_number,
+  [ "6" ] = parse_number,
+  [ "7" ] = parse_number,
+  [ "8" ] = parse_number,
+  [ "9" ] = parse_number,
+  [ "-" ] = parse_number,
+  [ "t" ] = parse_literal,
+  [ "f" ] = parse_literal,
+  [ "n" ] = parse_literal,
+  [ "[" ] = parse_array,
+  [ "{" ] = parse_object,
+}
+
+
+parse = function(str, idx)
+  local chr = str:sub(idx, idx)
+  local f = char_func_map[chr]
+  if f then
+    return f(str, idx)
+  end
+  decode_error(str, idx, "unexpected character '" .. chr .. "'")
+end
+
+
+function json.decode(str)
+  if type(str) ~= "string" then
+    error("expected argument of type string, got " .. type(str))
+  end
+  local res, idx = parse(str, next_char(str, 1, space_chars, true))
+  idx = next_char(str, idx, space_chars, true)
+  if idx <= #str then
+    decode_error(str, idx, "trailing garbage")
+  end
+  return res
+end
+
+
+return json
diff --git a/lua/sandborb/route_handler.lua b/lua/sandborb/route_handler.lua
new file mode 100644
index 0000000..98ece1d
--- /dev/null
+++ b/lua/sandborb/route_handler.lua
@@ -0,0 +1,37 @@
+local json = require "json"
+
+local function brackets(e)
+  sb_send_status(e, 200, "OK")
+  sb_send_header(e, "Content-Type", "application/json")
+  return json.encode({ 1, 2, 3, { x = 10 } })
+end
+
+local function banana(e)
+  sb_send_status(e, 200, "OK")
+  sb_send_header(e, "Content-Type", "text/html, charset=utf-8")
+  return "<h1>Bananas are delicious!</h1>"
+end
+
+local function error_404(e,p)
+  sb_send_status(e, 404, "Not Found")
+  sb_send_header(e, "Content-Type", "text/html, charset=utf-8")
+  return "<h1>404 - Page not found: " .. p .. "</h1>"
+end
+
+function HANDLE_ROUTE(e, path)
+  if path == "/" then
+    sb_send_status(e, 200, "OK")
+    sb_send_header(e, "Content-Type", "text/html, charset=utf-8")  
+    return "Hello, you are at the root!"
+  elseif path == "/about" then
+    sb_send_status(e, 200, "OK")
+    sb_send_header(e, "Content-Type", "text/text")  
+    return "This is the about page!"
+  elseif path == "/banana" then
+    return banana(e)
+  elseif path == "/json" then
+    return brackets(e)
+  else
+    return error_404(e, path)
+  end
+end
\ No newline at end of file
diff --git a/lua/sandborb/sandbird.c b/lua/sandborb/sandbird.c
new file mode 100644
index 0000000..e08c287
--- /dev/null
+++ b/lua/sandborb/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/sandbird.h b/lua/sandborb/sandbird.h
new file mode 100644
index 0000000..e1c0e7c
--- /dev/null
+++ b/lua/sandborb/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
+
+