├── luv.so ├── lhttp_parser.so ├── log.lua ├── path-filter.lua ├── .gitmodules ├── samples ├── test-bench.js ├── test-tcp.lua ├── test-bench.lua ├── test-errordocument.lua ├── test-pathfilter.lua ├── test-basicauth.lua ├── test-web.lua ├── test-upload.lua ├── test-stream.lua └── sample-streams.lua ├── error-document.lua ├── tick.lua ├── utils ├── path-filter.lua └── error-document.lua ├── Makefile ├── tests ├── test-base64.lua ├── test-web.lua └── test-autoheaders.lua ├── basic-auth.lua ├── repl.lua ├── base64.lua ├── send.lua ├── ensure.lua ├── moonslice.lua ├── fiber.lua ├── stream.lua ├── uv.lua ├── README.md ├── utils.lua ├── sha1.lua ├── autoheaders.lua ├── mime.lua ├── web.lua └── websocket.lua /luv.so: -------------------------------------------------------------------------------- 1 | luv/luv.so -------------------------------------------------------------------------------- /lhttp_parser.so: -------------------------------------------------------------------------------- 1 | lhttp_parser/lhttp_parser.so -------------------------------------------------------------------------------- /log.lua: -------------------------------------------------------------------------------- 1 | return function (app) 2 | return function (req, res) 3 | app(req, function (code, headers, body) 4 | print(req.method .. ' ' .. req.url.path .. ' ' .. code) 5 | res(code, headers, body) 6 | end) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /path-filter.lua: -------------------------------------------------------------------------------- 1 | return function (app, pathfilter, f, ...) 2 | local f2 = f(app, ...) 3 | return function (req, res) 4 | if pathfilter(req.url.path) then 5 | f2(req, res) 6 | else 7 | app(req, res) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lhttp_parser"] 2 | path = lhttp_parser 3 | url = https://github.com/creationix/lhttp_parser.git 4 | [submodule "luv"] 5 | path = luv 6 | url = https://github.com/creationix/luv.git 7 | [submodule "lua-pwauth"] 8 | path = lua-pwauth 9 | url = https://github.com/devurandom/lua-pwauth.git 10 | -------------------------------------------------------------------------------- /samples/test-bench.js: -------------------------------------------------------------------------------- 1 | require('http').createServer(function (req, res) { 2 | res.writeHead(200, { 3 | "Content-Type": "text/plain", 4 | "Content-Length": 12 5 | }); 6 | res.end("Hello World\n"); 7 | }).listen(8080, function () { 8 | console.log("http server listening at http://localhost:8080/"); 9 | }); 10 | -------------------------------------------------------------------------------- /error-document.lua: -------------------------------------------------------------------------------- 1 | return function (app, options) 2 | return function (req, res) 3 | app(req, function (code, headers, body) 4 | local errordocument = options[code] 5 | if errordocument then 6 | errordocument(code, headers, body, res) 7 | else 8 | res(code, headers, body) 9 | end 10 | end) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /tick.lua: -------------------------------------------------------------------------------- 1 | 2 | local tickQueue = {} 3 | local function tick() return function (callback) 4 | table.insert(tickQueue, callback) 5 | end end 6 | 7 | local function flushTickQueue() 8 | while #tickQueue > 0 do 9 | local queue = tickQueue 10 | tickQueue = {} 11 | for i, v in ipairs(queue) do 12 | v() 13 | end 14 | end 15 | end 16 | 17 | return { 18 | tick = tick, 19 | flushTickQueue = flushTickQueue 20 | } 21 | -------------------------------------------------------------------------------- /utils/path-filter.lua: -------------------------------------------------------------------------------- 1 | local pathfilter = {} 2 | 3 | function pathfilter.equal(path) 4 | return function (urlpath) 5 | return urlpath == path 6 | end 7 | end 8 | 9 | function pathfilter.notequal(path) 10 | return function (urlpath) 11 | return urlpath ~= path 12 | end 13 | end 14 | 15 | function pathfilter.match(path) 16 | return function (urlpath) 17 | return urlpath:match(path) 18 | end 19 | end 20 | 21 | function pathfilter.notmatch(path) 22 | return function (urlpath) 23 | return not urlpath:match(path) 24 | end 25 | end 26 | 27 | return pathfilter 28 | -------------------------------------------------------------------------------- /samples/test-tcp.lua: -------------------------------------------------------------------------------- 1 | local p = require('utils').prettyPrint 2 | local run = require('luv').run 3 | local createServer = require('uv').createServer 4 | local fiber = require('fiber') 5 | 6 | local host = os.getenv("IP") or "0.0.0.0" 7 | local port = os.getenv("PORT") or 8080 8 | 9 | -- Implement a simple echo server 10 | createServer(host, port, function (client) 11 | fiber.new(function () 12 | repeat 13 | local chunk = fiber.await(client.read()) 14 | fiber.await(client.write(chunk)) 15 | until not chunk 16 | end)() 17 | end) 18 | print("tcp echo server listening at port " .. port) 19 | 20 | run('default') 21 | -------------------------------------------------------------------------------- /samples/test-bench.lua: -------------------------------------------------------------------------------- 1 | local p = require('utils').prettyPrint 2 | local runOnce = require('luv').runOnce 3 | local socketHandler = require('web').socketHandler 4 | local createServer = require('uv').createServer 5 | 6 | local host = os.getenv("IP") or "0.0.0.0" 7 | local port = os.getenv("PORT") or 8080 8 | 9 | local app = function (req, res) 10 | -- p{req=req,res=res} 11 | res(200, { 12 | ["Content-Type"] = "text/plain" 13 | }, "Hello World\n") 14 | end 15 | 16 | app = require('autoheaders')(app) 17 | 18 | p{app=app} 19 | 20 | app({ 21 | method = "GET", 22 | url = { path = "/" }, 23 | headers = {} 24 | }, p) 25 | 26 | createServer(host, port, socketHandler(app)) 27 | print("http server listening at http://localhost:8080/") 28 | 29 | require('luv').run('default') 30 | 31 | --repeat 32 | -- print(".\n") 33 | --until runOnce() == 0 34 | print("done.") 35 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | LUA=luajit 3 | 4 | all: lhttp_parser/lhttp_parser.so luv/luv.so 5 | 6 | lua-pwauth/Makefile: 7 | git submodule update --init --recursive lua-pwauth 8 | 9 | lua-pwauth/lua-pam/pam.so: lua-pwauth/Makefile 10 | $(MAKE) -C lua-pwauth 11 | 12 | lhttp_parser/Makefile: 13 | git submodule update --init --recursive lhttp_parser 14 | 15 | lhttp_parser/lhttp_parser.so: lhttp_parser/Makefile 16 | $(MAKE) -C lhttp_parser 17 | 18 | luv/Makefile: 19 | git submodule update --init --recursive luv 20 | 21 | luv/luv.so: luv/Makefile 22 | $(MAKE) -C luv 23 | 24 | test: 25 | @ $(LUA) tests/test-autoheaders.lua && \ 26 | $(LUA) tests/test-web.lua && \ 27 | echo "All Tests Passed..." && \ 28 | echo "Now go write more!" 29 | 30 | clean: 31 | $(MAKE) -C luv clean 32 | $(MAKE) -C luv/libuv clean 33 | $(MAKE) -C lhttp_parser clean 34 | $(MAKE) -C lhttp_parser/http-parser clean 35 | $(MAKE) -C lua-pwauth clean 36 | $(MAKE) -C lua-pwauth/lua-pam clean 37 | -------------------------------------------------------------------------------- /utils/error-document.lua: -------------------------------------------------------------------------------- 1 | local errordocument = {} 2 | 3 | function errordocument.text(text) 4 | return function (code, headers, body, res) 5 | res(code, headers, text) 6 | end 7 | end 8 | 9 | function errordocument.file(path) 10 | return function (code, headers, body, res) 11 | local file = io.open(path) 12 | body = file:read("*a") 13 | res(code, headers, body) 14 | end 15 | end 16 | 17 | function errordocument.execute(path) 18 | return function (code, headers, body, res) 19 | local chunk = loadfile(path) 20 | local success, err = pcall(chunk, code, headers, body, res) 21 | if not success then 22 | error("Failed to execute error document handler: " .. err) 23 | res(code, headers, body) 24 | end 25 | end 26 | end 27 | 28 | function errordocument.redirect(url) 29 | return function (code, headers, body, res) 30 | code = 302 31 | headers["Location"] = url 32 | res(code, headers, {}) 33 | end 34 | end 35 | 36 | return errordocument 37 | -------------------------------------------------------------------------------- /tests/test-base64.lua: -------------------------------------------------------------------------------- 1 | local base64 = require "base64" 2 | local describe = require('ensure').describe 3 | 4 | local dec = "Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure." 5 | local enc = "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=" 6 | 7 | describe("base64", function() 8 | it("encodes properly", function(done) 9 | assert(base64.encode(dec) == enc) 10 | done() 11 | end) 12 | it("decodes properly", function(done) 13 | assert(base64.decode(enc) == dec) 14 | done() 15 | end) 16 | end) 17 | -------------------------------------------------------------------------------- /samples/test-errordocument.lua: -------------------------------------------------------------------------------- 1 | local p = require('utils').prettyPrint 2 | local socketHandler = require('web').socketHandler 3 | local createServer = require('uv').createServer 4 | local edoc = require('utils.error-document') 5 | 6 | local host = os.getenv("IP") or "0.0.0.0" 7 | local port = os.getenv("PORT") or 8080 8 | 9 | local state = 1 10 | local app = function (req, res) 11 | local code = 200 12 | if state > 3 then 13 | code = 404 14 | end 15 | 16 | res(code, { 17 | ["Content-Type"] = "text/plain" 18 | }, {"Hello ", "World ", tostring(state), "\n"}) 19 | 20 | if req.url.path == "/" then 21 | state = state + 1 22 | end 23 | end 24 | 25 | app = require('error-document')(app, { 26 | [404] = edoc.text("TEST 404"), 27 | }) 28 | 29 | app = require('autoheaders')(app) 30 | 31 | app = require('log')(app) 32 | 33 | p{app=app} 34 | 35 | app({ 36 | method = "GET", 37 | url = { path = "/" }, 38 | headers = {} 39 | }, p) 40 | 41 | createServer(host, port, socketHandler(app)) 42 | print("http server listening at http://localhost:8080/") 43 | 44 | require('luv').run('default') 45 | 46 | print("done.") 47 | -------------------------------------------------------------------------------- /samples/test-pathfilter.lua: -------------------------------------------------------------------------------- 1 | local p = require('utils').prettyPrint 2 | local socketHandler = require('web').socketHandler 3 | local createServer = require('uv').createServer 4 | local pflt = require('utils.path-filter') 5 | 6 | local host = os.getenv("IP") or "0.0.0.0" 7 | local port = os.getenv("PORT") or 8080 8 | 9 | local app = function (req, res) 10 | res(200, { 11 | ["Content-Type"] = "text/plain" 12 | }, {"Hello ", "World\n"}) 13 | end 14 | 15 | app = require('path-filter')(app, pflt.equal("/f"), function (app) 16 | return function(req, res) 17 | res(404, {}, {"Bam!"}) 18 | end 19 | end) 20 | 21 | app = require('path-filter')(app, pflt.notmatch("/f"), function (app) 22 | return function(req, res) 23 | res(403, {}, {"Bingo!"}) 24 | end 25 | end) 26 | 27 | app = require('autoheaders')(app) 28 | 29 | app = require('log')(app) 30 | 31 | p{app=app} 32 | 33 | app({ 34 | method = "GET", 35 | url = { path = "/" }, 36 | headers = {} 37 | }, p) 38 | 39 | createServer(host, port, socketHandler(app)) 40 | print("http server listening at http://localhost:8080/") 41 | 42 | require('luv').run('default') 43 | 44 | print("done.") 45 | -------------------------------------------------------------------------------- /basic-auth.lua: -------------------------------------------------------------------------------- 1 | local base64 = require "base64" 2 | 3 | return function (app, options) 4 | return function (req, res) 5 | local authorization = req.headers.authorization 6 | if not authorization then 7 | return res(401,{["Content-Type"] = "text/plain", ["WWW-Authenticate"] = "Basic realm="..options.realm},"Please auth!") 8 | end 9 | 10 | local userpass_b64 = authorization:match("Basic%s+(.*)") 11 | if not userpass_b64 then 12 | return res(400, {["Content-Type"] = "text/plain"}, "Your browser sent a bad Authorization HTTP header!") 13 | end 14 | 15 | local userpass = base64.decode(userpass_b64) 16 | if not userpass then 17 | return res(400, {["Content-Type"] = "text/plain"}, "Your browser sent a bad Authorization HTTP header!") 18 | end 19 | 20 | local username, password = userpass:match("([^:]*):(.*)") 21 | if not (username and password) then 22 | return res(400, {["Content-Type"] = "text/plain"}, "Your browser sent a bad Authorization HTTP header!") 23 | end 24 | 25 | local success, err = options.provider:authenticate(username, password) 26 | if not success then 27 | return res(403,{["Content-Type"] = "text/plain", ["WWW-Authenticate"] = "Basic realm="..options.realm},"

