2022-02-26 15:44:55 (UTC-03:00)
Marcel Rodrigues <marcelgmr@gmail.com>
first commit
diff --git a/src/app.lua b/src/app.lua new file mode 100644 index 0000000..352965b --- /dev/null +++ b/src/app.lua @@ -0,0 +1,33 @@ +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 + +local function new_app(routes) + local obj = setmetatable({}, App) + obj.routes = routes or {} + obj.server = http.new_http() + function obj.server:process(req) + 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 "nothing here", 404, "Not found" + end + return obj +end + +return {new_app=new_app} diff --git a/src/crypt.lua b/src/crypt.lua new file mode 100644 index 0000000..fa23583 --- /dev/null +++ b/src/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/src/http.lua b/src/http.lua new file mode 100644 index 0000000..b13a0f2 --- /dev/null +++ b/src/http.lua @@ -0,0 +1,90 @@ +local tcp = require "tcp" + +local function parse_uri(uri) + local path, query_str, fragment = uri:match("([^?#]*)%??([^#]*)#?(.*)") + 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 path, query, fragment +end + +local function parse_request(data) + local req = {payload="", headers={}} + local stage = "status" + for line in data:gmatch("(.-)\r?\n") do + if stage == "status" then + local target, protocol + req.method, target, protocol = line:match("(%S*) (%S*) (%S*)") + assert(protocol == "HTTP/1.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 + 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 .. "\r\n" + end + return data +end + +local function build_response(data, status, reason, cookies) + if status == nil then + status = 200 + reason = "OK" + end + local cookie_data = build_cookie_data(cookies) + local fmt = "HTTP/1.1 %03d %s\r\n%s\r\n%s" + return fmt:format(status, reason, cookie_data, 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 new file mode 100644 index 0000000..69cd5b4 --- /dev/null +++ b/src/ludweb.lua @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..6e7d488 --- /dev/null +++ b/src/sqlite.lua @@ -0,0 +1,42 @@ +local ffi = require "ffi" + +ffi.cdef[[ +typedef struct sqlite3 sqlite3; +int sqlite3_open( + const char *filename, /* Database filename (UTF-8) */ + sqlite3 **ppDb /* OUT: SQLite db handle */ +); +int sqlite3_close(sqlite3*); +int sqlite3_exec( + sqlite3*, /* An open database */ + const char *sql, /* SQL to be evaluated */ + int (*callback)(void*,int,char**,char**), /* Callback function */ + void *, /* 1st argument to callback */ + char **errmsg /* Error msg written here */ +); +]] +local C = ffi.load("sqlite3") + +local DB = {} +DB.__index = DB + +function DB:execute(sql, cb, arg) + if C.sqlite3_exec(self.db, sql, cb, arg, self.err) ~= 0 then + print(ffi.string(self.err[0])) + end +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 new file mode 100644 index 0000000..5b60b57 --- /dev/null +++ b/src/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/src/template.lua b/src/template.lua new file mode 100644 index 0000000..75c7f31 --- /dev/null +++ b/src/template.lua @@ -0,0 +1,94 @@ +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 + if line:match("%%%s+end") then + level = level - 1 + if level == 0 then + return line_num, else_line + end + elseif line:match("%%%s+else") then + if level == 1 then + else_line = line_num + end + elseif line:match("%%%s+set") == nil 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 sub(s) return 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: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 computed = line:gsub("{{(.-)}}", sub) + if #computed > 0 then + result = result .. computed .. "\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 {render_str=render_str, render_file=render_file} diff --git a/test/run_tests.lua b/test/run_tests.lua new file mode 100644 index 0000000..260a57b --- /dev/null +++ b/test/run_tests.lua @@ -0,0 +1,61 @@ +local ffi = require "ffi" + +ffi.cdef[[ +typedef unsigned long ino_t; +typedef unsigned long off_t; +struct dirent { + ino_t d_ino; /* Inode number */ + off_t d_off; /* Not an offset; see below */ + unsigned short d_reclen; /* Length of this record */ + unsigned char d_type; /* Type of file; not supported + by all filesystem types */ + char d_name[256]; /* Null-terminated filename */ +}; +int scandir(const char *restrict dirp, + struct dirent ***restrict namelist, + int (*filter)(const struct dirent *), + int (*compar)(const struct dirent **, + const struct dirent **)); +int alphasort(const struct dirent **a, const struct dirent **b); +]] +local C = ffi.C + +local function listdir(path, filter) + local c_filter = filter and function (entry) + return filter(ffi.string(entry.d_name)) + end + local pentries = ffi.new("struct dirent **[1]") + local n = C.scandir(path, pentries, c_filter, C.alphasort) + local entries = pentries[0] + local list = {} + for i = 1, n do + table.insert(list, ffi.string(entries[i-1].d_name)) + end + return list +end + +local function run_pattern(pattern) + local function filter(name) + return name:match(pattern) ~= nil + end + local list = listdir(".", filter) + local total_test_count = 0 + local mod_count = 0 + for _, name in ipairs(list) do + local modname = name:match(pattern) + print(modname) + local Suite = require(modname) + local test_count = 0 + for funcname in pairs(Suite) do + print(" "..funcname) + Suite[funcname]() + test_count = test_count + 1 + end + print(" "..test_count.." test(s) OK") + total_test_count = total_test_count + test_count + mod_count = mod_count + 1 + end + print(total_test_count.." test(s) in "..mod_count.." suite(s) OK") +end + +run_pattern("^(test_.*)%.lua") diff --git a/test/test_crypt.lua b/test/test_crypt.lua new file mode 100644 index 0000000..ac9f9d1 --- /dev/null +++ b/test/test_crypt.lua @@ -0,0 +1,101 @@ +local lud = require "ludweb" + +local hex = lud.crypt.hex + +local Suite = {} + +local b64_cases = { + -- generated by coreutils' `base64` command + {"Lorem Ipsum", "TG9yZW0gSXBzdW0="}, + {"dolor sit", "ZG9sb3Igc2l0"}, + {"amet", "YW1ldA=="}, +} + +function Suite.b64_enc() + for i, case in ipairs(b64_cases) do + local msg = "failed for '"..case[1].."'" + assert(lud.crypt.b64_enc(case[1]) == case[2], msg) + end +end + +function Suite.b64_dec() + for i, case in ipairs(b64_cases) do + local msg = "failed for '"..case[1].."'" + assert(lud.crypt.b64_dec(case[2]) == case[1], msg) + end +end + +function Suite.urandom() + for i = 1, 32 do + assert(#lud.crypt.urandom(i) == i) + end +end + +function Suite.uuid4() + local ids = {} + local n = 32 + for i = 1, n do + local id = lud.crypt.uuid4() + assert(#id == 16, "UUID has bad length: "..#id) + ids[id] = true + end + local j = 0 + for id in pairs(ids) do + j = j + 1 + end + assert(j == n, "UUIDs not unique") +end + +-- $ printf "Lorem Ipsum" | awk '{for(i=1; i<=256; i++) print $0}' +local big_str = ("Lorem Ipsum\n"):rep(256) + +function Suite.sha256() + local cases = { + -- generated by coreutils' `sha256sum` command + {"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", ""}, + {"16aba5393ad72c0041f5600ad3c2c52ec437a2f0c7fc08fadfc3c0fe9641d7a3", "Lorem ipsum dolor sit amet"}, + {"181eb9ac4d4d08b00a64d636ab7a6174c3781c32b3b3a32b8783e9dda7be3455", big_str}, + } + for i, case in ipairs(cases) do + assert(hex(lud.crypt.sha256(case[2])) == case[1], "failed for case "..i) + end +end + +function Suite.hmac() + local cases = { + -- generated by coreutils' `hmac256` command + {"bce68cb87da59c708eaff17571c501dee1c4aeed50d9552f0b8644286317bcc9", "test123", ""}, + {"dce75555c455f477dd2f54d08c78277b3f3c12ba7f69d78b0222eb7b85438e89", "foo", "Lorem Ipsum"}, + {"5dcbecdb10bb78da2ca11fe6849578e1d4a23bdcf23a71b8fd7a18f5d968a922", "kind-of-a-long-key-for-hmac", big_str}, + } + for i, case in ipairs(cases) do + assert(hex(lud.crypt.hmac(case[2], case[3])) == case[1], "failed for case "..i) + end +end + +function Suite.pbkdf2() + -- test vector from RFC 7914, section 11 + local cases = { + {"passwd", "salt", 1, 64, [[ + 55 ac 04 6e 56 e3 08 9f ec 16 91 c2 25 44 b6 05 + f9 41 85 21 6d de 04 65 e6 8b 9d 57 c2 0d ac bc + 49 ca 9c cc f1 79 b6 45 99 16 64 b3 9d 77 ef 31 + 7c 71 b8 45 b1 e3 0b d5 09 11 20 41 d3 a1 97 83 + ]]}, + -- the following takes a few seconds; comment it out for quick test + {"Password", "NaCl", 80000, 64, [[ + 4d dc d8 f6 0b 98 be 21 83 0c ee 5e f2 27 01 f9 + 64 1a 44 18 d0 4c 04 14 ae ff 08 87 6b 34 ab 56 + a1 d4 25 a1 22 58 33 54 9a db 84 1b 51 c9 b3 17 + 6a 27 2b de bb a1 d0 78 47 8f 62 b3 97 f3 3c 8d + ]]}, + } + for i, case in ipairs(cases) do + local pw, salt, c, dklen, hash = unpack(case) + hash = hash:gsub("%s", "") + local my_hash = hex(lud.crypt.pbkdf2(pw, salt, c, dklen)) + assert(my_hash == hash) + end +end + +return Suite diff --git a/test/test_template.lua b/test/test_template.lua new file mode 100644 index 0000000..cf72382 --- /dev/null +++ b/test/test_template.lua @@ -0,0 +1,83 @@ +local lud = require "ludweb" + +local render = lud.template.render_str + +local Suite = {} + +function Suite.render_nop() + local str = "foo bar\n" + assert(render(str) == str) +end + +function Suite.render_literal_expr() + local cases = { + {"foo {{13}} bar", "foo 13 bar"}, + {"foo {{13+21}} bar", "foo 34 bar"}, + {"{{(13+21)..' things'}}", "34 things"}, + } + for i, case in ipairs(cases) do + assert(render(case[1].."\n") == case[2].."\n", "failed for case "..i) + end +end + +function Suite.render_var_expr() + local cases = { + {"foo {{$n}} {{$foo}}", "foo 42 bar"}, + {"foo {{$n+21}} {{$foo}}", "foo 63 bar"}, + {"{{($n+21)..' things'}}", "63 things"}, + } + local env = {n=42, foo="bar"} + for i, case in ipairs(cases) do + assert(render(case[1].."\n", env) == case[2].."\n", "failed for case "..i) + end +end + +function Suite.render_assign() + local str = ([[ + % set foo = "bar" + foo = {{$foo}} + ]]):gsub(" *$", "") + local r = render(str, {foo=true}) + assert(r:gsub("^%s*", "") == "foo = bar\n") +end + +function Suite.render_if() + local str = ([[ + % if $foo then + foo is true + % else + foo is false + % end + ]]):gsub(" *$", "") + local r + r = render(str, {foo=true}) + assert(r:gsub("^%s*", "") == "foo is true\n") + r = render(str, {foo=false}) + assert(r:gsub("^%s*", "") == "foo is false\n") +end + +function Suite.render_while() + local str = ([[ + % set i = 1 + % while $i <= 3 do + i = {{$i}} + % set i = $i + 1 + % end + ]]):gsub(" *$", "") + local r = render(str, {foo=true}) + r = r:gsub("^ *", ""):gsub("\n *", "\n") + assert(r == "i = 1\ni = 2\ni = 3\n") +end + +function Suite.render_for() + local str = ([[ + % for i in {2, 3, 5} do + i = {{$i}} + % end + ]]):gsub(" *$", "") + local r = render(str, {foo=true}) + r = r:gsub("^ *", ""):gsub("\n *", "\n") + assert(r == "i = 2\ni = 3\ni = 5\n") +end + +return Suite