Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e4f3818
commit e94548f
Showing
10 changed files
with
536 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
local iStream = require('core').iStream | ||
local stringFormat = require('string').format | ||
local osDate = require('os').date | ||
|
||
return function (app) | ||
return function (req, res) | ||
app(req, function (code, headers, body) | ||
local hasDate = false | ||
local hasServer = false | ||
local hasContentLength = false | ||
local hasTransferEncoding = false | ||
for name in pairs(headers) do | ||
name = name:lower() | ||
if name == "date" then hasDate = true end | ||
if name == "server" then hasServer = true end | ||
if name == "content-length" then hasContentLength = true end | ||
if name == "transfer-encoding" then hasTransferEncoding = true end | ||
end | ||
if not hasDate then | ||
headers['Date'] = osDate("!%a, %d %b %Y %H:%M:%S GMT") | ||
end | ||
if not hasServer then | ||
headers['Server'] = "Luvit " .. process.version | ||
end | ||
if not hasContentLength or hasTransfarEncoding then | ||
if type(body) == "string" then | ||
headers["Content-Length"] = #body | ||
hasContentLength = true | ||
elseif type(body) == "table" then | ||
headers["Transfer-Encoding"] = "chunked" | ||
hasTransfarEncoding = true | ||
local originalStream = body | ||
body = iStream:new() | ||
originalStream:on("data", function (chunk) | ||
body:emit("data", stringFormat("%X\r\n%s\r\n", #chunk, chunk)) | ||
end) | ||
originalStream:on("end", function () | ||
body:emit("data", stringFormat("0\r\n\r\n\r\n")) | ||
body:emit("end") | ||
end) | ||
end | ||
end | ||
if req.should_keep_alive and hasContentLength then | ||
headers["Connection"] = "keep-alive" | ||
else | ||
headers["Connection"] = "close" | ||
req.should_keep_alive = false | ||
end | ||
res(code, headers, body) | ||
end) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
return function (app) | ||
return function (req, res) | ||
app(req, function (code, headers, body) | ||
print(req.method .. ' ' .. req.url.path .. ' ' .. code) | ||
res(code, headers, body) | ||
end) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
|
||
local fs = require 'fs' | ||
local pathJoin = require('path').join | ||
local urlParse = require('url').parse | ||
local getType = require('mime').getType | ||
local osDate = require('os').date | ||
local iStream = require('core').iStream | ||
|
||
local floor = require('math').floor | ||
local table = require 'table' | ||
|
||
-- For encoding numbers using bases up to 64 | ||
local digits = { | ||
"0", "1", "2", "3", "4", "5", "6", "7", | ||
"8", "9", "A", "B", "C", "D", "E", "F", | ||
"G", "H", "I", "J", "K", "L", "M", "N", | ||
"O", "P", "Q", "R", "S", "T", "U", "V", | ||
"W", "X", "Y", "Z", "a", "b", "c", "d", | ||
"e", "f", "g", "h", "i", "j", "k", "l", | ||
"m", "n", "o", "p", "q", "r", "s", "t", | ||
"u", "v", "w", "x", "y", "z", "_", "$" | ||
} | ||
local function numToBase(num, base) | ||
local parts = {} | ||
repeat | ||
table.insert(parts, digits[(num % base) + 1]) | ||
num = floor(num / base) | ||
until num == 0 | ||
return table.concat(parts) | ||
end | ||
|
||
local function calcEtag(stat) | ||
return (not stat.is_file and 'W/' or '') .. | ||
'"' .. numToBase(stat.ino or 0, 64) .. | ||
'-' .. numToBase(stat.size, 64) .. | ||
'-' .. numToBase(stat.mtime, 64) .. '"' | ||
end | ||
|
||
local function createDirStream(path, options) | ||
local stream = iStream:new() | ||
fs.readdir(path, function (err, files) | ||
if err then | ||
stream:emit("error", err) | ||
end | ||
local html = { | ||
'<!doctype html>', | ||
'<html>', | ||
'<head>', | ||
'<title>' .. path .. '</title>', | ||
'</head>', | ||
'<body>', | ||
'<h1>' .. path .. '</h1>', | ||
'<ul><li><a href="../">..</a></li>' | ||
} | ||
for i, file in ipairs(files) do | ||
html[#html + 1] = | ||
'<li><a href="' .. file .. '">' .. file .. '</a></li>' | ||
end | ||
html[#html + 1] = '</ul></body></html>\n' | ||
html = table.concat(html) | ||
stream:emit("data", html) | ||
stream:emit("end") | ||
end) | ||
return stream | ||
end | ||
|
||
|
||
return function (app, options) | ||
if not options.root then error("options.root is required") end | ||
local root = options.root | ||
|
||
return function (req, res) | ||
-- Ignore non-GET/HEAD requests | ||
if not (req.method == "HEAD" or req.method == "GET") then | ||
return app(req, res) | ||
end | ||
|
||
local function serve(path, fallback) | ||
fs.open(path, "r", function (err, fd) | ||
if err then | ||
if err.code == 'ENOENT' or err.code == 'ENOTDIR' then | ||
if fallback then return serve(fallback) end | ||
if err.code == 'ENOTDIR' and path:sub(#path) == '/' then | ||
return res(302, { | ||
["Location"] = req.url.path:sub(1, #req.url.path - 1) | ||
}) | ||
end | ||
return app(req, res) | ||
end | ||
return res(500, {}, tostring(err) .. "\n" .. require('debug').traceback() .. "\n") | ||
end | ||
|
||
fs.fstat(fd, function (err, stat) | ||
if err then | ||
-- This shouldn't happen often, forward it just in case. | ||
fs.close(fd) | ||
return res(500, {}, tostring(err) .. "\n" .. require('debug').traceback() .. "\n") | ||
end | ||
|
||
local etag = calcEtag(stat) | ||
local code = 200 | ||
local headers = { | ||
['Last-Modified'] = osDate("!%a, %d %b %Y %H:%M:%S GMT", stat.mtime), | ||
['ETag'] = etag | ||
} | ||
local stream | ||
|
||
if etag == req.headers['if-none-match'] then | ||
code = 304 | ||
end | ||
|
||
if path:sub(#path) == '/' then | ||
-- We're done with the fd, createDirStream opens it again by path. | ||
fs.close(fd) | ||
|
||
if not options.autoIndex then | ||
-- Ignore directory requests if we don't have autoIndex on | ||
return app(req, res) | ||
end | ||
|
||
if not stat.is_directory then | ||
-- Can't autoIndex non-directories | ||
return res(302, { | ||
["Location"] = req.url.path:sub(1, #req.url.path - 1) | ||
}) | ||
end | ||
|
||
headers["Content-Type"] = "text/html" | ||
-- Create the index stream | ||
if not (req.method == "HEAD" or code == 304) then | ||
stream = createDirStream(path, options.autoIndex) | ||
end | ||
else | ||
if stat.is_directory then | ||
-- Can't serve directories as files | ||
fs.close(fd) | ||
return res(302, { | ||
["Location"] = req.url.path .. "/" | ||
}) | ||
end | ||
|
||
headers["Content-Type"] = getType(path) | ||
headers["Content-Length"] = stat.size | ||
|
||
if req.method ~= "HEAD" then | ||
stream = fs.createReadStream(nil, {fd=fd}) | ||
else | ||
fs.close(fd) | ||
end | ||
end | ||
res(code, headers, stream) | ||
end) | ||
end) | ||
end | ||
|
||
local path = pathJoin(options.root, req.url.path) | ||
|
||
if options.index and path:sub(#path) == '/' then | ||
serve(pathJoin(path, options.index), path) | ||
else | ||
serve(path) | ||
end | ||
|
||
end | ||
end | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
local table = require('table') | ||
local Tcp = require('uv').Tcp | ||
local iStream = require('core').iStream | ||
local newHttpParser = require('http_parser').new | ||
local parseUrl = require('http_parser').parseUrl | ||
|
||
local web = {} | ||
|
||
local STATUS_CODES = { | ||
[100] = 'Continue', | ||
[101] = 'Switching Protocols', | ||
[102] = 'Processing', -- RFC 2518, obsoleted by RFC 4918 | ||
[200] = 'OK', | ||
[201] = 'Created', | ||
[202] = 'Accepted', | ||
[203] = 'Non-Authoritative Information', | ||
[204] = 'No Content', | ||
[205] = 'Reset Content', | ||
[206] = 'Partial Content', | ||
[207] = 'Multi-Status', -- RFC 4918 | ||
[300] = 'Multiple Choices', | ||
[301] = 'Moved Permanently', | ||
[302] = 'Moved Temporarily', | ||
[303] = 'See Other', | ||
[304] = 'Not Modified', | ||
[305] = 'Use Proxy', | ||
[307] = 'Temporary Redirect', | ||
[400] = 'Bad Request', | ||
[401] = 'Unauthorized', | ||
[402] = 'Payment Required', | ||
[403] = 'Forbidden', | ||
[404] = 'Not Found', | ||
[405] = 'Method Not Allowed', | ||
[406] = 'Not Acceptable', | ||
[407] = 'Proxy Authentication Required', | ||
[408] = 'Request Time-out', | ||
[409] = 'Conflict', | ||
[410] = 'Gone', | ||
[411] = 'Length Required', | ||
[412] = 'Precondition Failed', | ||
[413] = 'Request Entity Too Large', | ||
[414] = 'Request-URI Too Large', | ||
[415] = 'Unsupported Media Type', | ||
[416] = 'Requested Range Not Satisfiable', | ||
[417] = 'Expectation Failed', | ||
[418] = 'I\'m a teapot', -- RFC 2324 | ||
[422] = 'Unprocessable Entity', -- RFC 4918 | ||
[423] = 'Locked', -- RFC 4918 | ||
[424] = 'Failed Dependency', -- RFC 4918 | ||
[425] = 'Unordered Collection', -- RFC 4918 | ||
[426] = 'Upgrade Required', -- RFC 2817 | ||
[500] = 'Internal Server Error', | ||
[501] = 'Not Implemented', | ||
[502] = 'Bad Gateway', | ||
[503] = 'Service Unavailable', | ||
[504] = 'Gateway Time-out', | ||
[505] = 'HTTP Version not supported', | ||
[506] = 'Variant Also Negotiates', -- RFC 2295 | ||
[507] = 'Insufficient Storage', -- RFC 4918 | ||
[509] = 'Bandwidth Limit Exceeded', | ||
[510] = 'Not Extended' -- RFC 2774 | ||
} | ||
|
||
|
||
function web.createServer(host, port, onRequest) | ||
if not port then error("port is a required parameter") end | ||
local server = Tcp:new() | ||
server:bind(host or "0.0.0.0", port) | ||
server:listen(function () | ||
local client = Tcp:new() | ||
local done | ||
server:accept(client) | ||
client:readStart() | ||
local currentField, headers, url, request | ||
local parser = newHttpParser("request", { | ||
onMessageBegin = function () | ||
headers = {} | ||
end, | ||
onUrl = function (value) | ||
url = parseUrl(value) | ||
end, | ||
onHeaderField = function (field) | ||
currentField = field | ||
end, | ||
onHeaderValue = function (value) | ||
headers[currentField:lower()] = value | ||
end, | ||
onHeadersComplete = function (info) | ||
request = setmetatable(info, iStream.meta) | ||
request.url = url | ||
request.headers = headers | ||
request.parser = parser | ||
onRequest(request, function (statusCode, headers, body) | ||
local reasonPhrase = STATUS_CODES[statusCode] or 'unknown' | ||
if not reasonPhrase then error("Invalid response code " .. tostring(statusCode)) end | ||
|
||
local head = {"HTTP/1.1 " .. tostring(statusCode) .. " " .. reasonPhrase .. "\r\n"} | ||
for key, value in pairs(headers) do | ||
table.insert(head, key .. ": " .. value .. "\r\n") | ||
end | ||
table.insert(head, "\r\n") | ||
if type(body) == "string" then | ||
table.insert(head, body) | ||
end | ||
client:write(table.concat(head)) | ||
if type(body) ~= "table" then | ||
done(info.should_keep_alive) | ||
else | ||
body:on("data", function (chunk) | ||
client:write(chunk) | ||
end) | ||
body:on("end", function () | ||
done(info.should_keep_alive) | ||
end) | ||
end | ||
end) | ||
end, | ||
onBody = function (chunk) | ||
request:emit("data", chunk) | ||
end, | ||
onMessageComplete = function () | ||
request:emit("end") | ||
end | ||
}) | ||
client:on('data', function (chunk) | ||
if #chunk == 0 then return end | ||
local nparsed = parser:execute(chunk, 0, #chunk) | ||
-- TODO: handle various cases here | ||
end) | ||
client:on('end', function () | ||
parser:finish() | ||
end) | ||
|
||
done = function(keepAlive) | ||
if keepAlive then | ||
parser:reinitialize("request") | ||
else | ||
client:shutdown(function () | ||
client:close() | ||
end) | ||
end | ||
end | ||
|
||
|
||
end) | ||
return server | ||
end | ||
|
||
return web |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
var defs = {}; | ||
var modules = {}; | ||
function define(name, fn) { | ||
defs[name] = fn; | ||
} | ||
function require(name) { | ||
if (modules.hasOwnProperty(name)) return modules[name]; | ||
if (defs.hasOwnProperty(name)) { | ||
var fn = defs[name]; | ||
defs[name] = function () { throw new Error("Circular Dependency"); }; | ||
return modules[name] = fn(); | ||
} | ||
throw new Error("Module not found: " + name); | ||
} |
Oops, something went wrong.