diff options
author | Steven Barth <steven@midlink.org> | 2009-05-23 17:21:36 +0000 |
---|---|---|
committer | Steven Barth <steven@midlink.org> | 2009-05-23 17:21:36 +0000 |
commit | 8c4f847ea5b95aaf0e716beaf736b4e2b67655ae (patch) | |
tree | 5b931064a2df45c3903b66b425f49b621e9c31df /libs/lucid-http | |
parent | 0ad58e38b42dca2f46bc492b7f889b5031a9c6a1 (diff) |
GSoC Commit #1: LuCId + HTTP-Server
Diffstat (limited to 'libs/lucid-http')
-rw-r--r-- | libs/lucid-http/Makefile | 2 | ||||
-rw-r--r-- | libs/lucid-http/luasrc/lucid/http.lua | 33 | ||||
-rw-r--r-- | libs/lucid-http/luasrc/lucid/http/DirectoryPublisher.lua | 47 | ||||
-rw-r--r-- | libs/lucid-http/luasrc/lucid/http/LuciWebPublisher.lua | 62 | ||||
-rw-r--r-- | libs/lucid-http/luasrc/lucid/http/Redirector.lua | 31 | ||||
-rw-r--r-- | libs/lucid-http/luasrc/lucid/http/handler/catchall.lua | 53 | ||||
-rw-r--r-- | libs/lucid-http/luasrc/lucid/http/handler/file.lua | 250 | ||||
-rw-r--r-- | libs/lucid-http/luasrc/lucid/http/handler/luci.lua | 96 | ||||
-rw-r--r-- | libs/lucid-http/luasrc/lucid/http/server.lua | 522 |
9 files changed, 1096 insertions, 0 deletions
diff --git a/libs/lucid-http/Makefile b/libs/lucid-http/Makefile new file mode 100644 index 000000000..2bdfad16e --- /dev/null +++ b/libs/lucid-http/Makefile @@ -0,0 +1,2 @@ +include ../../build/module.mk +include ../../build/config.mk
\ No newline at end of file diff --git a/libs/lucid-http/luasrc/lucid/http.lua b/libs/lucid-http/luasrc/lucid/http.lua new file mode 100644 index 000000000..32ba5791d --- /dev/null +++ b/libs/lucid-http/luasrc/lucid/http.lua @@ -0,0 +1,33 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2009 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +local require, ipairs, pcall = require, ipairs, pcall +local srv = require "luci.lucid.http.server" + +module "luci.lucid.http" + +function factory(publisher) + local server = srv.Server() + for _, r in ipairs(publisher) do + local t = r[".type"] + local s, mod = pcall(require, "luci.lucid.http." .. (r[".type"] or "")) + if s and mod then + mod.factory(server, r) + else + return nil, mod + end + end + + return function(...) return server:process(...) end +end
\ No newline at end of file diff --git a/libs/lucid-http/luasrc/lucid/http/DirectoryPublisher.lua b/libs/lucid-http/luasrc/lucid/http/DirectoryPublisher.lua new file mode 100644 index 000000000..f471781a9 --- /dev/null +++ b/libs/lucid-http/luasrc/lucid/http/DirectoryPublisher.lua @@ -0,0 +1,47 @@ +--[[ +LuCId HTTP-Slave +(c) 2009 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +local ipairs, require, tostring, type = ipairs, require, tostring, type +local file = require "luci.lucid.http.handler.file" +local srv = require "luci.lucid.http.server" + +module "luci.lucid.http.DirectoryPublisher" + + +function factory(server, config) + config.domain = config.domain or "" + local vhost = server:get_vhosts()[config.domain] + if not vhost then + vhost = srv.VHost() + server:set_vhost(config.domain, vhost) + end + + local handler = file.Simple(config.name, config.physical, config) + if config.read then + for _, r in ipairs(config.read) do + if r:sub(1,1) == ":" then + handler:restrict({interface = r:sub(2)}) + else + handler:restrict({user = r}) + end + end + end + + if type(config.virtual) == "table" then + for _, v in ipairs(config.virtual) do + vhost:set_handler(v, handler) + end + else + vhost:set_handler(config.virtual, handler) + end +end
\ No newline at end of file diff --git a/libs/lucid-http/luasrc/lucid/http/LuciWebPublisher.lua b/libs/lucid-http/luasrc/lucid/http/LuciWebPublisher.lua new file mode 100644 index 000000000..0d0648967 --- /dev/null +++ b/libs/lucid-http/luasrc/lucid/http/LuciWebPublisher.lua @@ -0,0 +1,62 @@ +--[[ +LuCId HTTP-Slave +(c) 2009 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +local ipairs, pcall, type = ipairs, pcall, type +local luci = require "luci.lucid.http.handler.luci" +local srv = require "luci.lucid.http.server" + + +module "luci.lucid.http.LuciWebPublisher" + +function factory(server, config) + pcall(function() + require "luci.dispatcher" + require "luci.cbi" + end) + + config.domain = config.domain or "" + local vhost = server:get_vhosts()[config.domain] + if not vhost then + vhost = srv.VHost() + server:set_vhost(config.domain, vhost) + end + + local prefix + if config.physical and #config.physical > 0 then + prefix = {} + for k in config.physical:gmatch("[^/]+") do + if #k > 0 then + prefix[#prefix+1] = k + end + end + end + + local handler = luci.Luci(config.name, prefix) + if config.exec then + for _, r in ipairs(config.exec) do + if r:sub(1,1) == ":" then + handler:restrict({interface = r:sub(2)}) + else + handler:restrict({user = r}) + end + end + end + + if type(config.virtual) == "table" then + for _, v in ipairs(config.virtual) do + vhost:set_handler(v, handler) + end + else + vhost:set_handler(config.virtual, handler) + end +end
\ No newline at end of file diff --git a/libs/lucid-http/luasrc/lucid/http/Redirector.lua b/libs/lucid-http/luasrc/lucid/http/Redirector.lua new file mode 100644 index 000000000..c0af90b00 --- /dev/null +++ b/libs/lucid-http/luasrc/lucid/http/Redirector.lua @@ -0,0 +1,31 @@ +--[[ +LuCId HTTP-Slave +(c) 2009 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +local ipairs = ipairs +local catchall = require "luci.lucid.http.handler.catchall" +local srv = require "luci.lucid.http.server" + +module "luci.lucid.http.Redirector" + + +function factory(server, config) + config.domain = config.domain or "" + local vhost = server:get_vhosts()[config.domain] + if not vhost then + vhost = srv.VHost() + server:set_vhost(config.domain, vhost) + end + + local handler = catchall.Redirect(config.name, config.physical) + vhost:set_handler(config.virtual, handler) +end
\ No newline at end of file diff --git a/libs/lucid-http/luasrc/lucid/http/handler/catchall.lua b/libs/lucid-http/luasrc/lucid/http/handler/catchall.lua new file mode 100644 index 000000000..0523751bc --- /dev/null +++ b/libs/lucid-http/luasrc/lucid/http/handler/catchall.lua @@ -0,0 +1,53 @@ +--[[ +LuCId HTTP-Slave +(c) 2009 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +local srv = require "luci.lucid.http.server" +local proto = require "luci.http.protocol" + +module "luci.lucid.http.handler.catchall" + +Redirect = util.class(srv.Handler) + +function Redirect.__init__(self, name, target) + srv.Handler.__init__(self, name) + self.target = target +end + +function Redirect.handle_GET(self, request) + local target = self.target + local protocol = request.env.HTTPS and "https://" or "http://" + local server = request.env.SERVER_ADDR + if server:find(":") then + server = "[" .. server .. "]" + end + + if self.target:sub(1,1) == ":" then + target = protocol .. server .. target + end + + local s, e = target:find("%TARGET%", 1, true) + if s then + local req = protocol .. (request.env.HTTP_HOST or server) + .. request.env.REQUEST_URI + target = target:sub(1, s-1) .. req .. target:sub(e+1) + end + + return 302, { Location = target } +end + +Redirect.handle_POST = Redirect.handle_GET + +function Redirect.handle_HEAD(self, request) + local stat, head = self:handle_GET(request) + return stat, head +end
\ No newline at end of file diff --git a/libs/lucid-http/luasrc/lucid/http/handler/file.lua b/libs/lucid-http/luasrc/lucid/http/handler/file.lua new file mode 100644 index 000000000..d08e47025 --- /dev/null +++ b/libs/lucid-http/luasrc/lucid/http/handler/file.lua @@ -0,0 +1,250 @@ +--[[ + +HTTP server implementation for LuCI - file handler +(c) 2008 Steven Barth <steven@midlink.org> +(c) 2008 Freifunk Leipzig / Jo-Philipp Wich <xm@leipzig.freifunk.net> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +]]-- + +local ipairs, type, tonumber = ipairs, type, tonumber +local os = require "os" +local nixio = require "nixio", require "nixio.util" +local fs = require "nixio.fs" +local util = require "luci.util" +local ltn12 = require "luci.ltn12" +local srv = require "luci.lucid.http.server" +local string = require "string" + +local prot = require "luci.http.protocol" +local date = require "luci.http.protocol.date" +local mime = require "luci.http.protocol.mime" +local cond = require "luci.http.protocol.conditionals" + +module "luci.lucid.http.handler.file" + +Simple = util.class(srv.Handler) + +function Simple.__init__(self, name, docroot, options) + srv.Handler.__init__(self, name) + self.docroot = docroot + self.realdocroot = fs.realpath(self.docroot) + + options = options or {} + self.dirlist = not options.noindex + self.error404 = options.error404 +end + +function Simple.parse_range(self, request, size) + if not request.headers.Range then + return true + end + + local from, to = request.headers.Range:match("bytes=([0-9]*)-([0-9]*)") + if not (from or to) then + return true + end + + from, to = tonumber(from), tonumber(to) + if not (from or to) then + return true + elseif not from then + from, to = size - to, size - 1 + elseif not to then + to = size - 1 + end + + -- Not satisfiable + if from >= size then + return false + end + + -- Normalize + if to >= size then + to = size - 1 + end + + local range = "bytes " .. from .. "-" .. to .. "/" .. size + return from, (1 + to - from), range +end + +function Simple.getfile(self, uri) + if not self.realdocroot then + self.realdocroot = fs.realpath(self.docroot) + end + local file = fs.realpath(self.docroot .. uri) + if not file or file:sub(1, #self.realdocroot) ~= self.realdocroot then + return uri + end + return file, fs.stat(file) +end + +function Simple.handle_GET(self, request) + local file, stat = self:getfile(prot.urldecode(request.env.PATH_INFO, true)) + + if stat then + if stat.type == "reg" then + + -- Generate Entity Tag + local etag = cond.mk_etag( stat ) + + -- Check conditionals + local ok, code, hdrs + + ok, code, hdrs = cond.if_modified_since( request, stat ) + if ok then + ok, code, hdrs = cond.if_match( request, stat ) + if ok then + ok, code, hdrs = cond.if_unmodified_since( request, stat ) + if ok then + ok, code, hdrs = cond.if_none_match( request, stat ) + if ok then + local f, err = nixio.open(file) + + if f then + local code = 200 + local o, s, r = self:parse_range(request, stat.size) + + if not o then + return self:failure(416, "Invalid Range") + end + + local headers = { + ["Last-Modified"] = date.to_http( stat.mtime ), + ["Content-Type"] = mime.to_mime( file ), + ["ETag"] = etag, + ["Accept-Ranges"] = "bytes", + } + + if o == true then + s = stat.size + else + code = 206 + headers["Content-Range"] = r + f:seek(o) + end + + headers["Content-Length"] = s + + -- Send Response + return code, headers, srv.IOResource(f, s) + else + return self:failure( 403, err:gsub("^.+: ", "") ) + end + else + return code, hdrs + end + else + return code, hdrs + end + else + return code, hdrs + end + else + return code, hdrs + end + + elseif stat.type == "dir" then + + local ruri = request.env.REQUEST_URI:gsub("/$", "") + local duri = prot.urldecode( ruri, true ) + local root = self.docroot + + -- check for index files + local index_candidates = { + "index.html", "index.htm", "default.html", "default.htm", + "index.txt", "default.txt" + } + + -- try to find an index file and redirect to it + for i, candidate in ipairs( index_candidates ) do + local istat = fs.stat( + root .. "/" .. duri .. "/" .. candidate + ) + + if istat ~= nil and istat.type == "reg" then + return 302, { Location = ruri .. "/" .. candidate } + end + end + + + local html = string.format( + '<?xml version="1.0" encoding="utf-8"?>\n' .. + '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' .. + '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'.. + '<html xmlns="http://www.w3.org/1999/xhtml" ' .. + 'xml:lang="en" lang="en">\n' .. + '<head>\n' .. + '<title>Index of %s/</title>\n' .. + '<style type="text/css">\n' .. + 'body { color:#000000 } ' .. + 'li { border-bottom:1px dotted #CCCCCC; padding:3px } ' .. + 'small { font-size:60%%; color:#333333 } ' .. + 'p { margin:0 }' .. + '\n</style></head><body><h1>Index of %s/</h1><hr /><ul>'.. + '<li><p><a href="%s/../">../</a> ' .. + '<small>(parent directory)</small><br />' .. + '<small></small></li>', + duri, duri, ruri + ) + + local entries = fs.dir( file ) + + if type(entries) == "function" then + for i, e in util.vspairs(nixio.util.consume(entries)) do + local estat = fs.stat( file .. "/" .. e ) + + if estat.type == "dir" then + html = html .. string.format( + '<li><p><a href="%s/%s/">%s/</a> ' .. + '<small>(directory)</small><br />' .. + '<small>Changed: %s</small></li>', + ruri, prot.urlencode( e ), e, + date.to_http( estat.mtime ) + ) + else + html = html .. string.format( + '<li><p><a href="%s/%s">%s</a> ' .. + '<small>(%s)</small><br />' .. + '<small>Size: %i Bytes | ' .. + 'Changed: %s</small></li>', + ruri, prot.urlencode( e ), e, + mime.to_mime( e ), + estat.size, date.to_http( estat.mtime ) + ) + end + end + + html = html .. '</ul><hr /><address>LuCId-HTTPd' .. + '</address></body></html>' + + return 200, { + ["Date"] = date.to_http( os.time() ); + ["Content-Type"] = "text/html; charset=utf-8"; + }, ltn12.source.string(html) + else + return self:failure(403, "Permission denied") + end + else + return self:failure(403, "Unable to transmit " .. stat.type .. " " .. file) + end + else + if self.error404 then + return 302, { Location = self.error404 } + else + return self:failure(404, "No such file: " .. file) + end + end +end + +function Simple.handle_HEAD(self, ...) + local stat, head = self:handle_GET(...) + return stat, head +end diff --git a/libs/lucid-http/luasrc/lucid/http/handler/luci.lua b/libs/lucid-http/luasrc/lucid/http/handler/luci.lua new file mode 100644 index 000000000..c54e39366 --- /dev/null +++ b/libs/lucid-http/luasrc/lucid/http/handler/luci.lua @@ -0,0 +1,96 @@ +--[[ +LuCId HTTP-Slave +(c) 2009 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +local dsp = require "luci.dispatcher" +local util = require "luci.util" +local http = require "luci.http" +local ltn12 = require "luci.ltn12" +local srv = require "luci.lucid.http.server" +local coroutine = require "coroutine" +local type = type + +module "luci.lucid.http.handler.luci" + +Luci = util.class(srv.Handler) + +function Luci.__init__(self, name, prefix) + srv.Handler.__init__(self, name) + self.prefix = prefix +end + +function Luci.handle_HEAD(self, ...) + local stat, head = self:handle_GET(...) + return stat, head +end + +function Luci.handle_POST(self, ...) + return self:handle_GET(...) +end + +function Luci.handle_GET(self, request, sourcein) + local r = http.Request( + request.env, + sourcein + ) + + local res, id, data1, data2 = true, 0, nil, nil + local headers = {} + local status = 200 + local active = true + + local x = coroutine.create(dsp.httpdispatch) + while not id or id < 3 do + res, id, data1, data2 = coroutine.resume(x, r, self.prefix) + + if not res then + status = 500 + headers["Content-Type"] = "text/plain" + return status, headers, ltn12.source.string(id) + end + + if id == 1 then + status = data1 + elseif id == 2 then + if not headers[data1] then + headers[data1] = data2 + elseif type(headers[data1]) ~= "table" then + headers[data1] = {headers[data1], data2} + else + headers[data1][#headers[data1]+1] = data2 + end + end + end + + if id == 6 then + while (coroutine.resume(x)) do end + return status, headers, srv.IOResource(data1, data2) + end + + local function iter() + local res, id, data = coroutine.resume(x) + if not res then + return nil, id + elseif not id or not active then + return true + elseif id == 5 then + active = false + while (coroutine.resume(x)) do end + return nil + elseif id == 4 then + return data + end + end + + return status, headers, iter +end + diff --git a/libs/lucid-http/luasrc/lucid/http/server.lua b/libs/lucid-http/luasrc/lucid/http/server.lua new file mode 100644 index 000000000..f5de4e9a1 --- /dev/null +++ b/libs/lucid-http/luasrc/lucid/http/server.lua @@ -0,0 +1,522 @@ +--[[ +LuCId HTTP-Slave +(c) 2009 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +local ipairs, pairs = ipairs, pairs +local tostring, tonumber = tostring, tonumber +local pcall, assert, type = pcall, assert, type + +local os = require "os" +local nixio = require "nixio" +local util = require "luci.util" +local ltn12 = require "luci.ltn12" +local proto = require "luci.http.protocol" +local table = require "table" +local date = require "luci.http.protocol.date" + +module "luci.lucid.http.server" + +VERSION = "1.0" + +statusmsg = { + [200] = "OK", + [206] = "Partial Content", + [301] = "Moved Permanently", + [302] = "Found", + [304] = "Not Modified", + [400] = "Bad Request", + [401] = "Unauthorized", + [403] = "Forbidden", + [404] = "Not Found", + [405] = "Method Not Allowed", + [408] = "Request Time-out", + [411] = "Length Required", + [412] = "Precondition Failed", + [416] = "Requested range not satisfiable", + [500] = "Internal Server Error", + [503] = "Server Unavailable", +} + +-- File Resource +IOResource = util.class() + +function IOResource.__init__(self, fd, len) + self.fd, self.len = fd, len +end + + +-- Server handler implementation +Handler = util.class() + +function Handler.__init__(self, name) + self.name = name or tostring(self) +end + +-- Creates a failure reply +function Handler.failure(self, code, msg) + return code, { ["Content-Type"] = "text/plain" }, ltn12.source.string(msg) +end + +-- Access Restrictions +function Handler.restrict(self, restriction) + if not self.restrictions then + self.restrictions = {restriction} + else + self.restrictions[#self.restrictions+1] = restriction + end +end + +-- Check restrictions +function Handler.checkrestricted(self, request) + if not self.restrictions then + return + end + + local localif, user, pass + + for _, r in ipairs(self.restrictions) do + local stat = true + if stat and r.interface then -- Interface restriction + if not localif then + for _, v in ipairs(request.server.interfaces) do + if v.addr == request.env.SERVER_ADDR then + localif = v.name + break + end + end + end + + if r.interface ~= localif then + stat = false + end + end + + if stat and r.user then -- User restriction + local rh, pwe + if not user then + rh = (request.headers.Authorization or ""):match("Basic (.*)") + rh = rh and nixio.bin.b64decode(rh) or "" + user, pass = rh:match("(.*):(.*)") + pass = pass or "" + end + pwe = nixio.getsp and nixio.getsp(r.user) or nixio.getpw(r.user) + local pwh = (user == r.user) and pwe and (pwe.pwdp or pwe.passwd) + if not pwh or #pwh < 1 or nixio.crypt(pass, pwh) ~= pwh then + stat = false + end + end + + if stat then + return + end + end + + return 401, { + ["WWW-Authenticate"] = ('Basic realm=%q'):format(self.name), + ["Content-Type"] = 'text/plain' + }, ltn12.source.string("Unauthorized") +end + +-- Processes a request +function Handler.process(self, request, sourcein) + local stat, code, hdr, sourceout + + local stat, code, msg = self:checkrestricted(request) + if stat then -- Access Denied + return stat, code, msg + end + + -- Detect request Method + local hname = "handle_" .. request.env.REQUEST_METHOD + if self[hname] then + -- Run the handler + stat, code, hdr, sourceout = pcall(self[hname], self, request, sourcein) + + -- Check for any errors + if not stat then + return self:failure(500, code) + end + else + return self:failure(405, statusmsg[405]) + end + + return code, hdr, sourceout +end + + +VHost = util.class() + +function VHost.__init__(self) + self.handlers = {} +end + +function VHost.process(self, request, ...) + local handler + local hlen = -1 + local uri = request.env.SCRIPT_NAME + local sc = ("/"):byte() + + -- SCRIPT_NAME + request.env.SCRIPT_NAME = "" + + -- Call URI part + request.env.PATH_INFO = uri + + for k, h in pairs(self.handlers) do + if #k > hlen then + if uri == k or (uri:sub(1, #k) == k and uri:byte(#k+1) == sc) then + handler = h + hlen = #k + request.env.SCRIPT_NAME = k + request.env.PATH_INFO = uri:sub(#k+1) + end + end + end + + if handler then + return handler:process(request, ...) + else + return 404, nil, ltn12.source.string("No such handler") + end +end + +function VHost.get_handlers(self) + return self.handlers +end + +function VHost.set_handler(self, match, handler) + self.handlers[match] = handler +end + + +local function remapipv6(adr) + local map = "::ffff:" + if adr:sub(1, #map) == map then + return adr:sub(#map+1) + else + return adr + end +end + +local function chunksource(sock, buffer) + buffer = buffer or "" + return function() + local output + local _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n") + while not count and #buffer <= 1024 do + local newblock, code = sock:recv(1024 - #buffer) + if not newblock then + return nil, code + end + buffer = buffer .. newblock + _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n") + end + count = tonumber(count, 16) + if not count then + return nil, -1, "invalid encoding" + elseif count == 0 then + return nil + elseif count + 2 <= #buffer - endp then + output = buffer:sub(endp+1, endp+count) + buffer = buffer:sub(endp+count+3) + return output + else + output = buffer:sub(endp+1, endp+count) + buffer = "" + if count - #output > 0 then + local remain, code = sock:recvall(count-#output) + if not remain then + return nil, code + end + output = output .. remain + count, code = sock:recvall(2) + else + count, code = sock:recvall(count+2-#buffer+endp) + end + if not count then + return nil, code + end + return output + end + end +end + +local function chunksink(sock) + return function(chunk, err) + if not chunk then + return sock:writeall("0\r\n\r\n") + else + return sock:writeall(("%X\r\n%s\r\n"):format(#chunk, chunk)) + end + end +end + +Server = util.class() + +function Server.__init__(self) + self.vhosts = {} +end + +function Server.get_vhosts(self) + return self.vhosts +end + +function Server.set_vhost(self, name, vhost) + self.vhosts[name] = vhost +end + +function Server.error(self, client, code, msg) + hcode = tostring(code) + + client:writeall( "HTTP/1.0 " .. hcode .. " " .. + statusmsg[code] .. "\r\n" ) + client:writeall( "Connection: close\r\n" ) + client:writeall( "Content-Type: text/plain\r\n\r\n" ) + + if msg then + client:writeall( "HTTP-Error " .. code .. ": " .. msg .. "\r\n" ) + end + + client:close() +end + +local hdr2env = { + ["Content-Length"] = "CONTENT_LENGTH", + ["Content-Type"] = "CONTENT_TYPE", + ["Content-type"] = "CONTENT_TYPE", + ["Accept"] = "HTTP_ACCEPT", + ["Accept-Charset"] = "HTTP_ACCEPT_CHARSET", + ["Accept-Encoding"] = "HTTP_ACCEPT_ENCODING", + ["Accept-Language"] = "HTTP_ACCEPT_LANGUAGE", + ["Connection"] = "HTTP_CONNECTION", + ["Cookie"] = "HTTP_COOKIE", + ["Host"] = "HTTP_HOST", + ["Referer"] = "HTTP_REFERER", + ["User-Agent"] = "HTTP_USER_AGENT" +} + +function Server.parse_headers(self, source) + local env = {} + local req = {env = env, headers = {}} + local line, err + + repeat -- Ignore empty lines + line, err = source() + if not line then + return nil, err + end + until #line > 0 + + env.REQUEST_METHOD, env.REQUEST_URI, env.SERVER_PROTOCOL = + line:match("^([A-Z]+) ([^ ]+) (HTTP/1%.[01])$") + + if not env.REQUEST_METHOD then + return nil, "invalid magic" + end + + local key, envkey, val + repeat + line, err = source() + if not line then + return nil, err + elseif #line > 0 then + key, val = line:match("^([%w-]+)%s?:%s?(.*)") + if key then + req.headers[key] = val + envkey = hdr2env[key] + if envkey then + env[envkey] = val + end + else + return nil, "invalid header line" + end + else + break + end + until false + + env.SCRIPT_NAME, env.QUERY_STRING = env.REQUEST_URI:match("(.*)%??(.*)") + return req +end + + +function Server.process(self, client, env) + local sourcein = function() end + local sourcehdr = client:linesource() + local sinkout + local buffer + + local close = false + local stat, code, msg, message, err + + client:setsockopt("socket", "rcvtimeo", 15) + client:setsockopt("socket", "sndtimeo", 15) + + repeat + -- parse headers + message, err = self:parse_headers(sourcehdr) + + -- any other error + if not message or err then + if err == 11 then -- EAGAIN + break + else + return self:error(client, 400, err) + end + end + + -- Prepare sources and sinks + buffer = sourcehdr(true) + sinkout = client:sink() + message.server = env + + if client:is_tls_socket() then + message.env.HTTPS = "on" + end + + -- Addresses + message.env.REMOTE_ADDR = remapipv6(env.host) + message.env.REMOTE_PORT = env.port + + local srvaddr, srvport = client:getsockname() + message.env.SERVER_ADDR = remapipv6(srvaddr) + message.env.SERVER_PORT = srvport + + -- keep-alive + if message.env.SERVER_PROTOCOL == "HTTP/1.1" then + close = (message.env.HTTP_CONNECTION == "close") + else + close = not message.env.HTTP_CONNECTION + or message.env.HTTP_CONNECTION == "close" + end + + -- Uncomment this to disable keep-alive + close = close or env.config.nokeepalive + + if message.env.REQUEST_METHOD == "GET" + or message.env.REQUEST_METHOD == "HEAD" then + -- Be happy + + elseif message.env.REQUEST_METHOD == "POST" then + -- If we have a HTTP/1.1 client and an Expect: 100-continue header + -- respond with HTTP 100 Continue message + if message.env.SERVER_PROTOCOL == "HTTP/1.1" + and message.headers.Expect == '100-continue' then + client:writeall("HTTP/1.1 100 Continue\r\n\r\n") + end + + if message.headers['Transfer-Encoding'] and + message.headers['Transfer-Encoding'] ~= "identity" then + sourcein = chunksource(client, buffer) + buffer = nil + elseif message.env.CONTENT_LENGTH then + local len = tonumber(message.env.CONTENT_LENGTH) + if #buffer >= len then + sourcein = ltn12.source.string(buffer:sub(1, len)) + buffer = buffer:sub(len+1) + else + sourcein = ltn12.source.cat( + ltn12.source.string(buffer), + client:blocksource(nil, len - #buffer) + ) + end + else + return self:error(client, 411, statusmsg[411]) + end + else + return self:error(client, 405, statusmsg[405]) + end + + + local host = self.vhosts[message.env.HTTP_HOST] or self.vhosts[""] + if not host then + return self:error(client, 404, "No virtual host found") + end + + local code, headers, sourceout = host:process(message, sourcein) + headers = headers or {} + + -- Post process response + if sourceout then + if util.instanceof(sourceout, IOResource) then + if not headers["Content-Length"] then + headers["Content-Length"] = sourceout.len + end + end + if not headers["Content-Length"] then + if message.http_version == 1.1 then + headers["Transfer-Encoding"] = "chunked" + sinkout = chunksink(client) + else + close = true + end + end + elseif message.request_method ~= "head" then + headers["Content-Length"] = 0 + end + + if close then + headers["Connection"] = "close" + elseif message.env.SERVER_PROTOCOL == "HTTP/1.0" then + headers["Connection"] = "Keep-Alive" + end + + headers["Date"] = date.to_http(os.time()) + local header = { + message.env.SERVER_PROTOCOL .. " " .. tostring(code) .. " " + .. statusmsg[code], + "Server: LuCId-HTTPd/" .. VERSION + } + + + for k, v in pairs(headers) do + if type(v) == "table" then + for _, h in ipairs(v) do + header[#header+1] = k .. ": " .. h + end + else + header[#header+1] = k .. ": " .. v + end + end + + header[#header+1] = "" + header[#header+1] = "" + + -- Output + stat, code, msg = client:writeall(table.concat(header, "\r\n")) + + if sourceout and stat then + if util.instanceof(sourceout, IOResource) then + stat, code, msg = sourceout.fd:copyz(client, sourceout.len) + else + stat, msg = ltn12.pump.all(sourceout, sinkout) + end + end + + + -- Write errors + if not stat then + if msg then + nixio.syslog("err", "Error sending data to " .. env.host .. + ": " .. msg .. "\n") + end + break + end + + if buffer then + sourcehdr(buffer) + end + until close + + client:shutdown() + client:close() +end |