summaryrefslogtreecommitdiffhomepage
path: root/libs/lucid-http/luasrc/lucid
diff options
context:
space:
mode:
authorSteven Barth <steven@midlink.org>2009-05-23 17:21:36 +0000
committerSteven Barth <steven@midlink.org>2009-05-23 17:21:36 +0000
commit8c4f847ea5b95aaf0e716beaf736b4e2b67655ae (patch)
tree5b931064a2df45c3903b66b425f49b621e9c31df /libs/lucid-http/luasrc/lucid
parent0ad58e38b42dca2f46bc492b7f889b5031a9c6a1 (diff)
GSoC Commit #1: LuCId + HTTP-Server
Diffstat (limited to 'libs/lucid-http/luasrc/lucid')
-rw-r--r--libs/lucid-http/luasrc/lucid/http.lua33
-rw-r--r--libs/lucid-http/luasrc/lucid/http/DirectoryPublisher.lua47
-rw-r--r--libs/lucid-http/luasrc/lucid/http/LuciWebPublisher.lua62
-rw-r--r--libs/lucid-http/luasrc/lucid/http/Redirector.lua31
-rw-r--r--libs/lucid-http/luasrc/lucid/http/handler/catchall.lua53
-rw-r--r--libs/lucid-http/luasrc/lucid/http/handler/file.lua250
-rw-r--r--libs/lucid-http/luasrc/lucid/http/handler/luci.lua96
-rw-r--r--libs/lucid-http/luasrc/lucid/http/server.lua522
8 files changed, 1094 insertions, 0 deletions
diff --git a/libs/lucid-http/luasrc/lucid/http.lua b/libs/lucid-http/luasrc/lucid/http.lua
new file mode 100644
index 0000000000..32ba5791de
--- /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 0000000000..f471781a93
--- /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 0000000000..0d06489678
--- /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 0000000000..c0af90b00c
--- /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 0000000000..0523751bc1
--- /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 0000000000..d08e47025d
--- /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 0000000000..c54e39366a
--- /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 0000000000..f5de4e9a11
--- /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