login

<     >

2023-07-13 11:37:37 (UTC-03:00)

Marcel Rodrigues <marcelgmr@gmail.com>

change package file structure

formerly all library internal files were on the same path as ludweb.lua
this exposed internal files to user code potentially causing conflicts
now the module tcp.lua can only be imported as ludweb.tcp

diff --git a/lib/ludweb.lua b/lib/ludweb.lua
new file mode 100644
index 0000000..0e39704
--- /dev/null
+++ b/lib/ludweb.lua
@@ -0,0 +1,6 @@
+local ludweb = {}
+ludweb.app      = require "ludweb.app"
+ludweb.template = require "ludweb.template"
+ludweb.crypt    = require "ludweb.crypt"
+ludweb.sqlite   = require "ludweb.sqlite"
+return ludweb

diff --git a/lib/ludweb/app.lua b/lib/ludweb/app.lua
new file mode 100644
index 0000000..bbc0f60
--- /dev/null
+++ b/lib/ludweb/app.lua
@@ -0,0 +1,48 @@
+local http = require "ludweb.http"
+
+local App = {}
+App.__index = App
+
+function App:add_route(method, pattern, callback)
+    table.insert(self.routes, {method, pattern, callback})
+end
+
+function App:run(port)
+    self.server:run(port)
+end
+
+function App:mount(prefix, app)
+    if prefix:sub(#prefix) ~= "/" then
+        prefix = prefix .. "/"
+    end
+    table.insert(self.mounts, {prefix, app})
+end
+
+local function new_app(routes)
+    local obj = setmetatable({}, App)
+    obj.routes = routes or {}
+    obj.mounts = {}
+    obj.server = http.new_http()
+    function obj.server:process(req)
+        for i, mount in ipairs(obj.mounts) do
+            local prefix, app = unpack(mount)
+            if (req.path.."/"):sub(1, #prefix) == prefix then
+                req.path = req.path:sub(#prefix)
+                return app.server:process(req)
+            end
+        end
+        for i, route in ipairs(obj.routes) do
+            local method, pattern, func = unpack(route)
+            if req.method == method then
+                local params = {req.path:match("^"..pattern.."$")}
+                if #params > 0 then
+                    return func(req, unpack(params))
+                end
+            end
+        end
+        return "", 404, "Not found"
+    end
+    return obj
+end
+
+return {new_app=new_app}

diff --git a/lib/ludweb/crypt.lua b/lib/ludweb/crypt.lua
new file mode 100644
index 0000000..fa23583
--- /dev/null
+++ b/lib/ludweb/crypt.lua
@@ -0,0 +1,277 @@
+local bit = require "bit"
+local ffi = require "ffi"
+
+local bnot = bit.bnot
+local band, bor, bxor = bit.band, bit.bor, bit.bxor
+local lshift, rshift = bit.lshift, bit.rshift
+local rol, ror = bit.rol, bit.ror
+
+local char, byte = string.char, string.byte
+
+ffi.cdef[[
+ssize_t getrandom(void *buf, size_t buflen, unsigned int flags);
+]]
+local C = ffi.C
+
+local SHA256_H = {
+    0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
+    0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
+}
+local SHA256_K = {
+    0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
+    0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
+    0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
+    0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
+    0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
+    0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
+    0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
+    0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
+    0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
+    0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
+    0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
+    0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
+    0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
+    0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
+    0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
+    0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
+}
+
+local function hex(h)
+    local s = ""
+    for i = 1, #h do
+        s = s .. ("%02x"):format(byte(h, i))
+    end
+    return s
+end
+
+local function urandom(len)
+    local buf = ffi.new("char ["..len.."]")
+    assert(C.getrandom(buf, len, 0) == len)
+    return ffi.string(buf, len)
+end
+
+local function uuid4()
+    local id = urandom(16)
+    local version = char(bor(0x40, band(byte(id, 7), 0x0F)))
+    local variant = char(bor(0x40, band(byte(id, 9), 0x1F)))
+    id = id:sub(1, 6) .. version .. id:sub(8, 8) .. variant .. id:sub(10, 16)
+    return id
+end
+
+-- returns b-bytes big endian integer encoded as string
+local function be_to_str(n, b)
+    local s = ""
+    for i = 1, b do
+        s = char(bit.band(n, 0xFF)) .. s
+        n = rshift(n, 8)
+    end
+    return s
+end
+
+local function sha256_str_to_be32(s)
+    assert(#s == 4)
+    local n = 0
+    for i = 1, 4 do
+        n = lshift(n, 8)
+        n = bor(n, byte(s, i))
+    end
+    return n
+end
+
+-- returns padded message
+local function sha256_pad(s)
+    -- all lengths in bytes
+    local L = #s                         -- input length
+    local n = math.ceil((L+9) / 64) * 64 -- padded length
+    local k = n - (L+9)                  -- zero-padding length
+    s = s .. char(128)
+    s = s .. char(0):rep(k)
+    s = s .. be_to_str(L*8, 8)
+    assert(#s % 64 == 0)
+    return s
+end
+
+local function sha256_mod(n)
+    return band(n, 0xFFFFFFFF)
+end
+
+-- compute hash of a 512-bit chunk
+local function sha256_chunk(s, H)
+    local s0, s1
+    local K = SHA256_K
+    local W = {}
+    for i = 0, 15 do
+        local w_str = s:sub(i*4+1, (i+1)*4)
+        W[i] = sha256_str_to_be32(w_str)
+    end
+    for i = 16, 63 do
+        s0 = bxor(ror(W[i-15],  7), ror(W[i-15], 18), rshift(W[i-15],  3))
+        s1 = bxor(ror(W[i- 2], 17), ror(W[i- 2], 19), rshift(W[i- 2], 10))
+        W[i] = sha256_mod(W[i-16] + s0 + W[i-7] + s1)
+    end
+    local a, b, c, d, e, f, g, h = unpack(H)
+    for i = 0, 63 do
+        local ch, maj, t1, t2
+        s1 = bxor(ror(e, 6), ror(e, 11), ror(e, 25))
+        ch = bxor(band(e, f), band(bnot(e), g))
+        t1 = sha256_mod(h + s1 + ch + K[i+1] + W[i])
+        s0 = bxor(ror(a, 2), ror(a, 13), ror(a, 22))
+        maj = bxor(band(a, b), band(a, c), band(b, c))
+        t2 = sha256_mod(s0 + maj)
+        a, b, c, d, e, f, g, h = sha256_mod(t1+t2), a, b, c, sha256_mod(d+t1), e, f, g
+    end
+    local S = {a, b, c, d, e, f, g, h}
+    for i = 1, 8 do
+        H[i] = sha256_mod(H[i] + S[i])
+    end
+end
+
+local function sha256(s)
+    s = sha256_pad(s)
+    local nchunks = #s / 64
+    local H = {}
+    for i = 1, 8 do
+        H[i] = SHA256_H[i]
+    end
+    for i = 1, nchunks do
+        local chunk = s:sub((i-1)*64+1, i*64)
+        sha256_chunk(chunk, H)
+    end
+    local h = ""
+    for i = 1, 8 do
+        h = h .. be_to_str(H[i], 4)
+    end
+    return h
+end
+
+local function hmac(key, msg)
+    if #key > 64 then
+        key = sha256(key)
+    end
+    key = key .. char(0):rep(64 - #key)
+    assert(#key == 64)
+    local o_key_pad = ""
+    local i_key_pad = ""
+    for i = 1, 64 do
+        local k = byte(key, i)
+        o_key_pad = o_key_pad .. char(bxor(k, 0x5C))
+        i_key_pad = i_key_pad .. char(bxor(k, 0x36))
+    end
+    return sha256(o_key_pad .. sha256(i_key_pad .. msg))
+end
+
+local function pbkdf2(password, salt, c, dklen)
+    assert(dklen % 32 == 0)
+    local n = dklen / 32
+    local dk = ""
+    for i = 1, n do
+        local f = {}
+        for j = 1, 32 do
+            f[j] = 0
+        end
+        local u = salt .. be_to_str(i, 4)
+        for j = 1, c do
+            u = hmac(password, u)
+            for k = 1, 32 do
+                f[k] = bxor(f[k], byte(u, k))
+            end
+        end
+        dk = dk .. char(unpack(f))
+    end
+    assert(#dk == dklen)
+    return dk
+end
+
+local B64_C = {}
+for i = byte("A"), byte("Z") do
+    table.insert(B64_C, char(i))
+end
+for i = byte("a"), byte("z") do
+    table.insert(B64_C, char(i))
+end
+for i = byte("0"), byte("9") do
+    table.insert(B64_C, char(i))
+end
+table.insert(B64_C, "+")
+table.insert(B64_C, "/")
+local B64_D = {}
+for i, c in ipairs(B64_C) do
+    B64_D[c] = i - 1
+end
+local B64_P = "="
+
+local function b64_tri_to_quad(bin)
+    if #bin == 0 then
+        return ""
+    end
+    local n = lshift(bin[1], 16)
+    if #bin > 1 then
+        n = bor(n, lshift(bin[2], 8))
+        if #bin > 2 then
+            n = bor(n, bin[3])
+        end
+    end
+    local i1 = rshift(n, 18) + 1
+    local i2 = band(rshift(n, 12), 0x3F) + 1
+    local i3 = band(rshift(n, 6), 0x3F) + 1
+    local i4 = band(n, 0x3F) + 1
+    local quad = B64_C[i1] .. B64_C[i2]
+    if #bin > 1 then
+        quad = quad .. B64_C[i3]
+        if #bin > 2 then
+            quad = quad .. B64_C[i4]
+        end
+    end
+    quad = quad .. B64_P:rep(4 - #quad)
+    return quad
+end
+
+local function b64_enc(data)
+    local b64 = ""
+    local bin = {}
+    for i = 1, #data do
+        table.insert(bin, byte(data, i))
+        if #bin == 3 then
+            b64 = b64 .. b64_tri_to_quad(bin)
+            bin = {}
+        end
+    end
+    b64 = b64 .. b64_tri_to_quad(bin)
+    return b64
+end
+
+local function base64_pad(b64)
+    local padlen = -#b64 % 4
+    return b64 .. B64_P:rep(padlen)
+end
+
+local function b64_dec(b64)
+    b64 = base64_pad(b64)
+    local data = ""
+    for i = 1, #b64/4 do
+        local n = 0
+        for j = (i-1)*4+1, i*4 do
+            local c = b64:sub(j, j)
+            n = lshift(n, 6)
+            if c ~= B64_P then
+                n = bor(n, B64_D[c])
+            end
+        end
+        local tri = be_to_str(n, 3)
+        local j = i*4
+        if b64:sub(j, j) == B64_P then
+            if b64:sub(j-1, j-1) == B64_P then
+                tri = tri:sub(1, 1)
+            else
+                tri = tri:sub(1, 2)
+            end
+        end
+        data = data .. tri
+    end
+    return data
+end
+
+return {
+    hex=hex, urandom=urandom, uuid4=uuid4, sha256=sha256, hmac=hmac,
+    pbkdf2=pbkdf2, b64_enc=b64_enc, b64_dec=b64_dec
+}

diff --git a/lib/ludweb/http.lua b/lib/ludweb/http.lua
new file mode 100644
index 0000000..2afa5f4
--- /dev/null
+++ b/lib/ludweb/http.lua
@@ -0,0 +1,120 @@
+local tcp = require "ludweb.tcp"
+
+local function parse_query(query_str)
+    local query = {}
+    if #query_str > 0 then
+        for pair in (query_str.."&"):gmatch("([^&]*)&") do
+            local key, val = pair:match("([^=]*)=(.*)")
+            query[key] = val
+        end
+    end
+    return query
+end
+
+local function parse_uri(uri)
+    local path, query_str, fragment = uri:match("([^?#]*)%??([^#]*)#?(.*)")
+    local query = parse_query(query_str)
+    return path, query, fragment
+end
+
+local function parse_cookies(cookie_values)
+    local cookies = {}
+    if cookie_values ~= nil then
+        for pair in (cookie_values..";"):gmatch("([^;]*);") do
+            local key, val = pair:match("%s*([^=]*)=(.*)")
+            cookies[key] = val
+        end
+    end
+    return cookies
+end
+
+local function parse_request(data)
+    local req = {payload="", headers={}}
+    local stage = "status"
+    for line in (data.."\n"):gmatch("(.-)\r?\n") do
+        if stage == "status" then
+            local target, protocol
+            req.method, target, protocol = line:match("(%S*) (%S*) (%S*)")
+            assert(protocol:sub(1, 7) == "HTTP/1.")
+            req.path, req.query, req.fragment = parse_uri(target)
+            stage = "header"
+        elseif stage == "header" then
+            if line == "" then
+                stage = "payload"
+            else
+                local key, value = line:match("([^:]*):%s*(.*)")
+                req.headers[key:lower()] = value
+            end
+        else -- payload
+            req.payload = req.payload..line.."\n"
+        end
+    end
+    if req.headers["content-type"] == "application/x-www-form-urlencoded" then
+        local query_str = req.payload
+        query_str = query_str:sub(1, #query_str-1)
+        req["form"] = parse_query(query_str)
+    end
+    req.cookies = parse_cookies(req.headers["cookie"])
+    return req
+end
+
+-- cookies is a sequence of tables with the following keys:
+--   key, val -> cookie entry (required)
+--   path -> cookie scope, e.g., /
+--   age -> cookie expiration time in seconds
+local function build_cookie_data(cookies)
+    local data = ""
+    for i, c in ipairs(cookies or {}) do
+        local line = ("Set-Cookie: %s=%s; HttpOnly"):format(c.key, c.val)
+        if c.path ~= nil then
+            line = line .. "; Path=" .. c.path
+        end
+        if c.age ~= nil then
+            line = line .. "; Max-Age=" .. c.age
+        end
+        data = data .. line .. "\n"
+    end
+    return data
+end
+
+local function build_response(data, status, reason, cookies)
+    local header = ""
+    if status == nil then
+        status = 200
+        reason = "OK"
+    elseif status == 303 then
+        reason = reason or "See Other"
+        header = "Location: " .. data .. "\n"
+    end
+    header = header .. build_cookie_data(cookies)
+    -- it's nice to provide a minimal error message by default
+    if status ~= 200 and data == ""  then
+        data = status.." "..reason.."\n"
+    end
+    local fmt = "HTTP/1.1 %03d %s\r\n%s\r\n%s"
+    return fmt:format(status, reason, header, data)
+end
+
+local HTTP = {}
+HTTP.__index = HTTP
+
+function HTTP:run(port)
+    self.tcp:init(port)
+    self.tcp:run()
+end
+
+local function new_http()
+    local obj = setmetatable({}, HTTP)
+    obj.tcp = tcp.new_tcp(1000, 200)
+    function obj.tcp:process(datain)
+        local req = parse_request(datain)
+        local dataout, status, reason, cookies = obj:process(req)
+        if dataout == nil then
+            return nil
+        end
+        return build_response(dataout, status, reason, cookies)
+    end
+    return obj
+end
+
+return {new_http=new_http}

diff --git a/lib/ludweb/sqlite.lua b/lib/ludweb/sqlite.lua
new file mode 100644
index 0000000..8daa160
--- /dev/null
+++ b/lib/ludweb/sqlite.lua
@@ -0,0 +1,103 @@
+local ffi = require "ffi"
+
+ffi.cdef[[
+typedef struct sqlite3 sqlite3;
+typedef struct sqlite3_stmt sqlite3_stmt;
+int sqlite3_open(
+  const char *filename,   /* Database filename (UTF-8) */
+  sqlite3 **ppDb          /* OUT: SQLite db handle */
+);
+int sqlite3_close(sqlite3*);
+int sqlite3_prepare_v2(sqlite3 *conn, const char *zSql, int nByte,
+  sqlite3_stmt **ppStmt, const char **pzTail);
+int sqlite3_bind_null(sqlite3_stmt*, int);
+int sqlite3_bind_double(sqlite3_stmt*, int, double);
+int sqlite3_bind_text(sqlite3_stmt*,int,const char*,int,void(*)(void*));
+int sqlite3_step(sqlite3_stmt*);
+int sqlite3_column_count(sqlite3_stmt *pStmt);
+const char *sqlite3_column_name(sqlite3_stmt*, int N);
+int sqlite3_column_type(sqlite3_stmt*, int iCol);
+int sqlite3_column_int(sqlite3_stmt*, int iCol);
+double sqlite3_column_double(sqlite3_stmt*, int iCol);
+const unsigned char *sqlite3_column_text(sqlite3_stmt*, int iCol);
+const void *sqlite3_column_blob(sqlite3_stmt*, int iCol);
+int sqlite3_reset(sqlite3_stmt *pStmt);
+int sqlite3_finalize(sqlite3_stmt *pStmt);
+]]
+local C = ffi.load("sqlite3")
+
+local CODE = {
+    [0] = "OK", "ERROR", "INTERNAL", "PERM", "ABORT", "BUSY", "LOCKED", "NOMEM",
+    "READONLY", "INTERRUPT", "IOERR", "CORRUPT", "NOTFOUND", "FULL", "CANTOPEN",
+    "PROTOCOL", "EMPTY", "SCHEMA", "TOOBIG", "CONSTRAINT", "MISMATCH", "MISUSE",
+    "NOLFS", "AUTH", "FORMAT", "RANGE", "NOTADB", "NOTICE", "WARNING",
+    [100] = "ROW", [101] = "DONE"
+}
+
+local TYPE = {"INTEGER", "FLOAT", "TEXT", "BLOB", "NULL"}
+
+local DB = {}
+DB.__index = DB
+
+function DB:execute(sql, ...)
+    local pstmt = ffi.new("sqlite3_stmt *[1]")
+    local res = CODE[C.sqlite3_prepare_v2(self.db, sql, #sql, pstmt, nil)]
+    if res ~= "OK" then error(sql) end
+    local stmt = pstmt[0]
+    local arg = {...}
+    for i, v in ipairs(arg) do
+        if type(v) == "nil" then
+            C.sqlite3_bind_null(stmt, i)
+        elseif type(v) == "number" then
+            C.sqlite3_bind_double(stmt, i, v)
+        elseif type(v) == "string" then
+            C.sqlite3_bind_text(stmt, i, v, #v, ffi.cast("void(*)(void*)", 0))
+        else
+            error(("invalid type for query parameter: %s"):format(type(v)))
+        end
+    end
+    local rows = {}
+    repeat
+        local done = true
+        local res = CODE[C.sqlite3_step(stmt)]
+        -- TODO: handle res == "BUSY"
+        if res == "ROW" then
+            local row = {}
+            local ncols = C.sqlite3_column_count(stmt)
+            for i = 0, ncols-1 do
+                local col_name = ffi.string(C.sqlite3_column_name(stmt, i))
+                local col_type = TYPE[C.sqlite3_column_type(stmt, i)]
+                local value
+                if col_type == "INTEGER" then
+                    value = C.sqlite3_column_int(stmt, i)
+                elseif col_type == "FLOAT" then
+                    value = C.sqlite3_column_double(stmt, i)
+                elseif col_type == "TEXT" then
+                    value = ffi.string(C.sqlite3_column_text(stmt, i))
+                elseif col_type == "BLOB" then
+                    value = C.sqlite3_column_blob(stmt, i)
+                end
+                row[col_name] = value
+            end
+            table.insert(rows, row)
+            done = false
+        end
+    until done
+    C.sqlite3_finalize(stmt)
+    return rows
+end
+
+function DB:close()
+    C.sqlite3_close(self.db)
+end
+
+local function open(fname)
+    local self = setmetatable({}, DB)
+    local pdb = ffi.new("sqlite3 *[1]")
+    C.sqlite3_open(fname, pdb)
+    self.db = pdb[0]
+    self.err = ffi.new("char *[1]")
+    return self
+end
+
+return {open=open}

diff --git a/lib/ludweb/tcp.lua b/lib/ludweb/tcp.lua
new file mode 100644
index 0000000..5b60b57
--- /dev/null
+++ b/lib/ludweb/tcp.lua
@@ -0,0 +1,161 @@
+local ffi = require "ffi"
+local bit = require "bit"
+
+ffi.cdef[[
+int fcntl(int fd, int cmd, ... /* arg */ );
+typedef int socklen_t;
+struct sockaddr {
+    unsigned short   sa_family;
+    char             sa_data[14];
+};
+struct addrinfo {
+    int              ai_flags;
+    int              ai_family;
+    int              ai_socktype;
+    int              ai_protocol;
+    socklen_t        ai_addrlen;
+    struct sockaddr *ai_addr;
+    char            *ai_canonname;
+    struct addrinfo *ai_next;
+};
+typedef union epoll_data {
+    void    *ptr;
+    int      fd;
+    uint32_t u32;
+    uint64_t u64;
+} epoll_data_t;
+struct epoll_event {
+    uint32_t     events;    /* Epoll events */
+    epoll_data_t data;      /* User data variable */
+};
+int getaddrinfo(
+    const char *restrict node,
+    const char *restrict service,
+    const struct addrinfo *restrict hints,
+    struct addrinfo **restrict res
+);
+void freeaddrinfo(struct addrinfo *res);
+const char *gai_strerror(int errcode);
+int close(int fildes);
+int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);
+int socket(int domain, int type, int protocol);
+int bind(int socket, const struct sockaddr *address, socklen_t address_len);
+int listen(int socket, int backlog);
+int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
+ssize_t recv(int socket, void *buffer, size_t length, int flags);
+ssize_t send(int socket, const void *buffer, size_t length, int flags);
+int shutdown(int socket, int how);
+int epoll_create(int size);
+int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
+int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
+enum {F_GETFL = 3, F_SETFL = 4};
+enum {O_NONBLOCK = 2048};
+enum {AF_UNSPEC = 0};
+enum {SOCK_STREAM = 1};
+enum {AI_PASSIVE = 1};
+enum {EPOLLIN = 1, EPOLLET = -2147483648};
+enum {EPOLL_CTL_ADD = 1, EPOLL_CTL_DEL = 2};
+enum {SHUT_RDWR = 2};
+enum {SOL_SOCKET = 1};
+enum {SO_REUSEADDR = 2};
+]]
+local C = ffi.C
+
+local function set_non_blocking(sockfd)
+    local flags = C.fcntl(sockfd, C.F_GETFL, 0)
+    flags = bit.bor(flags, C.O_NONBLOCK)
+    C.fcntl(sockfd, C.F_SETFL, flags)
+end
+
+local TCP = {}
+TCP.__index = TCP
+
+function TCP:init(port)
+    local hints = ffi.new("struct addrinfo[1]")
+    hints[0].ai_family = C.AF_UNSPEC
+    hints[0].ai_socktype = C.SOCK_STREAM
+    hints[0].ai_flags = C.AI_PASSIVE
+
+    local servinfo = ffi.new("struct addrinfo[1][1]")
+    servinfo = ffi.cast("struct addrinfo **", servinfo)
+
+    local err = C.getaddrinfo(nil, tostring(port), hints, servinfo)
+    assert(err == 0, ("getaddrinfo error: %s"):format(ffi.string(C.gai_strerror(err))))
+    addr = servinfo[0][0]
+    local val = ffi.new("int [1]", 1) -- for setsockopt()
+    local sockfd = -1
+    while addr ~= nil do
+        sockfd = C.socket(addr.ai_family, addr.ai_socktype, addr.ai_protocol)
+        if sockfd >= 0 then
+            set_non_blocking(sockfd)
+            C.setsockopt(sockfd, C.SOL_SOCKET, C.SO_REUSEADDR, val, ffi.sizeof(val))
+            err = C.bind(sockfd, addr.ai_addr, addr.ai_addrlen)
+            if err >= 0 then
+                break
+            end
+            C.close(sockfd)
+        end
+        addr = addr.ai_next
+    end
+    assert(addr ~= nil, "bind error")
+    C.freeaddrinfo(servinfo[0])
+    assert(C.listen(sockfd, self.backlog) >= 0)
+    self.sockfd = sockfd
+end
+
+function TCP:run()
+    local efd = C.epoll_create(self.max_poll_size)
+    local ev = ffi.new("struct epoll_event[1]")
+    ev[0].events = bit.bor(C.EPOLLIN, C.EPOLLET)
+    ev[0].data.fd = self.sockfd
+    assert(C.epoll_ctl(efd, C.EPOLL_CTL_ADD, self.sockfd, ev) >= 0, "epoll_ctl error")
+
+    local evs = ffi.new("struct epoll_event["..self.max_poll_size.."]")
+    local curfds = 1
+
+    local client_addr = ffi.new("struct sockaddr[1]")
+    local addr_size = ffi.new("socklen_t[1]", ffi.sizeof(client_addr[0]))
+    local buflen = 4096
+    local buffer = ffi.new("char ["..buflen.."]")
+    local running = true
+    while running do
+        nfds = C.epoll_wait(efd, evs, curfds, -1)
+        assert(nfds >= 0, "epoll_wait error")
+        for n = 0, nfds-1 do
+            if evs[n].data.fd == self.sockfd then
+                local newfd = C.accept(self.sockfd, client_addr, addr_size)
+                if newfd < 0 then
+                    break
+                end
+                set_non_blocking(newfd)
+                ev[0].events = bit.bor(C.EPOLLIN, C.EPOLLET)
+                ev[0].data.fd = newfd
+                assert(C.epoll_ctl(efd, C.EPOLL_CTL_ADD, newfd, ev) >= 0, "epoll_ctl error")
+                curfds = curfds + 1
+            else
+                C.recv(evs[n].data.fd, buffer, buflen, 0)
+                local data = self:process(ffi.string(buffer))
+                if data == nil then
+                    running = false
+                else
+                    C.send(evs[n].data.fd, data, #data, 0)
+                end
+                C.epoll_ctl(efd, C.EPOLL_CTL_DEL, evs[n].data.fd, ev)
+                C.shutdown(evs[n].data.fd, C.SHUT_RDWR)
+                curfds = curfds - 1
+                C.close(evs[n].data.fd)
+            end
+        end
+    end
+    C.shutdown(self.sockfd, C.SHUT_RDWR)
+    C.close(self.sockfd)
+end
+
+local function new_tcp(max_poll_size, backlog)
+    local self = setmetatable({}, TCP)
+    self.max_poll_size = max_poll_size
+    self.backlog = backlog
+    return self
+end
+
+return {new_tcp=new_tcp}

diff --git a/lib/ludweb/template.lua b/lib/ludweb/template.lua
new file mode 100644
index 0000000..a28c087
--- /dev/null
+++ b/lib/ludweb/template.lua
@@ -0,0 +1,122 @@
+local function escape(str)
+    local esc_tab = {['"']=34, ["&"]=38, ["<"]=60, [">"]=62, ["{"]=123, ["}"]=125}
+    local esc = ""
+    for i = 1, #str do
+        local c = str:sub(i, i)
+        local n = esc_tab[c]
+        if n ~= nil then
+            c = "&#" .. n .. ";"
+        end
+        esc = esc .. c
+    end
+    return esc
+end
+
+local function eval(str, env)
+    local expr = str:gsub("%$(%w+)", "env.%1")
+    local code = "return function(env) return "..expr.." end"
+    return loadstring(code, "eval")()(env)
+end
+
+local function find_matching_end(lines, start, last)
+    local level = 1
+    local else_line
+    for line_num = start+1, last do
+        local line = lines[line_num]
+        if line:sub(1, 1) == "%" then
+            local keyword = line:match("%%%s*(%w+).*")
+            if keyword == "end" then
+                level = level - 1
+                if level == 0 then
+                    return line_num, else_line
+                end
+            elseif keyword == "else" then
+                if level == 1 then
+                    else_line = line_num
+                end
+            elseif keyword ~= "set" and keyword ~= "include" then
+                level = level + 1
+            end
+        end
+    end
+    error(("line %d: no matching end"):format(start))
+end
+
+local function render_block(lines, first, last, env)
+    local result = ""
+    local function raw_sub(s) return eval(s, env) end
+    local function esc_sub(s) return escape(tostring(eval(s, env))) end
+    local line_num = first
+    while line_num <= last do
+        local line = lines[line_num]
+        if line:match("%%%s*(%w+).*") == "set" then
+            local var, expr = line:match("%%%s*set%s+([%w_]+)%s*=%s*(.+)")
+            assert(var and expr)
+            env[var] = eval(expr, env)
+            line_num = line_num + 1
+        elseif line:match("%%%s*(%w+).*") == "include" then
+            local fname = line:match("%%%s*include%s+(.+)")
+            local str = io.input(fname):read("*a")
+            assert(str:sub(#str) == "\n")
+            local inc_lines = {}
+            for inc_line in str:gmatch("(.-)\r?\n") do
+                inc_line = inc_line:gsub("^%s*%%", "%%")
+                table.insert(inc_lines, inc_line)
+            end
+            result = result .. render_block(inc_lines, 1, #inc_lines, env)
+            line_num = line_num + 1
+        elseif line:sub(1, 1) == "%" then   -- block start
+            local block_type = line:match("%%%s*(%w+).*")
+            local block_end, else_line = find_matching_end(lines, line_num, last)
+            if block_type == "if" then
+                local expr = line:match("%%%s*if%s+(.+)%s+then")
+                assert(expr)
+                if eval(expr, env) then
+                    local true_end = else_line or block_end
+                    result = result .. render_block(lines, line_num+1, true_end-1, env)
+                elseif else_line ~= nil then
+                    result = result .. render_block(lines, else_line+1, block_end-1, env)
+                end
+            elseif block_type == "while" then
+                local expr = line:match("%%%s*while%s+(.+)%s+do")
+                assert(expr)
+                while eval(expr, env) do
+                    result = result .. render_block(lines, line_num+1, block_end-1, env)
+                end
+            elseif block_type == "for" then
+                local var, expr = line:match("%%%s*for%s+([%w_]+)%s+in%s+(.+)%s+do")
+                assert(var and expr)
+                for i, v in ipairs(eval(expr, env)) do
+                    env[var] = v
+                    result = result .. render_block(lines, line_num+1, block_end-1, env)
+                end
+            end
+            line_num = block_end + 1
+        else
+            local line = line:gsub("{{([^!].-)}}", esc_sub)
+            local line = line:gsub("{{!(.-)}}", raw_sub)
+            if #line > 0 then
+                result = result .. line .. "\n"
+            end
+            line_num = line_num + 1
+        end
+    end
+    return result
+end
+
+local function render_str(str, env)
+    assert(str:sub(#str) == "\n")
+    local lines = {}
+    for line in str:gmatch("(.-)\r?\n") do
+        line = line:gsub("^%s*%%", "%%")
+        table.insert(lines, line)
+    end
+    return render_block(lines, 1, #lines, env)
+end
+
+local function render_file(fname, env)
+    local str = io.input(fname):read("*a")
+    return render_str(str, env)
+end
+
+return {escape=escape, render_str=render_str, render_file=render_file}

diff --git a/src/app.lua b/src/app.lua
deleted file mode 100644
index 1542ddf..0000000
--- a/src/app.lua
+++ /dev/null
@@ -1,48 +0,0 @@
-local http = require "http"
-
-local App = {}
-App.__index = App
-
-function App:add_route(method, pattern, callback)
-    table.insert(self.routes, {method, pattern, callback})
-end
-
-function App:run(port)
-    self.server:run(port)
-end
-
-function App:mount(prefix, app)
-    if prefix:sub(#prefix) ~= "/" then
-        prefix = prefix .. "/"
-    end
-    table.insert(self.mounts, {prefix, app})
-end
-
-local function new_app(routes)
-    local obj = setmetatable({}, App)
-    obj.routes = routes or {}
-    obj.mounts = {}
-    obj.server = http.new_http()
-    function obj.server:process(req)
-        for i, mount in ipairs(obj.mounts) do
-            local prefix, app = unpack(mount)
-            if (req.path.."/"):sub(1, #prefix) == prefix then
-                req.path = req.path:sub(#prefix)
-                return app.server:process(req)
-            end
-        end
-        for i, route in ipairs(obj.routes) do
-            local method, pattern, func = unpack(route)
-            if req.method == method then
-                local params = {req.path:match("^"..pattern.."$")}
-                if #params > 0 then
-                    return func(req, unpack(params))
-                end
-            end
-        end
-        return "", 404, "Not found"
-    end
-    return obj
-end
-
-return {new_app=new_app}

diff --git a/src/crypt.lua b/src/crypt.lua
deleted file mode 100644
index fa23583..0000000
--- a/src/crypt.lua
+++ /dev/null
@@ -1,277 +0,0 @@
-local bit = require "bit"
-local ffi = require "ffi"
-
-local bnot = bit.bnot
-local band, bor, bxor = bit.band, bit.bor, bit.bxor
-local lshift, rshift = bit.lshift, bit.rshift
-local rol, ror = bit.rol, bit.ror
-
-local char, byte = string.char, string.byte
-
-ffi.cdef[[
-ssize_t getrandom(void *buf, size_t buflen, unsigned int flags);
-]]
-local C = ffi.C
-
-local SHA256_H = {
-    0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
-    0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
-}
-local SHA256_K = {
-    0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
-    0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
-    0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
-    0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
-    0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
-    0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
-    0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
-    0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
-    0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
-    0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
-    0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
-    0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
-    0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
-    0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
-    0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
-    0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
-}
-
-local function hex(h)
-    local s = ""
-    for i = 1, #h do
-        s = s .. ("%02x"):format(byte(h, i))
-    end
-    return s
-end
-
-local function urandom(len)
-    local buf = ffi.new("char ["..len.."]")
-    assert(C.getrandom(buf, len, 0) == len)
-    return ffi.string(buf, len)
-end
-
-local function uuid4()
-    local id = urandom(16)
-    local version = char(bor(0x40, band(byte(id, 7), 0x0F)))
-    local variant = char(bor(0x40, band(byte(id, 9), 0x1F)))
-    id = id:sub(1, 6) .. version .. id:sub(8, 8) .. variant .. id:sub(10, 16)
-    return id
-end
-
--- returns b-bytes big endian integer encoded as string
-local function be_to_str(n, b)
-    local s = ""
-    for i = 1, b do
-        s = char(bit.band(n, 0xFF)) .. s
-        n = rshift(n, 8)
-    end
-    return s
-end
-
-local function sha256_str_to_be32(s)
-    assert(#s == 4)
-    local n = 0
-    for i = 1, 4 do
-        n = lshift(n, 8)
-        n = bor(n, byte(s, i))
-    end
-    return n
-end
-
--- returns padded message
-local function sha256_pad(s)
-    -- all lengths in bytes
-    local L = #s                         -- input length
-    local n = math.ceil((L+9) / 64) * 64 -- padded length
-    local k = n - (L+9)                  -- zero-padding length
-    s = s .. char(128)
-    s = s .. char(0):rep(k)
-    s = s .. be_to_str(L*8, 8)
-    assert(#s % 64 == 0)
-    return s
-end
-
-local function sha256_mod(n)
-    return band(n, 0xFFFFFFFF)
-end
-
--- compute hash of a 512-bit chunk
-local function sha256_chunk(s, H)
-    local s0, s1
-    local K = SHA256_K
-    local W = {}
-    for i = 0, 15 do
-        local w_str = s:sub(i*4+1, (i+1)*4)
-        W[i] = sha256_str_to_be32(w_str)
-    end
-    for i = 16, 63 do
-        s0 = bxor(ror(W[i-15],  7), ror(W[i-15], 18), rshift(W[i-15],  3))
-        s1 = bxor(ror(W[i- 2], 17), ror(W[i- 2], 19), rshift(W[i- 2], 10))
-        W[i] = sha256_mod(W[i-16] + s0 + W[i-7] + s1)
-    end
-    local a, b, c, d, e, f, g, h = unpack(H)
-    for i = 0, 63 do
-        local ch, maj, t1, t2
-        s1 = bxor(ror(e, 6), ror(e, 11), ror(e, 25))
-        ch = bxor(band(e, f), band(bnot(e), g))
-        t1 = sha256_mod(h + s1 + ch + K[i+1] + W[i])
-        s0 = bxor(ror(a, 2), ror(a, 13), ror(a, 22))
-        maj = bxor(band(a, b), band(a, c), band(b, c))
-        t2 = sha256_mod(s0 + maj)
-        a, b, c, d, e, f, g, h = sha256_mod(t1+t2), a, b, c, sha256_mod(d+t1), e, f, g
-    end
-    local S = {a, b, c, d, e, f, g, h}
-    for i = 1, 8 do
-        H[i] = sha256_mod(H[i] + S[i])
-    end
-end
-
-local function sha256(s)
-    s = sha256_pad(s)
-    local nchunks = #s / 64
-    local H = {}
-    for i = 1, 8 do
-        H[i] = SHA256_H[i]
-    end
-    for i = 1, nchunks do
-        local chunk = s:sub((i-1)*64+1, i*64)
-        sha256_chunk(chunk, H)
-    end
-    local h = ""
-    for i = 1, 8 do
-        h = h .. be_to_str(H[i], 4)
-    end
-    return h
-end
-
-local function hmac(key, msg)
-    if #key > 64 then
-        key = sha256(key)
-    end
-    key = key .. char(0):rep(64 - #key)
-    assert(#key == 64)
-    local o_key_pad = ""
-    local i_key_pad = ""
-    for i = 1, 64 do
-        local k = byte(key, i)
-        o_key_pad = o_key_pad .. char(bxor(k, 0x5C))
-        i_key_pad = i_key_pad .. char(bxor(k, 0x36))
-    end
-    return sha256(o_key_pad .. sha256(i_key_pad .. msg))
-end
-
-local function pbkdf2(password, salt, c, dklen)
-    assert(dklen % 32 == 0)
-    local n = dklen / 32
-    local dk = ""
-    for i = 1, n do
-        local f = {}
-        for j = 1, 32 do
-            f[j] = 0
-        end
-        local u = salt .. be_to_str(i, 4)
-        for j = 1, c do
-            u = hmac(password, u)
-            for k = 1, 32 do
-                f[k] = bxor(f[k], byte(u, k))
-            end
-        end
-        dk = dk .. char(unpack(f))
-    end
-    assert(#dk == dklen)
-    return dk
-end
-
-local B64_C = {}
-for i = byte("A"), byte("Z") do
-    table.insert(B64_C, char(i))
-end
-for i = byte("a"), byte("z") do
-    table.insert(B64_C, char(i))
-end
-for i = byte("0"), byte("9") do
-    table.insert(B64_C, char(i))
-end
-table.insert(B64_C, "+")
-table.insert(B64_C, "/")
-local B64_D = {}
-for i, c in ipairs(B64_C) do
-    B64_D[c] = i - 1
-end
-local B64_P = "="
-
-local function b64_tri_to_quad(bin)
-    if #bin == 0 then
-        return ""
-    end
-    local n = lshift(bin[1], 16)
-    if #bin > 1 then
-        n = bor(n, lshift(bin[2], 8))
-        if #bin > 2 then
-            n = bor(n, bin[3])
-        end
-    end
-    local i1 = rshift(n, 18) + 1
-    local i2 = band(rshift(n, 12), 0x3F) + 1
-    local i3 = band(rshift(n, 6), 0x3F) + 1
-    local i4 = band(n, 0x3F) + 1
-    local quad = B64_C[i1] .. B64_C[i2]
-    if #bin > 1 then
-        quad = quad .. B64_C[i3]
-        if #bin > 2 then
-            quad = quad .. B64_C[i4]
-        end
-    end
-    quad = quad .. B64_P:rep(4 - #quad)
-    return quad
-end
-
-local function b64_enc(data)
-    local b64 = ""
-    local bin = {}
-    for i = 1, #data do
-        table.insert(bin, byte(data, i))
-        if #bin == 3 then
-            b64 = b64 .. b64_tri_to_quad(bin)
-            bin = {}
-        end
-    end
-    b64 = b64 .. b64_tri_to_quad(bin)
-    return b64
-end
-
-local function base64_pad(b64)
-    local padlen = -#b64 % 4
-    return b64 .. B64_P:rep(padlen)
-end
-
-local function b64_dec(b64)
-    b64 = base64_pad(b64)
-    local data = ""
-    for i = 1, #b64/4 do
-        local n = 0
-        for j = (i-1)*4+1, i*4 do
-            local c = b64:sub(j, j)
-            n = lshift(n, 6)
-            if c ~= B64_P then
-                n = bor(n, B64_D[c])
-            end
-        end
-        local tri = be_to_str(n, 3)
-        local j = i*4
-        if b64:sub(j, j) == B64_P then
-            if b64:sub(j-1, j-1) == B64_P then
-                tri = tri:sub(1, 1)
-            else
-                tri = tri:sub(1, 2)
-            end
-        end
-        data = data .. tri
-    end
-    return data
-end
-
-return {
-    hex=hex, urandom=urandom, uuid4=uuid4, sha256=sha256, hmac=hmac,
-    pbkdf2=pbkdf2, b64_enc=b64_enc, b64_dec=b64_dec
-}

diff --git a/src/http.lua b/src/http.lua
deleted file mode 100644
index 40ed167..0000000
--- a/src/http.lua
+++ /dev/null
@@ -1,120 +0,0 @@
-local tcp = require "tcp"
-
-local function parse_query(query_str)
-    local query = {}
-    if #query_str > 0 then
-        for pair in (query_str.."&"):gmatch("([^&]*)&") do
-            local key, val = pair:match("([^=]*)=(.*)")
-            query[key] = val
-        end
-    end
-    return query
-end
-
-local function parse_uri(uri)
-    local path, query_str, fragment = uri:match("([^?#]*)%??([^#]*)#?(.*)")
-    local query = parse_query(query_str)
-    return path, query, fragment
-end
-
-local function parse_cookies(cookie_values)
-    local cookies = {}
-    if cookie_values ~= nil then
-        for pair in (cookie_values..";"):gmatch("([^;]*);") do
-            local key, val = pair:match("%s*([^=]*)=(.*)")
-            cookies[key] = val
-        end
-    end
-    return cookies
-end
-
-local function parse_request(data)
-    local req = {payload="", headers={}}
-    local stage = "status"
-    for line in (data.."\n"):gmatch("(.-)\r?\n") do
-        if stage == "status" then
-            local target, protocol
-            req.method, target, protocol = line:match("(%S*) (%S*) (%S*)")
-            assert(protocol:sub(1, 7) == "HTTP/1.")
-            req.path, req.query, req.fragment = parse_uri(target)
-            stage = "header"
-        elseif stage == "header" then
-            if line == "" then
-                stage = "payload"
-            else
-                local key, value = line:match("([^:]*):%s*(.*)")
-                req.headers[key:lower()] = value
-            end
-        else -- payload
-            req.payload = req.payload..line.."\n"
-        end
-    end
-    if req.headers["content-type"] == "application/x-www-form-urlencoded" then
-        local query_str = req.payload
-        query_str = query_str:sub(1, #query_str-1)
-        req["form"] = parse_query(query_str)
-    end
-    req.cookies = parse_cookies(req.headers["cookie"])
-    return req
-end
-
--- cookies is a sequence of tables with the following keys:
---   key, val -> cookie entry (required)
---   path -> cookie scope, e.g., /
---   age -> cookie expiration time in seconds
-local function build_cookie_data(cookies)
-    local data = ""
-    for i, c in ipairs(cookies or {}) do
-        local line = ("Set-Cookie: %s=%s; HttpOnly"):format(c.key, c.val)
-        if c.path ~= nil then
-            line = line .. "; Path=" .. c.path
-        end
-        if c.age ~= nil then
-            line = line .. "; Max-Age=" .. c.age
-        end
-        data = data .. line .. "\n"
-    end
-    return data
-end
-
-local function build_response(data, status, reason, cookies)
-    local header = ""
-    if status == nil then
-        status = 200
-        reason = "OK"
-    elseif status == 303 then
-        reason = reason or "See Other"
-        header = "Location: " .. data .. "\n"
-    end
-    header = header .. build_cookie_data(cookies)
-    -- it's nice to provide a minimal error message by default
-    if status ~= 200 and data == ""  then
-        data = status.." "..reason.."\n"
-    end
-    local fmt = "HTTP/1.1 %03d %s\r\n%s\r\n%s"
-    return fmt:format(status, reason, header, data)
-end
-
-local HTTP = {}
-HTTP.__index = HTTP
-
-function HTTP:run(port)
-    self.tcp:init(port)
-    self.tcp:run()
-end
-
-local function new_http()
-    local obj = setmetatable({}, HTTP)
-    obj.tcp = tcp.new_tcp(1000, 200)
-    function obj.tcp:process(datain)
-        local req = parse_request(datain)
-        local dataout, status, reason, cookies = obj:process(req)
-        if dataout == nil then
-            return nil
-        end
-        return build_response(dataout, status, reason, cookies)
-    end
-    return obj
-end
-
-return {new_http=new_http}

diff --git a/src/ludweb.lua b/src/ludweb.lua
deleted file mode 100644
index 69cd5b4..0000000
--- a/src/ludweb.lua
+++ /dev/null
@@ -1,6 +0,0 @@
-local ludweb = {}
-ludweb.app      = require "app"
-ludweb.template = require "template"
-ludweb.crypt    = require "crypt"
-ludweb.sqlite   = require "sqlite"
-return ludweb

diff --git a/src/sqlite.lua b/src/sqlite.lua
deleted file mode 100644
index 8daa160..0000000
--- a/src/sqlite.lua
+++ /dev/null
@@ -1,103 +0,0 @@
-local ffi = require "ffi"
-
-ffi.cdef[[
-typedef struct sqlite3 sqlite3;
-typedef struct sqlite3_stmt sqlite3_stmt;
-int sqlite3_open(
-  const char *filename,   /* Database filename (UTF-8) */
-  sqlite3 **ppDb          /* OUT: SQLite db handle */
-);
-int sqlite3_close(sqlite3*);
-int sqlite3_prepare_v2(sqlite3 *conn, const char *zSql, int nByte,
-  sqlite3_stmt **ppStmt, const char **pzTail);
-int sqlite3_bind_null(sqlite3_stmt*, int);
-int sqlite3_bind_double(sqlite3_stmt*, int, double);
-int sqlite3_bind_text(sqlite3_stmt*,int,const char*,int,void(*)(void*));
-int sqlite3_step(sqlite3_stmt*);
-int sqlite3_column_count(sqlite3_stmt *pStmt);
-const char *sqlite3_column_name(sqlite3_stmt*, int N);
-int sqlite3_column_type(sqlite3_stmt*, int iCol);
-int sqlite3_column_int(sqlite3_stmt*, int iCol);
-double sqlite3_column_double(sqlite3_stmt*, int iCol);
-const unsigned char *sqlite3_column_text(sqlite3_stmt*, int iCol);
-const void *sqlite3_column_blob(sqlite3_stmt*, int iCol);
-int sqlite3_reset(sqlite3_stmt *pStmt);
-int sqlite3_finalize(sqlite3_stmt *pStmt);
-]]
-local C = ffi.load("sqlite3")
-
-local CODE = {
-    [0] = "OK", "ERROR", "INTERNAL", "PERM", "ABORT", "BUSY", "LOCKED", "NOMEM",
-    "READONLY", "INTERRUPT", "IOERR", "CORRUPT", "NOTFOUND", "FULL", "CANTOPEN",
-    "PROTOCOL", "EMPTY", "SCHEMA", "TOOBIG", "CONSTRAINT", "MISMATCH", "MISUSE",
-    "NOLFS", "AUTH", "FORMAT", "RANGE", "NOTADB", "NOTICE", "WARNING",
-    [100] = "ROW", [101] = "DONE"
-}
-
-local TYPE = {"INTEGER", "FLOAT", "TEXT", "BLOB", "NULL"}
-
-local DB = {}
-DB.__index = DB
-
-function DB:execute(sql, ...)
-    local pstmt = ffi.new("sqlite3_stmt *[1]")
-    local res = CODE[C.sqlite3_prepare_v2(self.db, sql, #sql, pstmt, nil)]
-    if res ~= "OK" then error(sql) end
-    local stmt = pstmt[0]
-    local arg = {...}
-    for i, v in ipairs(arg) do
-        if type(v) == "nil" then
-            C.sqlite3_bind_null(stmt, i)
-        elseif type(v) == "number" then
-            C.sqlite3_bind_double(stmt, i, v)
-        elseif type(v) == "string" then
-            C.sqlite3_bind_text(stmt, i, v, #v, ffi.cast("void(*)(void*)", 0))
-        else
-            error(("invalid type for query parameter: %s"):format(type(v)))
-        end
-    end
-    local rows = {}
-    repeat
-        local done = true
-        local res = CODE[C.sqlite3_step(stmt)]
-        -- TODO: handle res == "BUSY"
-        if res == "ROW" then
-            local row = {}
-            local ncols = C.sqlite3_column_count(stmt)
-            for i = 0, ncols-1 do
-                local col_name = ffi.string(C.sqlite3_column_name(stmt, i))
-                local col_type = TYPE[C.sqlite3_column_type(stmt, i)]
-                local value
-                if col_type == "INTEGER" then
-                    value = C.sqlite3_column_int(stmt, i)
-                elseif col_type == "FLOAT" then
-                    value = C.sqlite3_column_double(stmt, i)
-                elseif col_type == "TEXT" then
-                    value = ffi.string(C.sqlite3_column_text(stmt, i))
-                elseif col_type == "BLOB" then
-                    value = C.sqlite3_column_blob(stmt, i)
-                end
-                row[col_name] = value
-            end
-            table.insert(rows, row)
-            done = false
-        end
-    until done
-    C.sqlite3_finalize(stmt)
-    return rows
-end
-
-function DB:close()
-    C.sqlite3_close(self.db)
-end
-
-local function open(fname)
-    local self = setmetatable({}, DB)
-    local pdb = ffi.new("sqlite3 *[1]")
-    C.sqlite3_open(fname, pdb)
-    self.db = pdb[0]
-    self.err = ffi.new("char *[1]")
-    return self
-end
-
-return {open=open}

diff --git a/src/tcp.lua b/src/tcp.lua
deleted file mode 100644
index 5b60b57..0000000
--- a/src/tcp.lua
+++ /dev/null
@@ -1,161 +0,0 @@
-local ffi = require "ffi"
-local bit = require "bit"
-
-ffi.cdef[[
-int fcntl(int fd, int cmd, ... /* arg */ );
-typedef int socklen_t;
-struct sockaddr {
-    unsigned short   sa_family;
-    char             sa_data[14];
-};
-struct addrinfo {
-    int              ai_flags;
-    int              ai_family;
-    int              ai_socktype;
-    int              ai_protocol;
-    socklen_t        ai_addrlen;
-    struct sockaddr *ai_addr;
-    char            *ai_canonname;
-    struct addrinfo *ai_next;
-};
-typedef union epoll_data {
-    void    *ptr;
-    int      fd;
-    uint32_t u32;
-    uint64_t u64;
-} epoll_data_t;
-struct epoll_event {
-    uint32_t     events;    /* Epoll events */
-    epoll_data_t data;      /* User data variable */
-};
-int getaddrinfo(
-    const char *restrict node,
-    const char *restrict service,
-    const struct addrinfo *restrict hints,
-    struct addrinfo **restrict res
-);
-void freeaddrinfo(struct addrinfo *res);
-const char *gai_strerror(int errcode);
-int close(int fildes);
-int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);
-int socket(int domain, int type, int protocol);
-int bind(int socket, const struct sockaddr *address, socklen_t address_len);
-int listen(int socket, int backlog);
-int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
-ssize_t recv(int socket, void *buffer, size_t length, int flags);
-ssize_t send(int socket, const void *buffer, size_t length, int flags);
-int shutdown(int socket, int how);
-int epoll_create(int size);
-int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
-enum {F_GETFL = 3, F_SETFL = 4};
-enum {O_NONBLOCK = 2048};
-enum {AF_UNSPEC = 0};
-enum {SOCK_STREAM = 1};
-enum {AI_PASSIVE = 1};
-enum {EPOLLIN = 1, EPOLLET = -2147483648};
-enum {EPOLL_CTL_ADD = 1, EPOLL_CTL_DEL = 2};
-enum {SHUT_RDWR = 2};
-enum {SOL_SOCKET = 1};
-enum {SO_REUSEADDR = 2};
-]]
-local C = ffi.C
-
-local function set_non_blocking(sockfd)
-    local flags = C.fcntl(sockfd, C.F_GETFL, 0)
-    flags = bit.bor(flags, C.O_NONBLOCK)
-    C.fcntl(sockfd, C.F_SETFL, flags)
-end
-
-local TCP = {}
-TCP.__index = TCP
-
-function TCP:init(port)
-    local hints = ffi.new("struct addrinfo[1]")
-    hints[0].ai_family = C.AF_UNSPEC
-    hints[0].ai_socktype = C.SOCK_STREAM
-    hints[0].ai_flags = C.AI_PASSIVE
-
-    local servinfo = ffi.new("struct addrinfo[1][1]")
-    servinfo = ffi.cast("struct addrinfo **", servinfo)
-
-    local err = C.getaddrinfo(nil, tostring(port), hints, servinfo)
-    assert(err == 0, ("getaddrinfo error: %s"):format(ffi.string(C.gai_strerror(err))))
-    addr = servinfo[0][0]
-    local val = ffi.new("int [1]", 1) -- for setsockopt()
-    local sockfd = -1
-    while addr ~= nil do
-        sockfd = C.socket(addr.ai_family, addr.ai_socktype, addr.ai_protocol)
-        if sockfd >= 0 then
-            set_non_blocking(sockfd)
-            C.setsockopt(sockfd, C.SOL_SOCKET, C.SO_REUSEADDR, val, ffi.sizeof(val))
-            err = C.bind(sockfd, addr.ai_addr, addr.ai_addrlen)
-            if err >= 0 then
-                break
-            end
-            C.close(sockfd)
-        end
-        addr = addr.ai_next
-    end
-    assert(addr ~= nil, "bind error")
-    C.freeaddrinfo(servinfo[0])
-    assert(C.listen(sockfd, self.backlog) >= 0)
-    self.sockfd = sockfd
-end
-
-function TCP:run()
-    local efd = C.epoll_create(self.max_poll_size)
-    local ev = ffi.new("struct epoll_event[1]")
-    ev[0].events = bit.bor(C.EPOLLIN, C.EPOLLET)
-    ev[0].data.fd = self.sockfd
-    assert(C.epoll_ctl(efd, C.EPOLL_CTL_ADD, self.sockfd, ev) >= 0, "epoll_ctl error")
-
-    local evs = ffi.new("struct epoll_event["..self.max_poll_size.."]")
-    local curfds = 1
-
-    local client_addr = ffi.new("struct sockaddr[1]")
-    local addr_size = ffi.new("socklen_t[1]", ffi.sizeof(client_addr[0]))
-    local buflen = 4096
-    local buffer = ffi.new("char ["..buflen.."]")
-    local running = true
-    while running do
-        nfds = C.epoll_wait(efd, evs, curfds, -1)
-        assert(nfds >= 0, "epoll_wait error")
-        for n = 0, nfds-1 do
-            if evs[n].data.fd == self.sockfd then
-                local newfd = C.accept(self.sockfd, client_addr, addr_size)
-                if newfd < 0 then
-                    break
-                end
-                set_non_blocking(newfd)
-                ev[0].events = bit.bor(C.EPOLLIN, C.EPOLLET)
-                ev[0].data.fd = newfd
-                assert(C.epoll_ctl(efd, C.EPOLL_CTL_ADD, newfd, ev) >= 0, "epoll_ctl error")
-                curfds = curfds + 1
-            else
-                C.recv(evs[n].data.fd, buffer, buflen, 0)
-                local data = self:process(ffi.string(buffer))
-                if data == nil then
-                    running = false
-                else
-                    C.send(evs[n].data.fd, data, #data, 0)
-                end
-                C.epoll_ctl(efd, C.EPOLL_CTL_DEL, evs[n].data.fd, ev)
-                C.shutdown(evs[n].data.fd, C.SHUT_RDWR)
-                curfds = curfds - 1
-                C.close(evs[n].data.fd)
-            end
-        end
-    end
-    C.shutdown(self.sockfd, C.SHUT_RDWR)
-    C.close(self.sockfd)
-end
-
-local function new_tcp(max_poll_size, backlog)
-    local self = setmetatable({}, TCP)
-    self.max_poll_size = max_poll_size
-    self.backlog = backlog
-    return self
-end
-
-return {new_tcp=new_tcp}

diff --git a/src/template.lua b/src/template.lua
deleted file mode 100644
index a28c087..0000000
--- a/src/template.lua
+++ /dev/null
@@ -1,122 +0,0 @@
-local function escape(str)
-    local esc_tab = {['"']=34, ["&"]=38, ["<"]=60, [">"]=62, ["{"]=123, ["}"]=125}
-    local esc = ""
-    for i = 1, #str do
-        local c = str:sub(i, i)
-        local n = esc_tab[c]
-        if n ~= nil then
-            c = "&#" .. n .. ";"
-        end
-        esc = esc .. c
-    end
-    return esc
-end
-
-local function eval(str, env)
-    local expr = str:gsub("%$(%w+)", "env.%1")
-    local code = "return function(env) return "..expr.." end"
-    return loadstring(code, "eval")()(env)
-end
-
-local function find_matching_end(lines, start, last)
-    local level = 1
-    local else_line
-    for line_num = start+1, last do
-        local line = lines[line_num]
-        if line:sub(1, 1) == "%" then
-            local keyword = line:match("%%%s*(%w+).*")
-            if keyword == "end" then
-                level = level - 1
-                if level == 0 then
-                    return line_num, else_line
-                end
-            elseif keyword == "else" then
-                if level == 1 then
-                    else_line = line_num
-                end
-            elseif keyword ~= "set" and keyword ~= "include" then
-                level = level + 1
-            end
-        end
-    end
-    error(("line %d: no matching end"):format(start))
-end
-
-local function render_block(lines, first, last, env)
-    local result = ""
-    local function raw_sub(s) return eval(s, env) end
-    local function esc_sub(s) return escape(tostring(eval(s, env))) end
-    local line_num = first
-    while line_num <= last do
-        local line = lines[line_num]
-        if line:match("%%%s*(%w+).*") == "set" then
-            local var, expr = line:match("%%%s*set%s+([%w_]+)%s*=%s*(.+)")
-            assert(var and expr)
-            env[var] = eval(expr, env)
-            line_num = line_num + 1
-        elseif line:match("%%%s*(%w+).*") == "include" then
-            local fname = line:match("%%%s*include%s+(.+)")
-            local str = io.input(fname):read("*a")
-            assert(str:sub(#str) == "\n")
-            local inc_lines = {}
-            for inc_line in str:gmatch("(.-)\r?\n") do
-                inc_line = inc_line:gsub("^%s*%%", "%%")
-                table.insert(inc_lines, inc_line)
-            end
-            result = result .. render_block(inc_lines, 1, #inc_lines, env)
-            line_num = line_num + 1
-        elseif line:sub(1, 1) == "%" then   -- block start
-            local block_type = line:match("%%%s*(%w+).*")
-            local block_end, else_line = find_matching_end(lines, line_num, last)
-            if block_type == "if" then
-                local expr = line:match("%%%s*if%s+(.+)%s+then")
-                assert(expr)
-                if eval(expr, env) then
-                    local true_end = else_line or block_end
-                    result = result .. render_block(lines, line_num+1, true_end-1, env)
-                elseif else_line ~= nil then
-                    result = result .. render_block(lines, else_line+1, block_end-1, env)
-                end
-            elseif block_type == "while" then
-                local expr = line:match("%%%s*while%s+(.+)%s+do")
-                assert(expr)
-                while eval(expr, env) do
-                    result = result .. render_block(lines, line_num+1, block_end-1, env)
-                end
-            elseif block_type == "for" then
-                local var, expr = line:match("%%%s*for%s+([%w_]+)%s+in%s+(.+)%s+do")
-                assert(var and expr)
-                for i, v in ipairs(eval(expr, env)) do
-                    env[var] = v
-                    result = result .. render_block(lines, line_num+1, block_end-1, env)
-                end
-            end
-            line_num = block_end + 1
-        else
-            local line = line:gsub("{{([^!].-)}}", esc_sub)
-            local line = line:gsub("{{!(.-)}}", raw_sub)
-            if #line > 0 then
-                result = result .. line .. "\n"
-            end
-            line_num = line_num + 1
-        end
-    end
-    return result
-end
-
-local function render_str(str, env)
-    assert(str:sub(#str) == "\n")
-    local lines = {}
-    for line in str:gmatch("(.-)\r?\n") do
-        line = line:gsub("^%s*%%", "%%")
-        table.insert(lines, line)
-    end
-    return render_block(lines, 1, #lines, env)
-end
-
-local function render_file(fname, env)
-    local str = io.input(fname):read("*a")
-    return render_str(str, env)
-end
-
-return {escape=escape, render_str=render_str, render_file=render_file}