Skip to content
Permalink
Browse files

Copy web demo into examples folder

  • Loading branch information...
creationix committed Aug 29, 2012
1 parent e4f3818 commit e94548f36fb9fbf788d704043ca46a4b7710f5b1
@@ -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
@@ -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
@@ -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

@@ -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
@@ -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.

0 comments on commit e94548f

Please sign in to comment.
You can’t perform that action at this time.