Auth failed!

"..err.."

") 28 | end 29 | 30 | app(req, res) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /samples/test-basicauth.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | -- Workaround Lua module system for modules loaded by modules: 4 | package.path = package.path .. ";lua-?/?.lua;lua-pwauth/?.lua" 5 | package.cpath = package.cpath .. ";lua-?/?.so;lua-pwauth/lua-?/?.so" 6 | 7 | local p = require('utils').prettyPrint 8 | local socketHandler = require('web').socketHandler 9 | local createServer = require('uv').createServer 10 | 11 | local host = os.getenv("IP") or "0.0.0.0" 12 | local port = os.getenv("PORT") or 8080 13 | 14 | local app = function (req, res) 15 | res(200, { 16 | ["Content-Type"] = "text/plain" 17 | }, {"Hello ", "World\n"}) 18 | end 19 | 20 | --local sasl = require("pwauth").sasl 21 | --local provider = sasl.new{application="TEST", service="www", hostname="localhost", realm="TEST", mechanism=sasl.mechanisms.PLAIN} 22 | 23 | local pam = require("pwauth").pam 24 | local provider = pam.new("system-auth") 25 | 26 | app = require("basic-auth")(app, {realm="TEST", provider=provider}) 27 | 28 | app = require('autoheaders')(app) 29 | 30 | app = require('log')(app) 31 | 32 | p{app=app} 33 | 34 | app({ 35 | method = "GET", 36 | url = { path = "/" }, 37 | headers = {} 38 | }, p) 39 | 40 | createServer(host, port, socketHandler(app)) 41 | print("http server listening at http://localhost:8080/") 42 | 43 | require('luv').run('default') 44 | 45 | print("done.") 46 | -------------------------------------------------------------------------------- /samples/test-web.lua: -------------------------------------------------------------------------------- 1 | local p = require('utils').prettyPrint 2 | local run = require('luv').run 3 | local socketHandler = require('web').socketHandler 4 | local createServer = require('uv').createServer 5 | local websocket = require('websocket') 6 | 7 | local host = os.getenv("IP") or "0.0.0.0" 8 | local port = os.getenv("PORT") or 8080 9 | 10 | local app = function (req, res) 11 | -- p{req=req,res=res} 12 | if req.upgrade then 13 | local socket = websocket.upgrade(req) 14 | local function read() 15 | socket.read()(function (err, message) 16 | if err then error(err) end 17 | p(message) 18 | if message then 19 | socket.write("Hello " .. message)() 20 | read() 21 | else 22 | socket.write()() 23 | end 24 | end) 25 | end 26 | read() 27 | return 28 | end 29 | res(200, { 30 | ["Content-Type"] = "text/plain" 31 | }, {"Hello ", "World\n"}) 32 | end 33 | 34 | app = require('autoheaders')(app) 35 | 36 | app = require('log')(app) 37 | 38 | p{app=app} 39 | 40 | app({ 41 | method = "GET", 42 | url = { path = "/" }, 43 | headers = {} 44 | }, p) 45 | 46 | createServer(host, port, socketHandler(app)) 47 | print("http server listening at http://localhost:8080/") 48 | 49 | require('luv').run('default') 50 | 51 | --repeat 52 | -- print(".\n") 53 | --until run('once') == 0 54 | print("done.") 55 | -------------------------------------------------------------------------------- /samples/test-upload.lua: -------------------------------------------------------------------------------- 1 | local p = require('utils').prettyPrint 2 | local dump = require('utils').dump 3 | local run = require('luv').run 4 | local socketHandler = require('web').socketHandler 5 | local createServer = require('uv').createServer 6 | local newStream = require('stream').newStream 7 | local fiber = require('fiber') 8 | 9 | local host = os.getenv("IP") or "0.0.0.0" 10 | local port = os.getenv("PORT") or 8080 11 | 12 | local app = function (req, res) 13 | fiber.new(function () 14 | local parts = {} 15 | repeat 16 | local chunk = fiber.await(req.body.read()) 17 | if chunk then 18 | table.insert(parts, chunk) 19 | end 20 | until not chunk 21 | p(parts) 22 | req.body = parts 23 | local body = dump(req) .. "\n" 24 | res(200, { 25 | ["Content-Type"] = "text/plain" 26 | }, body) 27 | end)(function (err) 28 | if err then 29 | res(500, { 30 | ["Content-Type"] = "text/plain" 31 | }, err) 32 | end 33 | end) 34 | end 35 | 36 | app = require('autoheaders')(app) 37 | 38 | app = require('log')(app) 39 | 40 | p{app=app} 41 | 42 | local body = newStream() 43 | 44 | app({ 45 | method = "PUT", 46 | body = {read = body.read}, 47 | url = { path = "/" }, 48 | headers = {} 49 | }, p) 50 | 51 | body.write("Hello ")() 52 | body.write("World\n")() 53 | body.write()() 54 | 55 | createServer(host, port, socketHandler(app)) 56 | print("http server listening at http://localhost:8080/") 57 | 58 | repeat 59 | print(".\n") 60 | until run('once') == 0 61 | print("done.") 62 | -------------------------------------------------------------------------------- /repl.lua: -------------------------------------------------------------------------------- 1 | local utils = require('utils') 2 | local uv = require('luv') 3 | 4 | local buffer = '' 5 | local prompt = '>' 6 | 7 | io.stdin = uv.new_tty(0, 1) 8 | io.stdout = uv.new_tty(1) 9 | io.stderr = uv.new_tty(2) 10 | io.stdout.write = uv.write 11 | io.stderr.write = uv.write 12 | 13 | local function gatherResults(success, ...) 14 | local n = select('#', ...) 15 | return success, { n = n, ... } 16 | end 17 | 18 | local function printResults(results) 19 | for i = 1, results.n do 20 | results[i] = utils.dump(results[i]) 21 | end 22 | print(table.concat(results, '\t')) 23 | end 24 | 25 | local function evaluateLine(line) 26 | local chunk = buffer .. line 27 | local f, err = loadstring('return ' .. chunk, 'REPL') -- first we prefix return 28 | 29 | if not f then 30 | f, err = loadstring(chunk, 'REPL') -- try again without return 31 | end 32 | 33 | if f then 34 | buffer = '' 35 | local success, results = gatherResults(xpcall(f, debug.traceback)) 36 | 37 | if success then 38 | -- successful call 39 | if results.n > 0 then 40 | printResults(results) 41 | end 42 | else 43 | -- error 44 | print(results[1]) 45 | end 46 | else 47 | 48 | if err:match "''$" then 49 | -- Lua expects some more input; stow it away for next time 50 | buffer = chunk .. '\n' 51 | return '>>' 52 | else 53 | print(err) 54 | buffer = '' 55 | end 56 | end 57 | 58 | return '>' 59 | end 60 | 61 | uv.read_start(io.stdin) 62 | 63 | io.stdout:write(prompt .. " ") 64 | function io.stdin:ondata(line) 65 | evaluateLine(line) 66 | io.stdout:write(prompt .. " ") 67 | end 68 | function io.stdin:onend() 69 | os.exit() 70 | end 71 | 72 | uv.run() -------------------------------------------------------------------------------- /samples/test-stream.lua: -------------------------------------------------------------------------------- 1 | local p = require('utils').prettyPrint 2 | local run = require('luv').run 3 | local socketHandler = require('web').socketHandler 4 | local createServer = require('uv').createServer 5 | local fiber = require('fiber') 6 | 7 | local host = os.getenv("IP") or "0.0.0.0" 8 | local port = os.getenv("PORT") or 8080 9 | 10 | local tickQueue = {} 11 | local function nextTick(fn) 12 | table.insert(tickQueue, fn) 13 | end 14 | 15 | local body = { 16 | "Hello ", 17 | "World ", 18 | "A very long chunk goes here to test multiple byte lengths", 19 | {"1","2","3"}, 20 | "\n" 21 | } 22 | 23 | local app = function (req, res) 24 | -- p{req=req,res=res} 25 | 26 | local stream = {} 27 | local index = 1 28 | function stream:read() return function (callback) 29 | -- Make the stream sometimes async and sometimes sync 30 | if index > 3 then 31 | nextTick(function () 32 | callback(null, body[index]) 33 | index = index + 1 34 | end) 35 | else 36 | callback(null, body[index]) 37 | index = index + 1 38 | end 39 | end end 40 | 41 | res(200, { 42 | ["Content-Type"] = "text/plain" 43 | }, stream) 44 | end 45 | 46 | app = require('autoheaders')(app) 47 | 48 | app = require('log')(app) 49 | 50 | p{app=app} 51 | 52 | app({ 53 | method = "GET", 54 | url = { path = "/" }, 55 | headers = {} 56 | }, function (code, headers, body) 57 | fiber.new(function () 58 | -- Log the response and body chunks 59 | p(code, headers, body) 60 | repeat 61 | local chunk = fiber.await(body:read()) 62 | p(chunk) 63 | until not chunk 64 | end)() 65 | end) 66 | 67 | createServer(host, port, socketHandler(app)) 68 | print("http server listening at http://localhost:8080/") 69 | 70 | repeat 71 | while #tickQueue > 0 do 72 | p("flushing nextTick queue of length", #tickQueue) 73 | local queue = tickQueue 74 | tickQueue = {} 75 | for i, v in ipairs(queue) do 76 | v() 77 | end 78 | end 79 | p("waiting for further events...") 80 | until run('once') == 0 81 | 82 | --repeat 83 | -- print(".\n") 84 | -- 85 | print("done.") 86 | -------------------------------------------------------------------------------- /base64.lua: -------------------------------------------------------------------------------- 1 | local bit = require("bit") 2 | 3 | -- map: byte -> char 4 | local bytes = { 5 | "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", 6 | "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", 7 | "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", 8 | "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", 9 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/" 10 | } 11 | 12 | -- map: char -> byte 13 | local chars = {} 14 | for i, v in ipairs(bytes) do 15 | chars[v] = i 16 | end 17 | 18 | local base64 = {} 19 | 20 | function base64.decode(data) 21 | local parts = {} 22 | 23 | for i = 1, #data, 4 do 24 | local val = bit.lshift(chars[data:sub(i,i)] - 1, 18) 25 | + bit.lshift((chars[data:sub(i+1,i+1)] or 1) - 1, 12) 26 | + bit.lshift((chars[data:sub(i+2,i+2)] or 1) - 1, 6) 27 | + (chars[data:sub(i+3,i+3)] or 1) - 1 28 | 29 | table.insert(parts, string.char(bit.band(bit.rshift(val, 16), 0xff))) 30 | table.insert(parts, string.char(bit.band(bit.rshift(val, 8), 0xff))) 31 | table.insert(parts, string.char(bit.band(val, 0xff))) 32 | end 33 | 34 | if data:sub(#data-1) == "==" then 35 | parts[#parts] = nil 36 | parts[#parts] = nil 37 | elseif data:sub(#data) == "=" then 38 | parts[#parts] = nil 39 | end 40 | 41 | return table.concat(parts) 42 | end 43 | 44 | function base64.encode(data) 45 | local parts = {} 46 | 47 | for i = 1, #data, 3 do 48 | local val = bit.lshift(data:byte(i), 16) 49 | + bit.lshift(data:byte(i + 1) or 0, 8) 50 | + (data:byte(i + 2) or 0) 51 | 52 | table.insert(parts, bytes[bit.band(bit.rshift(val, 18), 0x3f) + 1]) 53 | table.insert(parts, bytes[bit.band(bit.rshift(val, 12), 0x3f) + 1]) 54 | table.insert(parts, bytes[bit.band(bit.rshift(val, 6), 0x3f) + 1]) 55 | table.insert(parts, bytes[bit.band(val, 0x3f) + 1]) 56 | end 57 | 58 | local rem = #data % 3 59 | if rem == 1 then 60 | parts[#parts] = "=" 61 | parts[#parts - 1] = "=" 62 | elseif rem == 2 then 63 | parts[#parts] = "=" 64 | end 65 | 66 | return table.concat(parts) 67 | end 68 | 69 | return base64 70 | -------------------------------------------------------------------------------- /send.lua: -------------------------------------------------------------------------------- 1 | local p = require('utils').prettyPrint 2 | local fs = require('uv').fs 3 | local await = require('fiber').await 4 | local wait = require('fiber').wait 5 | local newStream = require('stream').newStream 6 | local getType = require('mime').getType 7 | local floor = require('math').floor 8 | 9 | -- For encoding numbers using bases up to 64 10 | local digits = { 11 | "0", "1", "2", "3", "4", "5", "6", "7", 12 | "8", "9", "A", "B", "C", "D", "E", "F", 13 | "G", "H", "I", "J", "K", "L", "M", "N", 14 | "O", "P", "Q", "R", "S", "T", "U", "V", 15 | "W", "X", "Y", "Z", "a", "b", "c", "d", 16 | "e", "f", "g", "h", "i", "j", "k", "l", 17 | "m", "n", "o", "p", "q", "r", "s", "t", 18 | "u", "v", "w", "x", "y", "z", "_", "$" 19 | } 20 | local function numToBase(num, base) 21 | local parts = {} 22 | repeat 23 | table.insert(parts, digits[(num % base) + 1]) 24 | num = floor(num / base) 25 | until num == 0 26 | return table.concat(parts) 27 | end 28 | 29 | local function calcEtag(stat) 30 | return (not stat.is_file and 'W/' or '') .. 31 | '"' .. numToBase(stat.ino or 0, 64) .. 32 | '-' .. numToBase(stat.size, 64) .. 33 | '-' .. numToBase(stat.mtime, 64) .. '"' 34 | end 35 | local function sendFile(path, req, res) 36 | local err, fd = wait(fs.open(path, "r")) 37 | if not fd then 38 | return res(404, {}, err) 39 | end 40 | local stat = await(fs.fstat(fd)) 41 | local etag = calcEtag(stat) 42 | local code = 200 43 | local headers = { 44 | ['Last-Modified'] = os.date("!%a, %d %b %Y %H:%M:%S GMT", stat.mtime), 45 | ["ETag"] = etag 46 | } 47 | local body 48 | if req.headers["if-none-match"] == etag then 49 | code = 304 50 | end 51 | 52 | if code ~= 304 then 53 | headers["Content-Type"] = getType(path) 54 | headers["Content-Length"] = stat.size 55 | end 56 | 57 | if not (req.method == "HEAD" or code == 304) then 58 | body = newStream() 59 | end 60 | 61 | -- Start the response 62 | res(code, headers, body) 63 | 64 | if not body then return end 65 | 66 | -- Stream the file to the browser 67 | repeat 68 | local chunk = await(fs.read(fd, 10)) 69 | if #chunk == 0 then 70 | chunk = nil 71 | end 72 | await(body.write(chunk)) 73 | until not chunk 74 | 75 | wait(fs.close(fd)) 76 | end 77 | 78 | return { 79 | file = sendFile, 80 | numToBase = numToBase 81 | } 82 | -------------------------------------------------------------------------------- /tests/test-web.lua: -------------------------------------------------------------------------------- 1 | local web = require('web') 2 | local newStream = require('stream').newStream 3 | local await = require('fiber').await 4 | local p = require('utils').prettyPrint 5 | local describe = require('ensure').describe 6 | local same = require('ensure').same 7 | 8 | local function newPipe() 9 | local a = newStream() 10 | local b = newStream() 11 | return { write = a.write, read = b.read }, { write = b.write, read = a.read } 12 | end 13 | 14 | describe("web", function () 15 | 16 | it("should have a socketHandler function", function (done) 17 | assert(type(web) == "table") 18 | assert(type(web.socketHandler) == "function") 19 | done() 20 | end) 21 | 22 | describe("socketHandler", function () 23 | it("should return a function", function (done) 24 | local handler = web.socketHandler(function (req, res) end) 25 | assert(type(handler) == "function") 26 | done() 27 | end) 28 | 29 | it("should parse html and call app", function (done) 30 | local client, server = newPipe() 31 | client.write( 32 | "GET / HTTP/1.1\r\n" .. 33 | "User-Agent: curl/7.27.0\r\n" .. 34 | "Host: localhost:8080\r\n" .. 35 | "Accept: */*\r\n\r\n")() 36 | client.write()() 37 | web.socketHandler(function (req, res) 38 | assert(type(req.headers) == "table") 39 | assert(req.method == "GET") 40 | assert(req.upgrade == false) 41 | assert(same(req.url, {path="/"})) 42 | assert(same(req.headers, { 43 | ["user-agent"] = "curl/7.27.0", 44 | host = "localhost:8080", 45 | accept = "*/*" 46 | })) 47 | res(200, { 48 | ["Content-Type"] = "text/plain", 49 | ["Content-Length"] = 12 50 | }, "Hello World\n") 51 | local response = "" 52 | local expected = 53 | "HTTP/1.1 200 OK\r\n" .. 54 | "Content-Length: 12\r\n" .. 55 | "Content-Type: text/plain\r\n" .. 56 | "\r\n" .. 57 | "Hello World\n" 58 | local function read() 59 | client.read()(function (err, chunk) 60 | if err then error(err) end 61 | if chunk then 62 | if type(chunk) == "table" then 63 | chunk = table.concat(chunk) 64 | end 65 | response = response .. chunk 66 | if #response >= #expected then 67 | assert(expected == response) 68 | done() 69 | end 70 | read() 71 | else 72 | assert(expected == response) 73 | end 74 | end) 75 | end 76 | read() 77 | end)(server) 78 | end) 79 | end) 80 | 81 | 82 | 83 | end) -------------------------------------------------------------------------------- /samples/sample-streams.lua: -------------------------------------------------------------------------------- 1 | local uv = require('luv') 2 | local newStream = require('stream').newStream 3 | 4 | local function newFileReadStream(fd) 5 | local stream = newStream() 6 | local offset = 0 7 | local chunkSize = 40960 8 | local function read(err) 9 | if (err) error(err) 10 | uv.read(fd, offset, chunkSize, function (err, chunk) 11 | if err then error(err) end 12 | -- chunk will be nil when we've reached the end of the file 13 | if chunk then 14 | offset = offset + #chunk 15 | end 16 | -- The stream will call read immedietly if it wants more data 17 | -- It will call it later if it wants us to slow down 18 | stream.write(chunk)(read) 19 | end) 20 | end 21 | read() 22 | -- Export just the readable half 23 | return { 24 | read = stream.read 25 | } 26 | end 27 | 28 | local function newFileWriteStream(fd, onClose) 29 | local stream = newStream() 30 | local offset = 0 31 | local chunkSize = 40960 32 | local function write(err, chunk) 33 | if err error(err) 34 | if not chunk then 35 | return onClose() 36 | else 37 | uv.write(fd, offset, chunk, function (err) 38 | if err then error(err) end 39 | offset = offset + #chunk 40 | stream.read()(write) 41 | end) 42 | end 43 | stream.read()(write) 44 | -- Export just the writable half 45 | return { 46 | write = stream.write 47 | } 48 | end 49 | 50 | -- Handle is a uv_tcp_t instance from uv, it can be either client or server, 51 | -- the API is the same 52 | local function newHandleStream(handle) 53 | 54 | -- Connect data coming from the socket to emit on the stream 55 | local receiveStream = newStream() 56 | local function write(handle, chunk) 57 | -- If write doesn't callback sync, then we need to pause and resume the socket 58 | local async 59 | receiveStream.write(chunk)(function (err) 60 | if err then error(err) end 61 | if async == nil then async = false end 62 | if async then 63 | handle:readStart() 64 | end) 65 | if async == nil then 66 | async = true 67 | handle:readStop() 68 | end 69 | end 70 | handle.ondata = write 71 | handle.onend = write 72 | 73 | -- Connect data being written to the stream and write it to the handle 74 | local sendStream = newStream() 75 | local function read(err) 76 | if err then error(err) end 77 | local async 78 | sendStream.read()(function (err, chunk) 79 | if err then error(err) end 80 | if chunk then 81 | handle:write(chunk, read) 82 | else 83 | handle:shutdown(read) 84 | end 85 | end) 86 | end 87 | read() 88 | 89 | -- Return the halfs of the streams we're not using 90 | return { 91 | read = receiveStream.read, 92 | write = sendStream.write 93 | } 94 | end 95 | -------------------------------------------------------------------------------- /ensure.lua: -------------------------------------------------------------------------------- 1 | local p = require('utils').prettyPrint 2 | local wrap = require('fiber').new 3 | local pathKey = {} -- key for paths 4 | 5 | local pass = "\27[1;32m◀\27[0mPASS\27[1;32m▶\27[0m\27[0;32m " 6 | local fail = "\27[1;31m◀\27[0mFAIL\27[1;31m▶\27[0m\27[0;31m " 7 | local tests 8 | local index = 1 9 | local position 10 | local test 11 | 12 | -- Emulate Lua 5.1 getfenv if it is missing: 13 | local getfenv = getfenv or function(f, t) 14 | f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func) 15 | local name, env 16 | local up = 0 17 | repeat 18 | up = up + 1 19 | name, env = debug.getupvalue(f, up) 20 | until name == '_ENV' or name == nil 21 | return env 22 | end 23 | 24 | -- Emulate Lua 5.1 setfenv if it is missing: 25 | local setfenv = setfenv or function(f, t) 26 | f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func) 27 | local name 28 | local up = 0 29 | repeat 30 | up = up + 1 31 | name = debug.getupvalue(f, up) 32 | until name == '_ENV' or name == nil 33 | if name then 34 | debug.upvaluejoin(f, up, function() return t end, 1) -- use unique upvalue, set it to f 35 | end 36 | end 37 | 38 | local function run() 39 | test = tests[index] 40 | if not test then 41 | os.exit() 42 | end 43 | position = "(" .. index .. "/" .. #tests .. ") " 44 | index = index + 1 45 | wrap(test.block, function () 46 | print(position .. pass .. test.name .. "\27[0m") 47 | run() 48 | end)(function (err) 49 | if err then 50 | print(position .. fail .. test.name .. "\27[0m") 51 | print(err) 52 | run() 53 | end 54 | end) 55 | 56 | end 57 | 58 | local function describe(name, block, cleanup) 59 | local isOuter = not tests 60 | if isOuter then 61 | tests = {} 62 | end 63 | local parentenv = getfenv(block) 64 | local parentPath = parentenv[pathKey] 65 | local path = parentPath and (parentPath .. " - " .. name) or name 66 | local env = setmetatable({ 67 | [pathKey] = path, 68 | describe = describe, 69 | it = function (name, block) 70 | table.insert(tests, {name=path .. " - " .. name, block=block}) 71 | end 72 | }, { __index = parentenv }) 73 | setfenv(block, env) 74 | block() 75 | if isOuter then 76 | run() 77 | if cleanup then cleanup() end 78 | print(position .. fail .. test.name .. "\27[0m") 79 | print("Process exited before done() was called") 80 | os.exit(-1) 81 | end 82 | end 83 | 84 | local function same(a, b) 85 | if a == b then return true end 86 | if not (type(a) == "table" and type(b) == "table") then return false end 87 | for k, v in pairs(a) do 88 | if not same(b[k], v) then return false end 89 | end 90 | for k in pairs(b) do 91 | if not a[k] then return false end 92 | end 93 | return true 94 | end 95 | 96 | return { 97 | describe = describe, 98 | same = same 99 | } 100 | -------------------------------------------------------------------------------- /moonslice.lua: -------------------------------------------------------------------------------- 1 | local newFiber = require('fiber').new 2 | local wait = require('fiber').wait 3 | local fs = require('uv').fs 4 | local websocket = require('websocket') 5 | local sendFile = require('send').file 6 | local numToBase= require('send').numToBase 7 | 8 | local App = {} 9 | 10 | function App:get(path, fn) 11 | table.insert(self, {function (req) 12 | return req.method == "GET" and req.url.path:match(path) 13 | end, fn, "get " .. path}) 14 | end 15 | 16 | function App:post(path, fn) 17 | table.insert(self, {function (req) 18 | return req.method == "POST" and req.url.path:match(path) 19 | end, fn, "post " .. path}) 20 | end 21 | 22 | function App:put(path, fn) 23 | table.insert(self, {function (req) 24 | return req.method == "PUT" and req.url.path:match(path) 25 | end, fn, "put " .. path}) 26 | end 27 | 28 | function App:delete(path, fn) 29 | table.insert(self, {function (req) 30 | return req.method == "DELETE" and req.url.path:match(path) 31 | end, fn, "delete " .. path}) 32 | end 33 | 34 | function App:websocket(path, fn) 35 | table.insert(self, {function (req) 36 | return req.upgrade and req.url.path:match(path) 37 | end, function (req, res) 38 | fn(req, websocket.upgrade(req)) 39 | end, "websocket " .. path}) 40 | end 41 | 42 | function App:static(root, options) 43 | table.insert(self, {function (req) 44 | if not(req.method == "GET" or req.method == "HEAD") then 45 | return false 46 | end 47 | if options.index and req.url.path:sub(#req.url.path) == "/" then 48 | req.url.path = req.url.path .. options.index 49 | end 50 | local path = root .. req.url.path 51 | local err, stat = wait(fs.stat(path)) 52 | return stat and stat.is_file 53 | end, function (req, res) 54 | local path = root .. req.url.path 55 | sendFile(path, req, res) 56 | end, "static " .. root}) 57 | end 58 | 59 | -------------------------------------------------------------------------------- 60 | 61 | local appMeta = { __index = App } 62 | 63 | local index = 0 64 | function appMeta:__call(req, res) 65 | local id 66 | if self.log then 67 | index = index + 1 68 | id = numToBase(index, 64) 69 | local realRes = res 70 | res = function (code, headers, body) 71 | print("-> " .. id .. " " .. code) 72 | realRes(code, headers, body) 73 | end 74 | end 75 | newFiber(function() 76 | if id then 77 | local address = req.socket.address.address .. ":" .. req.socket.address.port 78 | print("<- " .. id .. " " .. req.method .. " " .. req.url.path .. " " .. address) 79 | end 80 | for i, pair in ipairs(self) do 81 | if pair[1](req) then 82 | if id then 83 | print("-- " .. id .. " " .. pair[3]) 84 | end 85 | return pair[2](req, res) 86 | end 87 | end 88 | return res(404, {}, "") 89 | end)(function (err) 90 | if err then 91 | err = tostring(err) .. "\n" 92 | io.stderr:write(err) 93 | return res(500, { 94 | ["Content-Length"] = #err, 95 | ["Content-Type"] = "text/plain" 96 | }, err) 97 | end 98 | end) 99 | end 100 | 101 | return function () 102 | return setmetatable({}, appMeta) 103 | end 104 | -------------------------------------------------------------------------------- /fiber.lua: -------------------------------------------------------------------------------- 1 | local coroutine = require('coroutine') 2 | local debug = require('debug') 3 | 4 | -- Make table.unpack accessible in Lua 5.2 5 | local unpack = unpack or table.unpack 6 | 7 | local fiber = {} 8 | 9 | -- Map of managed coroutines 10 | local fibers = {} 11 | 12 | local function check(co, success, err, ...) 13 | local fiber = fibers[co] 14 | 15 | if not success then 16 | err = debug.traceback(co, err, 1) 17 | if fiber and fiber.callback then 18 | return fiber.callback(err, ...) 19 | end 20 | error(err) 21 | end 22 | 23 | -- Abort on non-managed coroutines. 24 | if not fiber then 25 | return err, ... 26 | end 27 | 28 | -- If the fiber is done, pass the result to the callback and cleanup. 29 | if not fiber.paused then 30 | fibers[co] = nil 31 | if fiber.callback then 32 | fiber.callback(nil, err, ...) 33 | end 34 | return err, ... 35 | end 36 | 37 | fiber.paused = false 38 | end 39 | 40 | -- Create a managed fiber as a continuable 41 | function fiber.new(fn, ...) 42 | local args = {...} 43 | local nargs = select("#", ...) 44 | return function (callback) 45 | local co = coroutine.create(fn) 46 | local fiber = { 47 | callback = callback 48 | } 49 | fibers[co] = fiber 50 | 51 | check(co, coroutine.resume(co, unpack(args, 1, nargs))) 52 | end 53 | end 54 | 55 | -- Wait in this coroutine for the continuation to complete 56 | function fiber.wait(continuation) 57 | 58 | if type(continuation) ~= "function" then 59 | error("Continuation must be a function.") 60 | end 61 | 62 | -- Find out what thread we're running in. 63 | local co, isMain = coroutine.running() 64 | 65 | -- When main, Lua 5.1 `co` will be nil, lua 5.2, `isMain` will be true 66 | if not co or isMain then 67 | error("Can't wait from the main thread.") 68 | end 69 | 70 | local fiber = fibers[co] 71 | 72 | -- Execute the continuation 73 | local async, ret, nret 74 | continuation(function (...) 75 | 76 | -- If async hasn't been set yet, that means the callback was called before 77 | -- the continuation returned. We should store the result and wait till it 78 | -- returns later on. 79 | if not async then 80 | async = false 81 | ret = {...} 82 | nret = select("#", ...) 83 | return 84 | end 85 | 86 | -- Callback was called we can resume the coroutine. 87 | -- When it yields, check for managed coroutines 88 | check(co, coroutine.resume(co, ...)) 89 | 90 | end) 91 | 92 | -- If the callback was called early, we can just return the value here and 93 | -- not bother suspending the coroutine in the first place. 94 | if async == false then 95 | return unpack(ret, 1, nret) 96 | end 97 | 98 | -- Mark that the contination has returned. 99 | async = true 100 | 101 | -- Mark the fiber as paused if there is one. 102 | if fiber then fiber.paused = true end 103 | 104 | -- Suspend the coroutine and wait for the callback to be called. 105 | return coroutine.yield() 106 | end 107 | 108 | -- This is a wrapper around wait that strips off the first result and 109 | -- interprets is as an error to throw. 110 | function fiber.await(...) 111 | -- TODO: find out if there is a way to count the number of return values from 112 | -- fiber.wait while still storing the results in a table. 113 | local results = {fiber.wait(...)} 114 | local nresults = sel 115 | if results[1] then 116 | error(results[1]) 117 | end 118 | return unpack(results, 2) 119 | end 120 | 121 | return fiber 122 | -------------------------------------------------------------------------------- /stream.lua: -------------------------------------------------------------------------------- 1 | 2 | local Queue = {} 3 | -- Get an item from the font of the queue 4 | function Queue:shift() 5 | if self.index > self.headLength then 6 | -- When the head is empty, swap it with the tail to get fresh items 7 | self.head, self.tail = self.tail, self.head 8 | self.index = 1 9 | self.headLength = #self.head 10 | -- If it's still empty, return nothing 11 | if self.headLength == 0 then 12 | return 13 | end 14 | end 15 | 16 | -- There was an item in the head, let's pull it out 17 | local value = self.head[self.index] 18 | -- And remove it from the head 19 | self.head[self.index] = nil 20 | -- And bump the index 21 | self.index = self.index + 1 22 | self.length = self.length - 1 23 | return value 24 | end 25 | 26 | -- Put an item back on the queue 27 | function Queue:unshift(item) 28 | self.headLength = self.headLength + 1 29 | return table.insert(self.head, 1, item) 30 | end 31 | 32 | -- Push a new item on the back of the queue 33 | function Queue:push(item) 34 | -- Pushes always go to the write-only tail 35 | self.length = self.length + 1 36 | return table.insert(self.tail, item) 37 | end 38 | 39 | function Queue:initialize() 40 | end 41 | 42 | local metaQueue = {__index=Queue} 43 | 44 | local function newQueue() 45 | return setmetatable({ 46 | head = {}, 47 | tail = {}, 48 | index = 1, 49 | headLength = 0, 50 | length = 0 51 | }, metaQueue) 52 | end 53 | 54 | 55 | local function newStream() 56 | 57 | -- If there are more than this many buffered input chunks, readStop the source 58 | local highWaterMark = 1 59 | -- If there are less than this many buffered chunks, readStart the source 60 | local lowWaterMark = 1 61 | 62 | local paused = false 63 | local processing = false 64 | 65 | local inputQueue = newQueue() 66 | local readerQueue = newQueue() 67 | local resumeList = {} 68 | 69 | local function processReaders() 70 | if processing then return end 71 | processing = true 72 | while inputQueue.length > 0 and readerQueue.length > 0 do 73 | local chunk = inputQueue:shift() 74 | local reader = readerQueue:shift() 75 | reader(nil, chunk) 76 | end 77 | local watermark = inputQueue.length - readerQueue.length 78 | if not paused then 79 | if watermark > highWaterMark then 80 | paused = true 81 | end 82 | else 83 | if watermark < lowWaterMark then 84 | paused = false 85 | if #resumeList > 0 then 86 | local callbacks = resumeList 87 | resumeList = {} 88 | for i = 1, #callbacks do 89 | callbacks[i]() 90 | end 91 | end 92 | end 93 | end 94 | processing = false 95 | end 96 | 97 | local function read() return function (callback) 98 | readerQueue:push(callback) 99 | processReaders() 100 | end end 101 | 102 | local function write(chunk) return function (callback) 103 | inputQueue:push(chunk) 104 | processReaders() 105 | if callback then 106 | if paused then 107 | table.insert(resumeList, callback) 108 | else 109 | callback() 110 | end 111 | end 112 | end end 113 | 114 | return { 115 | read = read, 116 | write = write 117 | } 118 | end 119 | 120 | local function newPipe() 121 | -- Create two streams 122 | local a, b = newStream(), newStream() 123 | -- Cross their write functions 124 | a.write, b.write = b.write, a.write 125 | -- Return them as two duplex streams that are the two ends of the pipe 126 | return a, b 127 | end 128 | 129 | 130 | return { 131 | newStream = newStream, 132 | newPipe = newPipe 133 | } 134 | -------------------------------------------------------------------------------- /uv.lua: -------------------------------------------------------------------------------- 1 | local uv = require('luv') 2 | local newPipe = require('stream').newPipe 3 | 4 | local function noop() end 5 | local continuable = {} 6 | 7 | -- Handle is a uv_tcp_t instance from uv, it can be either client or server, 8 | -- the API is the same 9 | local function newHandleStream(handle) 10 | -- Get a duplex pipe from the stream library 11 | local internal, external = newPipe() 12 | -- Connect data coming from the socket to emit on the stream 13 | local function write(handle, chunk) 14 | -- If write doesn't callback sync, then we need to pause and resume the socket 15 | local async 16 | internal.write(chunk)(function (err) 17 | if err then error(err) end 18 | if async == nil then async = false end 19 | if async then 20 | uv.read_start(handle) 21 | end 22 | end) 23 | if async == nil then 24 | async = true 25 | uv.read_stop(handle) 26 | end 27 | end 28 | handle.ondata = write 29 | handle.onend = write 30 | uv.read_start(handle) 31 | 32 | -- Connect data being written to the stream and write it to the handle 33 | local function read(err) 34 | if err then error(err) end 35 | local async 36 | internal.read()(function (err, chunk) 37 | if err then 38 | uv.close(handle) 39 | error(err) 40 | end 41 | if chunk then 42 | uv.write(handle, chunk, read) 43 | else 44 | uv.shutdown(handle, function () 45 | uv.close(handle) 46 | end) 47 | end 48 | end) 49 | end 50 | read() 51 | 52 | external.address = uv.tcp_getpeername(handle) 53 | return external 54 | end 55 | 56 | function continuable.createServer(host, port, onConnection) 57 | local server = uv.new_tcp() 58 | uv.tcp_bind(server, host, port) 59 | function server:onconnection() 60 | local client = uv.new_tcp() 61 | uv.accept(server, client) 62 | onConnection(newHandleStream(client)) 63 | end 64 | uv.listen(server) 65 | return server 66 | end 67 | 68 | function continuable.createClient(address, port, onConnect) 69 | local client = uv.new_tcp() 70 | uv.tcp_connect(client, address, port, function (status) 71 | if status == 0 then 72 | onConnect(newHandleStream(client)) 73 | else 74 | onConnect(nil) 75 | end 76 | end ) 77 | return client 78 | end 79 | 80 | function continuable.timeout(ms) return function (callback) 81 | local timer = uv.new_timer() 82 | timer.ontimeout = function () 83 | callback() 84 | uv.close(timer) 85 | end 86 | uv.timer_start(timer, ms, 0) 87 | return timer 88 | end end 89 | 90 | function continuable.interval(ms) return function (callback) 91 | local timer = uv.new_timer() 92 | timer.ontimeout = callback 93 | uv.timer_start(timer, ms, ms) 94 | return timer 95 | end end 96 | 97 | local fs = {} 98 | continuable.fs = fs 99 | function fs.open(path, flags, mode) return function (callback) 100 | mode = mode or tonumber("666", 8) 101 | return uv.fs_open(path, flags, mode, callback) 102 | end end 103 | 104 | function fs.close(fd) return function (callback) 105 | return uv.fs_close(fd, callback or noop) 106 | end end 107 | 108 | function fs.stat(path) return function (callback) 109 | return uv.fs_stat(path, callback) 110 | end end 111 | function fs.fstat(fd) return function (callback) 112 | return uv.fs_fstat(fd, callback) 113 | end end 114 | function fs.lstat(path) return function (callback) 115 | return uv.fs_lstat(path, callback) 116 | end end 117 | 118 | function fs.read(fd, length, offset) return function (callback) 119 | return uv.fs_read(fd, length, offset, callback) 120 | end end 121 | 122 | function fs.write(fd, chunk, offset) return function (callback) 123 | return uv.fs_write(fd, chunk, offset, callback) 124 | end end 125 | 126 | return continuable 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Moonslice is a collection of interfaces and lua libraries. 2 | 3 | It's a mix of the code and technology from the luvit project mixed with some 4 | new experimental APIs that are designed to make it more lua friendly and easier 5 | to code. 6 | 7 | Two external dependencies are `luv` and `lhttp_parser`. The first is a new set 8 | of lua - libuv bindings designed to be standalone, minimal and fast. The latter 9 | is the http_parser bindings used in luvit, but packaged as a standalone project. 10 | 11 | Included in this directory is a `Makefile` that pulls in the gitsubmodules and 12 | builds the two libraries. The respective libraries that use them have symlinks 13 | in place already. 14 | 15 | # Continuable 16 | 17 | This is a collection of libraries that implement the continuable interface. 18 | 19 | In short, the continuable interface is like node.js style callbacks except the 20 | function that accepts the callback is returned from the initial function call 21 | as a continuable closure. 22 | 23 | ```lua 24 | fs.readFile("/path/to/file.txt")(function (err, contents) 25 | ... 26 | end) 27 | ``` 28 | 29 | # Continuable.stream 30 | 31 | This library contains the stream implementation. It's a simple queue where 32 | writes to one end come out in reads to the other end. Built-in is backpressure 33 | and controlled buffering via low-water and high-water marks for full proper 34 | flow-control. 35 | 36 | The streams have only `.read()` and `.write()` continuable style functions. 37 | They are mobile and can be moved around at will as seen in this example for 38 | creating a duplex pipe with a stream at each end. 39 | 40 | ```lua 41 | local function newPipe() 42 | -- Create two streams 43 | local a, b = newStream(), newStream() 44 | -- Cross their write functions 45 | a.write, b.write = b.write, a.write 46 | -- Return them as two duplex streams that are the two ends of the pipe 47 | return a, b 48 | end 49 | ``` 50 | 51 | # Web 52 | 53 | This library is a new web interface. Web consumes a raw http stream (usually 54 | over TCP, but any stream will do) and a web `app`. It parses HTTP requests on 55 | the stream and calls the app function. When the app function responds, it 56 | writes the corresponding HTTP data to the socket. 57 | 58 | ```lua 59 | local function app(req, res) 60 | res(200, { 61 | ["Content-Type"] = "text/plain" 62 | }, "Hello World\n") 63 | end 64 | ``` 65 | 66 | ## Web.autoheaders 67 | 68 | This middleware wraps around any web app and adds in all sorts of useful spec 69 | adherence. It does useful things like auto Content-Length header. Also it can 70 | do chunked encoding on the body stream if it's unable to calculate the length. 71 | 72 | ```lua 73 | -- Just wrap your app in autoheaders to get a new app 74 | app = autoheaders(app) 75 | ``` 76 | 77 | ## Web.log 78 | 79 | A very simple middleware to log HTTP requests. 80 | 81 | ```lua 82 | -- Just wrap your app in autoheaders to get a new app 83 | app = log(app) 84 | ``` 85 | 86 | ## Web.websocket 87 | 88 | **TODO**: Finish implementing this API 89 | 90 | A sample websocket implementation to prove that web is capable of handling HTTP 91 | upgrades. 92 | 93 | ```lua 94 | local app = function (req, res) 95 | if not req.upgrade then 96 | res(400, {}, "Websocket only\n") 97 | end 98 | local socket = websocket.upgrade(req) 99 | repeat 100 | local message, head = await(socket.read()) 101 | p({ 102 | message=message, 103 | opcode=head.opcode 104 | }) 105 | socket.write("Hello " .. message)() 106 | until not message 107 | end 108 | ``` 109 | 110 | ## Web.gzip 111 | 112 | **TODO**: Implement this 113 | 114 | A middleware to gzip body streams. This is just an example of how this would be 115 | done. I think I'll implement it using FFI calls to zlib. -------------------------------------------------------------------------------- /utils.lua: -------------------------------------------------------------------------------- 1 | 2 | local table = require('table') 3 | 4 | local utils = {} 5 | 6 | local colors = { 7 | black = "0;30", 8 | red = "0;31", 9 | green = "0;32", 10 | yellow = "0;33", 11 | blue = "0;34", 12 | magenta = "0;35", 13 | cyan = "0;36", 14 | white = "0;37", 15 | B = "1;", 16 | Bblack = "1;30", 17 | Bred = "1;31", 18 | Bgreen = "1;32", 19 | Byellow = "1;33", 20 | Bblue = "1;34", 21 | Bmagenta = "1;35", 22 | Bcyan = "1;36", 23 | Bwhite = "1;37" 24 | } 25 | 26 | if utils._useColors == nil then 27 | utils._useColors = true 28 | end 29 | 30 | function utils.color(color_name) 31 | if utils._useColors then 32 | return "\27[" .. (colors[color_name] or "0") .. "m" 33 | else 34 | return "" 35 | end 36 | end 37 | 38 | function utils.colorize(color_name, string, reset_name) 39 | return utils.color(color_name) .. tostring(string) .. utils.color(reset_name) 40 | end 41 | 42 | local backslash, null, newline, carriage, tab, quote, quote2, obracket, cbracket 43 | 44 | function utils.loadColors (n) 45 | if n ~= nil then utils._useColors = n end 46 | backslash = utils.colorize("Bgreen", "\\\\", "green") 47 | null = utils.colorize("Bgreen", "\\0", "green") 48 | newline = utils.colorize("Bgreen", "\\n", "green") 49 | carriage = utils.colorize("Bgreen", "\\r", "green") 50 | tab = utils.colorize("Bgreen", "\\t", "green") 51 | quote = utils.colorize("Bgreen", '"', "green") 52 | quote2 = utils.colorize("Bgreen", '"') 53 | obracket = utils.colorize("B", '[') 54 | cbracket = utils.colorize("B", ']') 55 | end 56 | 57 | utils.loadColors () 58 | 59 | function utils.dump(o, depth) 60 | local t = type(o) 61 | if t == 'string' then 62 | return quote .. o:gsub("\\", backslash):gsub("%z", null):gsub("\n", newline):gsub("\r", carriage):gsub("\t", tab) .. quote2 63 | end 64 | if t == 'nil' then 65 | return utils.colorize("Bblack", "nil") 66 | end 67 | if t == 'boolean' then 68 | return utils.colorize("yellow", tostring(o)) 69 | end 70 | if t == 'number' then 71 | return utils.colorize("blue", tostring(o)) 72 | end 73 | if t == 'userdata' then 74 | return utils.colorize("magenta", tostring(o)) 75 | end 76 | if t == 'thread' then 77 | return utils.colorize("Bred", tostring(o)) 78 | end 79 | if t == 'function' then 80 | return utils.colorize("cyan", tostring(o)) 81 | end 82 | if t == 'cdata' then 83 | return utils.colorize("Bmagenta", tostring(o)) 84 | end 85 | if t == 'table' then 86 | if type(depth) == 'nil' then 87 | depth = 0 88 | end 89 | if depth > 1 then 90 | return utils.colorize("yellow", tostring(o)) 91 | end 92 | local indent = (" "):rep(depth) 93 | 94 | -- Check to see if this is an array 95 | local is_array = true 96 | local i = 1 97 | for k,v in pairs(o) do 98 | if not (k == i) then 99 | is_array = false 100 | end 101 | i = i + 1 102 | end 103 | 104 | local first = true 105 | local lines = {} 106 | i = 1 107 | local estimated = 0 108 | for k,v in (is_array and ipairs or pairs)(o) do 109 | local s 110 | if is_array then 111 | s = "" 112 | else 113 | if type(k) == "string" and k:find("^[%a_][%a%d_]*$") then 114 | s = k .. ' = ' 115 | else 116 | s = '[' .. utils.dump(k, 100) .. '] = ' 117 | end 118 | end 119 | s = s .. utils.dump(v, depth + 1) 120 | lines[i] = s 121 | estimated = estimated + #s 122 | i = i + 1 123 | end 124 | if estimated > 200 then 125 | return "{\n " .. indent .. table.concat(lines, ",\n " .. indent) .. "\n" .. indent .. "}" 126 | else 127 | return "{ " .. table.concat(lines, ", ") .. " }" 128 | end 129 | end 130 | -- This doesn't happen right? 131 | return tostring(o) 132 | end 133 | 134 | -- A nice global data dumper 135 | function utils.prettyPrint(...) 136 | local n = select('#', ...) 137 | local arguments = { ... } 138 | 139 | for i = 1, n do 140 | arguments[i] = utils.dump(arguments[i]) 141 | end 142 | 143 | io.stdout:write(table.concat(arguments, "\t") .. "\n") 144 | end 145 | 146 | -- prettyprint to stderr 147 | function utils.debug(...) 148 | local n = select('#', ...) 149 | local arguments = { ... } 150 | 151 | for i = 1, n do 152 | arguments[i] = utils.dump(arguments[i]) 153 | end 154 | 155 | io.stderr:write(table.concat(arguments, "\t") .. "\n") 156 | end 157 | 158 | return utils 159 | 160 | -------------------------------------------------------------------------------- /sha1.lua: -------------------------------------------------------------------------------- 1 | ------------------------------------------------- 2 | --- *** SHA-1 algorithm for Lua *** --- 3 | ------------------------------------------------- 4 | --- Author: Martin Huesser --- 5 | --- Date: 2008-06-16 --- 6 | --- License: You may use this code in your --- 7 | --- projects as long as this header --- 8 | --- stays intact. --- 9 | ------------------------------------------------- 10 | 11 | local strlen = string.len 12 | local strchar = string.char 13 | local strbyte = string.byte 14 | local strsub = string.sub 15 | local floor = math.floor 16 | local bnot = bit.bnot 17 | local band = bit.band 18 | local bor = bit.bor 19 | local bxor = bit.bxor 20 | local shl = bit.lshift 21 | local shr = bit.rshift 22 | local h0, h1, h2, h3, h4 23 | 24 | ------------------------------------------------- 25 | 26 | local function LeftRotate(val, nr) 27 | return shl(val, nr) + shr(val, 32 - nr) 28 | end 29 | 30 | ------------------------------------------------- 31 | 32 | local function ToHex(num) 33 | local i, d 34 | local str = "" 35 | for i = 1, 8 do 36 | d = band(num, 15) 37 | if (d < 10) then 38 | str = strchar(d + 48) .. str 39 | else 40 | str = strchar(d + 87) .. str 41 | end 42 | num = floor(num / 16) 43 | end 44 | return str 45 | end 46 | 47 | local function ToBin(num) 48 | return table.concat({ 49 | strchar(band(shr(num, 24), 255)), 50 | strchar(band(shr(num, 16), 255)), 51 | strchar(band(shr(num, 8), 255)), 52 | strchar(band(num, 255)) 53 | }) 54 | end 55 | 56 | ------------------------------------------------- 57 | 58 | local function PreProcess(str) 59 | local bitlen, i 60 | local str2 = "" 61 | bitlen = strlen(str) * 8 62 | str = str .. strchar(128) 63 | i = 56 - band(strlen(str), 63) 64 | if (i < 0) then 65 | i = i + 64 66 | end 67 | for i = 1, i do 68 | str = str .. strchar(0) 69 | end 70 | for i = 1, 8 do 71 | str2 = strchar(band(bitlen, 255)) .. str2 72 | bitlen = floor(bitlen / 256) 73 | end 74 | return str .. str2 75 | end 76 | 77 | ------------------------------------------------- 78 | 79 | local function MainLoop(str) 80 | local a, b, c, d, e, f, k, t 81 | local i, j 82 | local w = {} 83 | while (str ~= "") do 84 | for i = 0, 15 do 85 | w[i] = 0 86 | for j = 1, 4 do 87 | w[i] = w[i] * 256 + strbyte(str, i * 4 + j) 88 | end 89 | end 90 | for i = 16, 79 do 91 | w[i] = LeftRotate(bxor(bxor(w[i - 3], w[i - 8]), bxor(w[i - 14], w[i - 16])), 1) 92 | end 93 | a = h0 94 | b = h1 95 | c = h2 96 | d = h3 97 | e = h4 98 | for i = 0, 79 do 99 | if (i < 20) then 100 | f = bor(band(b, c), band(bnot(b), d)) 101 | k = 1518500249 102 | elseif (i < 40) then 103 | f = bxor(bxor(b, c), d) 104 | k = 1859775393 105 | elseif (i < 60) then 106 | f = bor(bor(band(b, c), band(b, d)), band(c, d)) 107 | k = 2400959708 108 | else 109 | f = bxor(bxor(b, c), d) 110 | k = 3395469782 111 | end 112 | t = LeftRotate(a, 5) + f + e + k + w[i] 113 | e = d 114 | d = c 115 | c = LeftRotate(b, 30) 116 | b = a 117 | a = t 118 | end 119 | h0 = band(h0 + a, 4294967295) 120 | h1 = band(h1 + b, 4294967295) 121 | h2 = band(h2 + c, 4294967295) 122 | h3 = band(h3 + d, 4294967295) 123 | h4 = band(h4 + e, 4294967295) 124 | str = strsub(str, 65) 125 | end 126 | end 127 | 128 | ------------------------------------------------- 129 | 130 | local function sha1(str) 131 | str = PreProcess(str) 132 | h0 = 1732584193 133 | h1 = 4023233417 134 | h2 = 2562383102 135 | h3 = 0271733878 136 | h4 = 3285377520 137 | MainLoop(str) 138 | return table.concat({ 139 | ToHex(h0), 140 | ToHex(h1), 141 | ToHex(h2), 142 | ToHex(h3), 143 | ToHex(h4) 144 | }) 145 | end 146 | 147 | local function sha1_binary(str) 148 | str = PreProcess(str) 149 | h0 = 1732584193 150 | h1 = 4023233417 151 | h2 = 2562383102 152 | h3 = 0271733878 153 | h4 = 3285377520 154 | MainLoop(str) 155 | return table.concat({ 156 | ToBin(h0), 157 | ToBin(h1), 158 | ToBin(h2), 159 | ToBin(h3), 160 | ToBin(h4) 161 | }) 162 | end 163 | 164 | return { 165 | sha1 = sha1, 166 | sha1_binary = sha1_binary 167 | } 168 | 169 | ------------------------------------------------- 170 | ------------------------------------------------- 171 | ------------------------------------------------- -------------------------------------------------------------------------------- /autoheaders.lua: -------------------------------------------------------------------------------- 1 | local stringFormat = require('string').format 2 | local osDate = require('os').date 3 | 4 | return function (app, options) 5 | if not options then 6 | options = {} 7 | end 8 | if options.autoServer == nil then 9 | options.autoServer = "MoonSlice " .. _VERSION 10 | end 11 | if options.autoDate == nil then 12 | options.autoDate = true 13 | end 14 | if options.autoChunkedEncoding == nil then 15 | options.autoChunkedEncoding = true 16 | end 17 | if options.autoContentLength == nil then 18 | options.autoContentLength = true 19 | end 20 | return function (req, res) 21 | if req.headers.expect == "100-continue" then 22 | req.socket:write("HTTP/1.1 100 Continue\r\n\r\n")() 23 | end 24 | app(req, function (code, headers, body) 25 | local hasDate = false 26 | local hasServer = false 27 | local hasContentLength = false 28 | local hasTransferEncoding = false 29 | for name, value in pairs(headers) do 30 | if type(name) == "number" then 31 | local a, b 32 | a, b, name = value:find("([^:]*)") 33 | end 34 | name = name:lower() 35 | if name == "date" then hasDate = true end 36 | if name == "server" then hasServer = true end 37 | if name == "content-length" then hasContentLength = true end 38 | if name == "transfer-encoding" then hasTransferEncoding = true end 39 | end 40 | if not hasDate and options.autoDate then 41 | headers['Date'] = osDate("!%a, %d %b %Y %H:%M:%S GMT") 42 | end 43 | if not hasServer and options.autoServer then 44 | headers['Server'] = options.autoServer 45 | end 46 | if body and (not hasContentLength) and (not hasTransferEncoding) then 47 | local isStream = type(body) == "table" and type(body.read) == "function" 48 | if not isStream and options.autoContentLength then 49 | if type(body) == "table" then 50 | local length = 0 51 | for i, v in ipairs(body) do 52 | length = length + #v 53 | end 54 | headers["Content-Length"] = length 55 | else 56 | headers["Content-Length"] = #body 57 | end 58 | hasContentLength = true 59 | end 60 | 61 | if not hasContentLength and options.autoChunkedEncoding then 62 | headers["Transfer-Encoding"] = "chunked" 63 | hasTransferEncoding = true 64 | if not isStream then 65 | if type(body) == "table" then 66 | local length = 0 67 | for i, v in ipairs(body) do 68 | length = length + #v 69 | end 70 | table.insert(body, 1, stringFormat("%X\r\n", length)) 71 | table.insert(body, "\r\n0\r\n\r\n") 72 | else 73 | body = { 74 | stringFormat("%X\r\n", #body), 75 | body, 76 | "\r\n0\r\n\r\n" 77 | } 78 | end 79 | else 80 | local originalStream = body 81 | local done = false 82 | body = {} 83 | -- http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1 84 | function body:read() return function (callback) 85 | if done then 86 | return callback() 87 | end 88 | originalStream:read()(function (err, chunk) 89 | if err then return callback(err) end 90 | if chunk then 91 | local parts = {} 92 | if type(chunk) == "table" then 93 | local length = 0 94 | for i, v in ipairs(chunk) do 95 | length = length + #v 96 | end 97 | table.insert(parts, stringFormat("%X\r\n", length)) 98 | for i, v in ipairs(chunk) do 99 | table.insert(parts, v) 100 | end 101 | else 102 | table.insert(parts, stringFormat("%X\r\n", #chunk)) 103 | table.insert(parts, chunk) 104 | end 105 | table.insert(parts, "\r\n") 106 | return callback(nil, parts) 107 | end 108 | done = true 109 | -- This line is last-chunk, an empty trailer, and CRLF combined 110 | callback(nil, "0\r\n\r\n") 111 | end) 112 | end end 113 | end 114 | end 115 | 116 | end 117 | if req.should_keep_alive and (hasContentLength or hasTransferEncoding or code == 304) then 118 | headers["Connection"] = "keep-alive" 119 | else 120 | headers["Connection"] = "close" 121 | req.should_keep_alive = false 122 | end 123 | res(code, headers, body) 124 | end) 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /mime.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Copyright 2012 The Luvit Authors. All Rights Reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS-IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | --]] 18 | 19 | local mime = {} 20 | local table = { 21 | ["3gp"] = "video/3gpp", 22 | a = "application/octet-stream", 23 | ai = "application/postscript", 24 | aif = "audio/x-aiff", 25 | aiff = "audio/x-aiff", 26 | asc = "application/pgp-signature", 27 | asf = "video/x-ms-asf", 28 | asm = "text/x-asm", 29 | asx = "video/x-ms-asf", 30 | atom = "application/atom+xml", 31 | au = "audio/basic", 32 | avi = "video/x-msvideo", 33 | bat = "application/x-msdownload", 34 | bin = "application/octet-stream", 35 | bmp = "image/bmp", 36 | bz2 = "application/x-bzip2", 37 | c = "text/x-c", 38 | cab = "application/vnd.ms-cab-compressed", 39 | cc = "text/x-c", 40 | chm = "application/vnd.ms-htmlhelp", 41 | class = "application/octet-stream", 42 | com = "application/x-msdownload", 43 | conf = "text/plain", 44 | cpp = "text/x-c", 45 | crt = "application/x-x509-ca-cert", 46 | css = "text/css", 47 | csv = "text/csv", 48 | cxx = "text/x-c", 49 | deb = "application/x-debian-package", 50 | der = "application/x-x509-ca-cert", 51 | diff = "text/x-diff", 52 | djv = "image/vnd.djvu", 53 | djvu = "image/vnd.djvu", 54 | dll = "application/x-msdownload", 55 | dmg = "application/octet-stream", 56 | doc = "application/msword", 57 | dot = "application/msword", 58 | dtd = "application/xml-dtd", 59 | dvi = "application/x-dvi", 60 | ear = "application/java-archive", 61 | eml = "message/rfc822", 62 | eps = "application/postscript", 63 | exe = "application/x-msdownload", 64 | f = "text/x-fortran", 65 | f77 = "text/x-fortran", 66 | f90 = "text/x-fortran", 67 | flv = "video/x-flv", 68 | ["for"] = "text/x-fortran", 69 | gem = "application/octet-stream", 70 | gemspec = "text/x-script.ruby", 71 | gif = "image/gif", 72 | gz = "application/x-gzip", 73 | h = "text/x-c", 74 | hh = "text/x-c", 75 | htm = "text/html", 76 | html = "text/html", 77 | ico = "image/vnd.microsoft.icon", 78 | ics = "text/calendar", 79 | ifb = "text/calendar", 80 | iso = "application/octet-stream", 81 | jar = "application/java-archive", 82 | java = "text/x-java-source", 83 | jnlp = "application/x-java-jnlp-file", 84 | jpeg = "image/jpeg", 85 | jpg = "image/jpeg", 86 | js = "application/javascript", 87 | json = "application/json", 88 | less = "text/css", 89 | log = "text/plain", 90 | lua = "text/x-lua", 91 | luac = "application/x-lua-bytecode", 92 | m3u = "audio/x-mpegurl", 93 | m4v = "video/mp4", 94 | man = "text/troff", 95 | manifest = "text/cache-manifest", 96 | markdown = "text/markdown", 97 | mathml = "application/mathml+xml", 98 | mbox = "application/mbox", 99 | mdoc = "text/troff", 100 | md = "text/markdown", 101 | me = "text/troff", 102 | mid = "audio/midi", 103 | midi = "audio/midi", 104 | mime = "message/rfc822", 105 | mml = "application/mathml+xml", 106 | mng = "video/x-mng", 107 | mov = "video/quicktime", 108 | mp3 = "audio/mpeg", 109 | mp4 = "video/mp4", 110 | mp4v = "video/mp4", 111 | mpeg = "video/mpeg", 112 | mpg = "video/mpeg", 113 | ms = "text/troff", 114 | msi = "application/x-msdownload", 115 | odp = "application/vnd.oasis.opendocument.presentation", 116 | ods = "application/vnd.oasis.opendocument.spreadsheet", 117 | odt = "application/vnd.oasis.opendocument.text", 118 | ogg = "application/ogg", 119 | p = "text/x-pascal", 120 | pas = "text/x-pascal", 121 | pbm = "image/x-portable-bitmap", 122 | pdf = "application/pdf", 123 | pem = "application/x-x509-ca-cert", 124 | pgm = "image/x-portable-graymap", 125 | pgp = "application/pgp-encrypted", 126 | pkg = "application/octet-stream", 127 | pl = "text/x-script.perl", 128 | pm = "text/x-script.perl-module", 129 | png = "image/png", 130 | pnm = "image/x-portable-anymap", 131 | ppm = "image/x-portable-pixmap", 132 | pps = "application/vnd.ms-powerpoint", 133 | ppt = "application/vnd.ms-powerpoint", 134 | ps = "application/postscript", 135 | psd = "image/vnd.adobe.photoshop", 136 | py = "text/x-script.python", 137 | qt = "video/quicktime", 138 | ra = "audio/x-pn-realaudio", 139 | rake = "text/x-script.ruby", 140 | ram = "audio/x-pn-realaudio", 141 | rar = "application/x-rar-compressed", 142 | rb = "text/x-script.ruby", 143 | rdf = "application/rdf+xml", 144 | roff = "text/troff", 145 | rpm = "application/x-redhat-package-manager", 146 | rss = "application/rss+xml", 147 | rtf = "application/rtf", 148 | ru = "text/x-script.ruby", 149 | s = "text/x-asm", 150 | sgm = "text/sgml", 151 | sgml = "text/sgml", 152 | sh = "application/x-sh", 153 | sig = "application/pgp-signature", 154 | snd = "audio/basic", 155 | so = "application/octet-stream", 156 | svg = "image/svg+xml", 157 | svgz = "image/svg+xml", 158 | swf = "application/x-shockwave-flash", 159 | t = "text/troff", 160 | tar = "application/x-tar", 161 | tbz = "application/x-bzip-compressed-tar", 162 | tci = "application/x-topcloud", 163 | tcl = "application/x-tcl", 164 | tex = "application/x-tex", 165 | texi = "application/x-texinfo", 166 | texinfo = "application/x-texinfo", 167 | text = "text/plain", 168 | tif = "image/tiff", 169 | tiff = "image/tiff", 170 | torrent = "application/x-bittorrent", 171 | tr = "text/troff", 172 | ttf = "application/x-font-ttf", 173 | txt = "text/plain", 174 | vcf = "text/x-vcard", 175 | vcs = "text/x-vcalendar", 176 | vrml = "model/vrml", 177 | war = "application/java-archive", 178 | wav = "audio/x-wav", 179 | webm = "video/webm", 180 | wma = "audio/x-ms-wma", 181 | wmv = "video/x-ms-wmv", 182 | wmx = "video/x-ms-wmx", 183 | wrl = "model/vrml", 184 | wsdl = "application/wsdl+xml", 185 | xbm = "image/x-xbitmap", 186 | xhtml = "application/xhtml+xml", 187 | xls = "application/vnd.ms-excel", 188 | xml = "application/xml", 189 | xpm = "image/x-xpixmap", 190 | xsl = "application/xml", 191 | xslt = "application/xslt+xml", 192 | yaml = "text/yaml", 193 | yml = "text/yaml", 194 | zip = "application/zip", 195 | } 196 | mime.table = table 197 | mime.default = "application/octet-stream" 198 | 199 | function mime.getType(path) 200 | return mime.table[path:lower():match("[^.]*$")] or mime.default 201 | end 202 | 203 | return mime 204 | 205 | -------------------------------------------------------------------------------- /tests/test-autoheaders.lua: -------------------------------------------------------------------------------- 1 | local autoheaders = require('autoheaders') 2 | local newStream = require('stream').newStream 3 | local await = require('fiber').await 4 | local p = require('utils').prettyPrint 5 | local tick = require('tick').tick 6 | local flushTickQueue = require('tick').flushTickQueue 7 | local describe = require('ensure').describe 8 | local same = require('ensure').same 9 | 10 | describe("autoheaders", function () 11 | 12 | it("is a function", function (done) 13 | assert(type(autoheaders) == "function") 14 | done() 15 | end) 16 | 17 | describe("string body", function () 18 | local app = function (req, res) 19 | res(200, { 20 | ["Content-Type"] = "text/plain" 21 | }, "Hello World\n") 22 | end 23 | local request = { 24 | method = "GET", 25 | url = { path = "/" }, 26 | headers = {} 27 | } 28 | 29 | it("proxies to main app", function (done) 30 | autoheaders(app)(request, function (code, headers, body) 31 | assert(code == 200) 32 | assert(body == "Hello World\n") 33 | end) 34 | done() 35 | end) 36 | 37 | it("adds Server header", function (done) 38 | autoheaders(app, {autoServer="BustedServer"})(request, function (code, headers, body) 39 | assert(headers["Server"] == "BustedServer") 40 | end) 41 | done() 42 | end) 43 | 44 | it("adds Date header", function (done) 45 | autoheaders(app)(request, function (code, headers, body) 46 | assert(type(headers["Date"]) == "string") 47 | end) 48 | done() 49 | end) 50 | 51 | it("adds Content-Length header", function (done) 52 | autoheaders(app)(request, function (code, headers, body) 53 | assert(headers["Content-Length"] == #body) 54 | end) 55 | done() 56 | end) 57 | 58 | it("Adds Connection: close", function (done) 59 | autoheaders(app)(request, function (code, headers, body) 60 | assert(headers["Connection"]:lower() == "close") 61 | end) 62 | done() 63 | end) 64 | 65 | it("Adds Connection: keep-alive", function (done) 66 | local request = { 67 | method = "GET", 68 | url = { path = "/" }, 69 | should_keep_alive = true, 70 | headers = { 71 | Connection = "Keep-Alive" 72 | } 73 | } 74 | autoheaders(app)(request, function (code, headers, body) 75 | assert(headers["Connection"]:lower() == "keep-alive") 76 | end) 77 | done() 78 | end) 79 | 80 | it("should do chunked encoding", function (done) 81 | autoheaders(app, {autoContentLength=false})(request, function (code, headers, body) 82 | assert(headers["Transfer-Encoding"] == "chunked") 83 | assert(headers["Content-Length"] == nil) 84 | assert(same(body, { 85 | "C\r\n", 86 | "Hello World\n", 87 | "\r\n0\r\n\r\n" 88 | })) 89 | end) 90 | done() 91 | end) 92 | 93 | end) 94 | 95 | describe("array body", function () 96 | local app = function (req, res) 97 | res(200, { 98 | ["Content-Type"] = "text/plain" 99 | }, {"Hello ", "World\n"}) 100 | end 101 | local request = { 102 | method = "GET", 103 | url = { path = "/" }, 104 | headers = {} 105 | } 106 | 107 | it("should add Content-Length header", function (done) 108 | autoheaders(app)(request, function (code, headers, body) 109 | assert(headers["Content-Length"] == 12) 110 | end) 111 | done() 112 | end) 113 | 114 | it("should do chunked encoding", function (done) 115 | autoheaders(app, {autoContentLength=false})(request, function (code, headers, body) 116 | assert(headers["Transfer-Encoding"] == "chunked") 117 | assert(headers["Content-Length"] == nil) 118 | assert(same(body, { 119 | "C\r\n", 120 | "Hello ", 121 | "World\n", 122 | "\r\n0\r\n\r\n" 123 | })) 124 | end) 125 | done() 126 | end) 127 | 128 | end) 129 | 130 | describe("sync stream body", function () 131 | local stream = newStream() 132 | local app = function (req, res) 133 | res(200, { 134 | ["Content-Type"] = "text/plain" 135 | }, stream) 136 | end 137 | local request = { 138 | method = "GET", 139 | url = { path = "/" }, 140 | headers = {} 141 | } 142 | stream.write("Hello ")() 143 | stream.write({"my ", "fun "})() 144 | stream.write("World\n")() 145 | stream.write()() 146 | 147 | it("should do chunked encoding", function (done) 148 | autoheaders(app)(request, function (code, headers, body) 149 | assert(headers["Transfer-Encoding"] == "chunked") 150 | assert(headers["Content-Length"] == nil) 151 | assert(type(body) == "table") 152 | assert(type(body.read) == "function") 153 | local parts = {} 154 | repeat 155 | local chunk = await(body.read()) 156 | if chunk then 157 | table.insert(parts, chunk) 158 | end 159 | until not chunk 160 | assert(same({ 161 | { "6\r\n", "Hello ", "\r\n" }, 162 | { "7\r\n", "my ", "fun ", "\r\n" }, 163 | { "6\r\n", "World\n", "\r\n" }, 164 | "0\r\n\r\n" 165 | }, parts)) 166 | end) 167 | done() 168 | end) 169 | 170 | end) 171 | 172 | describe("async stream body", function () 173 | local stream = newStream() 174 | local app = function (req, res) 175 | res(200, { 176 | ["Content-Type"] = "text/plain" 177 | }, stream) 178 | end 179 | local request = { 180 | method = "GET", 181 | url = { path = "/" }, 182 | headers = {} 183 | } 184 | 185 | it("should do chunked encoding", function (done) 186 | autoheaders(app)(request, function (code, headers, body) 187 | assert(headers["Transfer-Encoding"] == "chunked") 188 | assert(headers["Content-Length"] == nil) 189 | assert(type(body) == "table") 190 | assert(type(body.read) == "function") 191 | local parts = {} 192 | repeat 193 | local chunk = await(body.read()) 194 | if chunk then 195 | table.insert(parts, chunk) 196 | end 197 | until not chunk 198 | assert(same({ 199 | { "6\r\n", "Hello ", "\r\n" }, 200 | { "7\r\n", "my ", "fun ", "\r\n" }, 201 | { "6\r\n", "World\n", "\r\n" }, 202 | "0\r\n\r\n" 203 | }, parts)) 204 | end) 205 | done() 206 | end) 207 | 208 | local input = { 209 | "Hello ", 210 | {"my ", "fun "}, 211 | "World\n" 212 | } 213 | local index = 1 214 | local function next() 215 | local message = input[index] 216 | index = index + 1 217 | stream.write(message)(function () 218 | if message then 219 | tick()(next) 220 | end 221 | end) 222 | end 223 | tick()(next) 224 | 225 | end) 226 | 227 | 228 | end, flushTickQueue) 229 | -------------------------------------------------------------------------------- /web.lua: -------------------------------------------------------------------------------- 1 | local newHttpParser = require('lhttp_parser').new 2 | local parseUrl = require('lhttp_parser').parseUrl 3 | local newStream = require('stream').newStream 4 | local table = require('table') 5 | local stringFormat = require('string').format 6 | 7 | local web = {} 8 | 9 | local STATUS_CODES = { 10 | [100] = 'Continue', 11 | [101] = 'Switching Protocols', 12 | [102] = 'Processing', -- RFC 2518, obsoleted by RFC 4918 13 | [200] = 'OK', 14 | [201] = 'Created', 15 | [202] = 'Accepted', 16 | [203] = 'Non-Authoritative Information', 17 | [204] = 'No Content', 18 | [205] = 'Reset Content', 19 | [206] = 'Partial Content', 20 | [207] = 'Multi-Status', -- RFC 4918 21 | [300] = 'Multiple Choices', 22 | [301] = 'Moved Permanently', 23 | [302] = 'Moved Temporarily', 24 | [303] = 'See Other', 25 | [304] = 'Not Modified', 26 | [305] = 'Use Proxy', 27 | [307] = 'Temporary Redirect', 28 | [400] = 'Bad Request', 29 | [401] = 'Unauthorized', 30 | [402] = 'Payment Required', 31 | [403] = 'Forbidden', 32 | [404] = 'Not Found', 33 | [405] = 'Method Not Allowed', 34 | [406] = 'Not Acceptable', 35 | [407] = 'Proxy Authentication Required', 36 | [408] = 'Request Time-out', 37 | [409] = 'Conflict', 38 | [410] = 'Gone', 39 | [411] = 'Length Required', 40 | [412] = 'Precondition Failed', 41 | [413] = 'Request Entity Too Large', 42 | [414] = 'Request-URI Too Large', 43 | [415] = 'Unsupported Media Type', 44 | [416] = 'Requested Range Not Satisfiable', 45 | [417] = 'Expectation Failed', 46 | [418] = 'I\'m a teapot', -- RFC 2324 47 | [422] = 'Unprocessable Entity', -- RFC 4918 48 | [423] = 'Locked', -- RFC 4918 49 | [424] = 'Failed Dependency', -- RFC 4918 50 | [425] = 'Unordered Collection', -- RFC 4918 51 | [426] = 'Upgrade Required', -- RFC 2817 52 | [500] = 'Internal Server Error', 53 | [501] = 'Not Implemented', 54 | [502] = 'Bad Gateway', 55 | [503] = 'Service Unavailable', 56 | [504] = 'Gateway Time-out', 57 | [505] = 'HTTP Version not supported', 58 | [506] = 'Variant Also Negotiates', -- RFC 2295 59 | [507] = 'Insufficient Storage', -- RFC 4918 60 | [509] = 'Bandwidth Limit Exceeded', 61 | [510] = 'Not Extended' -- RFC 2774 62 | } 63 | 64 | 65 | function web.socketHandler(app) return function (client) 66 | 67 | local currentField, headers, url, request, done 68 | local parser, bodyStream 69 | parser = newHttpParser("request", { 70 | onMessageBegin = function () 71 | headers = {} 72 | end, 73 | onUrl = function (value) 74 | url = parseUrl(value) 75 | end, 76 | onHeaderField = function (field) 77 | currentField = field 78 | end, 79 | onHeaderValue = function (value) 80 | headers[currentField:lower()] = value 81 | end, 82 | onHeadersComplete = function (info) 83 | request = info 84 | bodyStream = newStream() 85 | request.body = { 86 | read = bodyStream.read, 87 | unshift = bodyStream.unshift 88 | } 89 | request.url = url 90 | request.headers = headers 91 | request.parser = parser 92 | request.socket = client 93 | app(request, function (statusCode, headers, body) 94 | local reasonPhrase = STATUS_CODES[statusCode] or 'unknown' 95 | if not reasonPhrase then error("Invalid response code " .. tostring(statusCode)) end 96 | 97 | local head = { 98 | stringFormat("HTTP/1.1 %s %s\r\n", statusCode, reasonPhrase) 99 | } 100 | for key, value in pairs(headers) do 101 | if type(key) == "number" then 102 | table.insert(head, value) 103 | table.insert(head, "\r\n") 104 | else 105 | table.insert(head, stringFormat("%s: %s\r\n", key, value)) 106 | end 107 | end 108 | table.insert(head, "\r\n") 109 | local isStream = type(body) == "table" and type(body.read) == "function" 110 | if not isStream then 111 | if type(body) == "table" then 112 | for i, v in ipairs(body) do 113 | table.insert(head, body[i]) 114 | end 115 | else 116 | table.insert(head, body) 117 | end 118 | end 119 | client.write(head)() 120 | if not isStream then 121 | done(info.should_keep_alive) 122 | else 123 | 124 | local function abort(err) 125 | client.write(tostring(err))(function () 126 | done(false) 127 | end) 128 | end 129 | -- Assume it's a readable stream and pipe it to the client 130 | local function consume() 131 | local isAsync 132 | -- pump with trampoline in case of sync streams 133 | repeat 134 | isAsync = nil 135 | body.read()(function (err, chunk) 136 | if err then return abort(err) end 137 | if chunk then 138 | client.write(chunk)() 139 | else 140 | return done(info.should_keep_alive) 141 | end 142 | if isAsync == true then 143 | -- It was async, so we need to start a new repeat loop 144 | consume() 145 | elseif isAsync == nil then 146 | -- It was sync, mark as sure 147 | isAsync = false 148 | end 149 | end) 150 | -- read returned before calling the callback, it's async. 151 | if isAsync == nil then 152 | isAsync = true 153 | end 154 | until isAsync == true 155 | end 156 | consume() 157 | 158 | end 159 | end) 160 | end, 161 | onBody = function (chunk) 162 | bodyStream.write(chunk)() 163 | end, 164 | onMessageComplete = function () 165 | bodyStream.write()() 166 | end 167 | }) 168 | 169 | done = function(keepAlive) 170 | if keepAlive then 171 | parser:reinitialize("request") 172 | else 173 | client.write() 174 | end 175 | end 176 | 177 | -- Consume the tcp stream and send it to the HTTP parser 178 | local function onRead(err, chunk) 179 | if (err) then error(err) end 180 | if chunk then 181 | if #chunk > 0 then 182 | local nparsed = parser:execute(chunk, 0, #chunk) 183 | if request and request.upgrade then 184 | -- Put the extra data back on the socket if there is any 185 | if nparsed < #chunk then 186 | client.unshift(chunk:sub(nparsed + 1)) 187 | end 188 | -- Stop pumping to html parser 189 | return 190 | end 191 | if nparsed < #chunk then 192 | -- Parse error, close the connection 193 | return client.write()() 194 | end 195 | end 196 | return client.read()(onRead) 197 | end 198 | parser:finish() 199 | end 200 | client.read()(onRead) 201 | 202 | end end 203 | 204 | return web 205 | -------------------------------------------------------------------------------- /websocket.lua: -------------------------------------------------------------------------------- 1 | local ffi = require('ffi') 2 | local bit = require('bit') 3 | local sha1_binary = require("sha1").sha1_binary 4 | local newPipe = require('stream').newPipe 5 | local p = require('utils').prettyPrint 6 | local base64Encode = require("base64").encode 7 | 8 | ffi.cdef([[ 9 | typedef struct { 10 | int8_t fin, rsv1, rsv2, rsv3, opcode, mask; 11 | uint64_t length, offset; 12 | } websocket_frame; 13 | ]]) 14 | 15 | local function frame(message, head) 16 | local key 17 | local len = #message 18 | local size = len + 2 19 | if head.mask then 20 | key = ffi.new("unsigned char[?]", 4) 21 | key[0] = math.random(0,255) 22 | key[1] = math.random(0,255) 23 | key[2] = math.random(0,255) 24 | key[3] = math.random(0,255) 25 | size = size + 4 26 | end 27 | if len >= 65536 then 28 | head.length = 127 29 | size = size + 8 30 | elseif len >= 126 then 31 | head.length = 126 32 | size = size + 2 33 | else 34 | head.length = len 35 | end 36 | 37 | local payload = ffi.new("unsigned char[?]", size) 38 | payload[0] = (head.fin and 128 or 0) 39 | + (head.rsv1 and 64 or 0) 40 | + (head.rsv2 and 32 or 0) 41 | + (head.rsv3 and 16 or 0) 42 | + (head.opcode or 0) 43 | payload[1] = (head.mask and 128 or 0) 44 | + (head.length) 45 | local offset 46 | 47 | if head.length == 127 then 48 | payload[2] = bit.band(bit.rshift(len, 56), 0xff) 49 | payload[3] = bit.band(bit.rshift(len, 48), 0xff) 50 | payload[4] = bit.band(bit.rshift(len, 40), 0xff) 51 | payload[5] = bit.band(bit.rshift(len, 32), 0xff) 52 | payload[6] = bit.band(bit.rshift(len, 24), 0xff) 53 | payload[7] = bit.band(bit.rshift(len, 16), 0xff) 54 | payload[8] = bit.band(bit.rshift(len, 8), 0xff) 55 | payload[9] = bit.band(len, 0xff) 56 | offset = 10 57 | elseif head.length == 126 then 58 | payload[2] = bit.band(bit.rshift(len, 8), 0xff) 59 | payload[3] = bit.band(len, 0xff) 60 | offset = 4 61 | else 62 | offset = 2 63 | end 64 | 65 | if key then 66 | payload[offset] = key[0] 67 | payload[offset + 1] = key[1] 68 | payload[offset + 2] = key[2] 69 | payload[offset + 3] = key[3] 70 | offset = offset + 4 71 | end 72 | 73 | for i = 1, len do 74 | local byte = message:byte(i) 75 | if key then 76 | payload[offset] = bit.bxor(byte, key[(i-1)%4]) 77 | else 78 | payload[offset] = byte 79 | end 80 | offset = offset + 1 81 | end 82 | return ffi.string(payload, offset) 83 | end 84 | 85 | -- Simple state machine to deframe websocket 13 traffic 86 | local function deframer(onMessage) 87 | local state = 0 88 | local head, payload 89 | 90 | local function startKey() 91 | key = ffi.new("unsigned char[?]", 4) 92 | state = 12 93 | end 94 | 95 | local function emit(message) 96 | onMessage(message, head) 97 | head = nil 98 | key = nil 99 | payload = nil 100 | state = 0 101 | end 102 | 103 | local function startBody() 104 | if head.length == 0 then 105 | return emit("") 106 | end 107 | payload = ffi.new("unsigned char[?]", head.length) 108 | head.offset = 0 109 | state = 16 110 | end 111 | local states = { 112 | [0] = function (byte) -- HEADER BYTE 1 113 | head = ffi.new("websocket_frame") 114 | head.fin = bit.rshift(bit.band(byte, 128), 7) 115 | head.rsv1 = bit.rshift(bit.band(byte, 64), 6) 116 | head.rsv2 = bit.rshift(bit.band(byte, 32), 5) 117 | head.rsv3 = bit.rshift(bit.band(byte, 16), 4) 118 | head.opcode = bit.band(byte, 15) 119 | 120 | state = 1 121 | end, 122 | [1] = function (byte) -- HEADER BYTE 2 123 | head.mask = bit.rshift(bit.band(byte, 128), 7) 124 | length = bit.band(byte, 127) 125 | if length == 126 then 126 | state = 2 127 | elseif length == 127 then 128 | state = 5 129 | else 130 | head.length = length 131 | if head.mask then 132 | startKey() 133 | else 134 | startBody() 135 | end 136 | end 137 | end, 138 | [2] = function (byte) -- length16-1 139 | head.length = bit.lshift(byte, 8) 140 | state = 3 141 | end, 142 | [3] = function (byte) -- length16-2 143 | head.length = head.length + byte 144 | if head.mask then 145 | startKey() 146 | else 147 | startBody() 148 | end 149 | end, 150 | [4] = function (byte) -- length64-1 151 | head.length = bit.lshift(byte, 56) 152 | state = 5 153 | end, 154 | [5] = function (byte) -- length64-2 155 | head.length = head.length + bit.lshift(byte, 48) 156 | state = 6 157 | end, 158 | [6] = function (byte) -- length64-3 159 | head.length = head.length + bit.lshift(byte, 40) 160 | state = 7 161 | end, 162 | [7] = function (byte) -- length64-4 163 | head.length = head.length + bit.lshift(byte, 32) 164 | state = 8 165 | end, 166 | [8] = function (byte) -- length64-5 167 | head.length = head.length + bit.lshift(byte, 24) 168 | state = 9 169 | end, 170 | [9] = function (byte) -- length64-6 171 | head.length = head.length + bit.lshift(byte, 16) 172 | state = 10 173 | end, 174 | [10] = function (byte) -- length64-7 175 | head.length = head.length + bit.lshift(byte, 8) 176 | state = 11 177 | end, 178 | [11] = function (byte) -- length64-8 179 | head.length = head.length + byte 180 | if head.mask then 181 | startKey() 182 | else 183 | startBody() 184 | end 185 | end, 186 | [12] = function (byte) -- masking-key-1 187 | key[0] = byte 188 | state = 13 189 | end, 190 | [13] = function (byte) -- masking-key-2 191 | key[1] = byte 192 | state = 14 193 | end, 194 | [14] = function (byte) -- masking-key-3 195 | key[2] = byte 196 | state = 15 197 | end, 198 | [15] = function (byte) -- masking-key-4 199 | key[3] = byte 200 | startBody() 201 | end, 202 | [16] = function (byte) -- payload data 203 | if head.offset >= head.length then 204 | error("OOB error") 205 | end 206 | if key then 207 | payload[head.offset] = bit.bxor(byte, key[head.offset % 4]) 208 | else 209 | payload[head.offset] = byte 210 | end 211 | head.offset = head.offset + 1 212 | if head.offset == head.length then 213 | emit(ffi.string(payload, head.length)) 214 | end 215 | end 216 | } 217 | 218 | return function (chunk) 219 | for i = 1, #chunk do 220 | states[state](chunk:byte(i)) 221 | end 222 | end 223 | end 224 | 225 | local function getToken(key) 226 | return base64Encode(sha1_binary(key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) 227 | end 228 | 229 | local function upgrade(req) 230 | local key = req.headers["sec-websocket-key"] 231 | local token = getToken(key) 232 | local socket = req.socket 233 | socket.write({ 234 | "HTTP/1.1 101 Switching Protocols\r\n", 235 | "Upgrade: websocket\r\n", 236 | "Connection: Upgrade\r\n", 237 | "Sec-WebSocket-Accept: ", token, "\r\n", 238 | "\r\n" 239 | })() 240 | local internal, external = newPipe() 241 | local parser = deframer(function (message, head) 242 | if head.opcode == 0x8 then 243 | internal.write()() 244 | else 245 | internal.write(message)() 246 | end 247 | end) 248 | local function onRead(err, chunk) 249 | if err then error(err) end 250 | if chunk then 251 | parser(chunk) 252 | socket.read()(onRead) 253 | else 254 | internal.write()() 255 | end 256 | end 257 | socket.read()(onRead) 258 | 259 | local function read() 260 | internal.read()(function (err, message) 261 | if err then error(err) end 262 | if message then 263 | socket.write(frame(message, { 264 | fin = true, 265 | opcode = 1 266 | }))() 267 | read() 268 | end 269 | end) 270 | end 271 | read() 272 | 273 | return external 274 | 275 | end 276 | 277 | return { 278 | upgrade = upgrade, 279 | deframer = deframer, 280 | frame = frame, 281 | getToken = getToken 282 | } 283 | --------------------------------------------------------------------------------