├── 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 |
--------------------------------------------------------------------------------