-- -- This file is part of LEM, a Lua Event Machine. -- Copyright 2011-2013 Emil Renner Berthing -- Copyright 2013 Halfdan Mouritzen -- -- LEM is free software: you can redistribute it and/or modify it -- under the terms of the GNU Lesser General Public License as -- published by the Free Software Foundation, either version 3 of -- the License, or (at your option) any later version. -- -- LEM is distributed in the hope that it will be useful, but -- WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU Lesser General Public License for more details. -- -- You should have received a copy of the GNU Lesser General Public -- License along with LEM. If not, see . -- local setmetatable = setmetatable local tostring = tostring local tonumber = tonumber local pairs = pairs local type = type local date = os.date local format = string.format local concat = table.concat local remove = table.remove local io = require 'lem.io' require 'lem.http' local M = {} local status_string = { -- 1xx Informational [100] = '100 Continue', [101] = '101 Switching Protocols', [102] = '102 Processing', -- WebDAV -- 2xx Success [200] = '200 OK', [201] = '201 Created', [202] = '202 Accepted', [203] = '203 Non-Authoritative Information', [204] = '204 No Content', [205] = '205 Reset Content', [206] = '206 Partial Content', [207] = '207 Multi-Status', -- WebDAV [208] = '208 Already Reported', -- WebDAV [226] = '226 IM Used', [230] = '230 Authentication Successfull', -- 3xx Redirection [300] = '300 Multiple Choices', [301] = '301 Moved Permanently', [302] = '302 Found', [303] = '303 See Other', [304] = '304 Not Modified', [305] = '305 Use Proxy', [306] = '306 Switch Proxy', [307] = '307 Temporary Redirect', [308] = '308 Permanent Redirect', -- 4xx Client Error [400] = '400 Bad Request', [401] = '401 Unauthorized', [402] = '402 Payment Required', [403] = '403 Forbidden', [404] = '404 Not Found', [405] = '405 Method Not Allowed', [406] = '406 Not Acceptable', [407] = '407 Proxy Authentication Required', [408] = '408 Request Timeout', [409] = '409 Conflict', [410] = '410 Gone', [411] = '411 Length Required', [412] = '412 Precondition Failed', [413] = '413 Request Entity Too Large', [414] = '414 Request-URI Too Long', [415] = '415 Unsupported Media Type', [416] = '416 Requested Range Not Satisfiable', [417] = '417 Expectation Failed', -- ... [422] = '422 Unprocessable Entity', -- WebDAV [423] = '423 Locked', -- WebDAV [424] = '424 Failed Dependency', -- WebDAV -- ... [426] = '426 Upgrade Required', [428] = '428 Precondition Required', [429] = '429 Too Many Requests', [431] = '431 Request Header Fields Too Large', -- 5xx Server Error [500] = '500 Internal Server Error', [501] = '501 Not Implemented', [502] = '502 Bad Gateway', [503] = '503 Service Unavailable', [504] = '504 Gateway Timeout', [505] = '505 HTTP Version Not Supported', [506] = '506 Variant Also Negotiates', [507] = '507 Insufficient Storage', -- WebDAV [508] = '508 Loop Detected', -- WebDAV -- ... [510] = '510 Not Extended', [511] = '511 Network Authentication Required', [531] = '531 Access Denied', } M.status_string = status_string function M.not_found(req, res) if req.headers['expect'] ~= '100-continue' then req:body() end res.status = 404 res.headers['Content-Type'] = 'text/html; charset=UTF-8' res:add([[ Not Found

Not found

]]) end function M.htmlerror(num, text) local str = format([[ %s

%s

]], text, text) return function(req, res) res.status = num res.headers['Content-Type'] = 'text/html; charset=UTF-8' res.headers['Connection'] = 'close' res:add(str) end end M.method_not_allowed = M.htmlerror(405, 'Method Not Allowed') M.expectation_failed = M.htmlerror(417, 'Expectation Failed') M.version_not_supported = M.htmlerror(505, 'HTTP Version Not Supported') M.bad_request = M.htmlerror(400, 'Bad Request') function M.debug() end do local gsub, char, tonumber = string.gsub, string.char, tonumber local function tochar(str) return char(tonumber(str, 16)) end function M.urldecode(str) return gsub(gsub(str, '+', ' '), '%%(%x%x)', tochar) end end do local Response = {} Response.__index = Response M.Response = Response function new_response(req) local n = 0 return setmetatable({ headers = {}, version = req.version, add = function(self, ...) n = n + 1 self[n] = format(...) end }, Response) end local function check_match(entry, req, res, ok, ...) if not ok then return false end local handler = entry[req.method] if handler then handler(req, res, ok, ...) else M.method_not_allowed(req, res) end return true end local urldecode = M.urldecode local function handleHTTP(self, client) repeat local req, err = client:read('HTTPRequest') if not req then self.debug('read', err) break end local method, uri, version = req.method, req.uri, req.version req.path = urldecode(uri:match('^([^?]*)')) local res = new_response(req) if version ~= '1.0' and version ~= '1.1' then M.version_not_supported(req, res) version = '1.1' else local expect = req.headers['expect'] if expect and expect ~= '100-continue' then M.expectation_failed(req, res) else self.handler(req, res) end end local headers = res.headers local file, close = res.file, false if type(file) == 'string' then file, err = io.open(file) if file then close = true else self.debug('open', err) res = new_response(req) headers = res.headers M.not_found(req, res) end end if not res.status then if #res == 0 and file == nil then res.status = 204 else res.status = 200 end end if headers['Content-Length'] == nil and res.status ~= 204 then local len if file then len = file:size() else len = 0 for i = 1, #res do len = len + #res[i] end end headers['Content-Length'] = len end if headers['Date'] == nil then headers['Date'] = date('!%a, %d %b %Y %T GMT') end if headers['Server'] == nil then headers['Server'] = 'Hathaway/0.1 LEM/0.3' end if req.headers['connection'] == 'close' and headers['Connection'] == nil then headers['Connection'] = 'close' end local robe, i = {}, 1 do local status = res.status if type(status) == 'number' then status = status_string[status] end robe[1] = format('HTTP/%s %s\r\n', version, status) end for k, v in pairs(headers) do i = i + 1 robe[i] = format('%s: %s\r\n', k, tostring(v)) end i = i + 1 robe[i] = '\r\n' client:cork() local ok, err = client:write(concat(robe)) if not ok then self.debug('write', err) break end if method ~= 'HEAD' then if file then ok, err = client:sendfile(file, headers['Content-Length']) if close then file:close() end else local body = concat(res) if #body > 0 then ok, err = client:write(body) end end if not ok then self.debug('write', err) break end end client:uncork() until version == '1.0' or headers['Connection'] == 'close' client:close() end local Server = {} Server.__index = Server M.Server = Server function Server:run(handler) return self.socket:autospawn(function(...) return handleHTTP(self, ...) end) end local type, setmetatable = type, setmetatable function M.new(host, port, handler) local socket, err if type(host) == 'string' then socket, err = io.tcp.listen(host, port) if not socket then return nil, err end else socket = host handler = port end return setmetatable({ socket = socket, handler = handler, debug = M.debug }, Server) end end return M -- vim: ts=2 sw=2 noet: