GSoC Commit #1: LuCId + HTTP-Server

This commit is contained in:
Steven Barth 2009-05-23 17:21:36 +00:00
parent 0ad58e38b4
commit 8c4f847ea5
20 changed files with 1739 additions and 10 deletions

View file

@ -52,10 +52,11 @@ runboa: hostenv
runhttpd: hostenv
build/hostenv.sh $(realpath host) $(LUA_MODULEDIR) $(LUA_LIBRARYDIR) "$(realpath host/usr/bin/lucittpd) $(realpath host)/usr/lib/lucittpd/plugins"
runluci: runhttpd
runluci: luahost
build/hostenv.sh $(realpath host) $(LUA_MODULEDIR) $(LUA_LIBRARYDIR) "$(realpath libs/httpd/host/runluci) $(realpath host) $(HTDOCS)"
runlua: hostenv
build/hostenv.sh $(realpath host) $(LUA_MODULEDIR) $(LUA_LIBRARYDIR) lua
build/hostenv.sh $(realpath host) $(LUA_MODULEDIR) $(LUA_LIBRARYDIR) "lua -i build/setup.lua"
runshell: hostenv
build/hostenv.sh $(realpath host) $(LUA_MODULEDIR) $(LUA_LIBRARYDIR) $$SHELL

View file

@ -38,9 +38,6 @@ luastrip: luasource
luacompile: luasource
for i in $$(find dist -name *.lua -not -name debug.lua); do $(LUAC) $(LUAC_OPTIONS) -o $$i $$i; done
luagzip: luacompile
for i in $$(find dist -name *.lua -not -name debug.lua); do gzip -f9 $$i; done
luaclean:
rm -rf dist

11
build/setup.lua Normal file
View file

@ -0,0 +1,11 @@
local SYSROOT = os.getenv("LUCI_SYSROOT")
require "uci"
require "luci.model.uci".cursor = function(config, save)
return uci.cursor(config or SYSROOT .. "/etc/config", save or SYSROOT .. "/tmp/.uci")
end
local x = require "luci.uvl".UVL.__init__
require "luci.uvl".UVL.__init__ = function(self, schemedir)
x(self, schemedir or SYSROOT .. "/lib/uci/schema")
end

2
libs/lucid-http/Makefile Normal file
View file

@ -0,0 +1,2 @@
include ../../build/module.mk
include ../../build/config.mk

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

2
libs/lucid/Makefile Normal file
View file

@ -0,0 +1,2 @@
include ../../build/config.mk
include ../../build/module.mk

View file

@ -0,0 +1,61 @@
config lucid main
option pollinterval 15000
option daemon 1
option debug 1
list supports tcpserver
list supports server
config DirectoryPublisher webroot
option name 'Webserver Share'
option physical host/www
option virtual ''
option domain ''
list read ':lo'
list read ':br-lan'
list read 'root'
config LuciWebPublisher luciweb
option name 'LuCI Webapplication'
option physical ''
list virtual /luci
option domain ''
list exec ':lo'
list exec ':br-lan'
list exec 'root'
config RPCPublisher mainrpc
option namespace 'luci.lucid.rpc'
list export system
list export ruci
list exec ':lo'
list exec 'root'
config tcpserver httpd
option entrypoint "luci.lucid.http"
list supports DirectoryPublisher
list supports LuciWebPublisher
config tcpserver rpcd
option entrypoint "luci.lucid.rpc"
list supports RPCPublisher
config daemon http
option slave httpd
list address 8080
list publisher webroot
list publisher luciweb
option enabled 1
config daemon https
option slave httpd
list address 4443
list publisher webroot
list publisher luciweb
option enabled 1
option encryption enable
config daemon rpc
option slave rpcd
list address 12900
list publisher mainrpc
option enabled 1

266
libs/lucid/luasrc/lucid.lua Normal file
View file

@ -0,0 +1,266 @@
--[[
LuCI - Lua Development Framework
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 nixio = require "nixio"
local table = require "table"
local uci = require "luci.model.uci"
local os = require "os"
local io = require "io"
local pairs, require, pcall, assert, type = pairs, require, pcall, assert, type
local ipairs, tonumber, collectgarbage = ipairs, tonumber, collectgarbage
module "luci.lucid"
local slaves = {}
local pollt = {}
local tickt = {}
local tpids = {}
local tcount = 0
local ifaddrs = nixio.getifaddrs()
cursor = uci.cursor()
state = uci.cursor_state()
UCINAME = "lucid"
local cursor = cursor
local state = state
local UCINAME = UCINAME
local SSTATE = "/tmp/.lucid_store"
function start()
prepare()
local detach = cursor:get(UCINAME, "main", "daemonize")
if detach == "1" then
local stat, code, msg = daemonize()
if not stat then
nixio.syslog("crit", "Unable to detach process: " .. msg .. "\n")
ox.exit(2)
end
end
run()
end
function prepare()
local debug = tonumber((cursor:get(UCINAME, "main", "debug")))
nixio.openlog("lucid", "pid", "perror")
if debug ~= 1 then
nixio.setlogmask("warning")
end
cursor:foreach(UCINAME, "daemon", function(config)
if config.enabled ~= "1" then
return
end
local key = config[".name"]
if not config.slave then
nixio.syslog("crit", "Daemon "..key.." is missing a slave\n")
os.exit(1)
else
nixio.syslog("info", "Initializing daemon " .. key)
end
state:revert(UCINAME, key)
local daemon, code, err = prepare_daemon(config)
if daemon then
state:set(UCINAME, key, "status", "started")
nixio.syslog("info", "Prepared daemon " .. key)
else
state:set(UCINAME, key, "status", "error")
state:set(UCINAME, key, "error", err)
nixio.syslog("err", "Failed to initialize daemon "..key..": "..
err .. "\n")
end
end)
end
function run()
local pollint = tonumber((cursor:get(UCINAME, "main", "pollinterval")))
while true do
local stat, code = nixio.poll(pollt, pollint)
if stat and stat > 0 then
for _, polle in ipairs(pollt) do
if polle.revents ~= 0 and polle.handler then
polle.handler(polle)
end
end
elseif stat == 0 then
ifaddrs = nixio.getifaddrs()
collectgarbage("collect")
end
for _, cb in ipairs(tickt) do
cb()
end
local pid, stat, code = nixio.wait(-1, "nohang")
while pid and pid > 0 do
tcount = tcount - 1
if tpids[pid] and tpids[pid] ~= true then
tpids[pid](pid, stat, code)
end
pid, stat, code = nixio.wait(-1, "nohang")
end
end
end
function register_pollfd(polle)
pollt[#pollt+1] = polle
return true
end
function unregister_pollfd(polle)
for k, v in ipairs(pollt) do
if v == polle then
table.remove(pollt, k)
return true
end
end
return false
end
function close_pollfds()
for k, v in ipairs(pollt) do
if v.fd and v.fd.close then
v.fd:close()
end
end
end
function register_tick(cb)
tickt[#tickt+1] = cb
return true
end
function unregister_tick(cb)
for k, v in ipairs(tickt) do
if v == cb then
table.remove(tickt, k)
return true
end
end
return false
end
function create_process(threadcb, waitcb)
local threadlimit = tonumber(cursor:get(UCINAME, "main", "threadlimit"))
if threadlimit and #tpids >= tcount then
nixio.syslog("warning", "Unable to create thread: process limit reached")
return nil
end
local pid, code, err = nixio.fork()
if pid and pid ~= 0 then
tpids[pid] = waitcb
tcount = tcount + 1
elseif pid == 0 then
local code = threadcb()
os.exit(code)
else
nixio.syslog("err", "Unable to fork(): " .. err)
end
return pid, code, err
end
function prepare_daemon(config)
nixio.syslog("info", "Preparing daemon " .. config[".name"])
local modname = cursor:get(UCINAME, config.slave)
if not modname then
return nil, -1, "invalid slave"
end
local stat, module = pcall(require, _NAME .. "." .. modname)
if not stat or not module.prepare_daemon then
return nil, -2, "slave type not supported"
end
config.slave = prepare_slave(config.slave)
return module.prepare_daemon(config, _M)
end
function prepare_slave(name)
local slave = slaves[name]
if not slave then
local config = cursor:get_all(UCINAME, name)
local stat, module = pcall(require, config and config.entrypoint)
if stat then
slave = {module = module, config = config}
end
end
if slave then
return slave
else
return nil, module
end
end
function get_interfaces()
return ifaddrs
end
function revoke_privileges(user, group)
if nixio.getuid() == 0 then
return nixio.setgid(group) and nixio.setuid(user)
end
end
function securestate()
local stat = nixio.fs.stat(SSTATE) or {}
local uid = nixio.getuid()
if stat.type ~= "dir" or (stat.modedec % 100) ~= 0 or stat.uid ~= uid then
nixio.fs.remover(SSTATE)
if not nixio.fs.mkdir(SSTATE, 700) then
local errno = nixio.errno()
nixio.syslog("err", "Integrity check on secure state failed!")
return nil, errno, nixio.perror(errno)
end
end
return uci.cursor(nil, SSTATE)
end
function daemonize()
if nixio.getppid() == 1 then
return
end
local pid, code, msg = nixio.fork()
if not pid then
return nil, code, msg
elseif pid > 0 then
os.exit(0)
end
nixio.setsid()
nixio.chdir("/")
local devnull = nixio.open("/dev/null", nixio.open_flags("rdwr"))
nixio.dup(devnull, nixio.stdin)
nixio.dup(devnull, nixio.stdout)
nixio.dup(devnull, nixio.stderr)
return true
end

View file

@ -0,0 +1,192 @@
--[[
LuCI - Lua Development Framework
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 os = require "os"
local nixio = require "nixio"
local lucid = require "luci.lucid"
local ipairs, type, require, setmetatable = ipairs, type, require, setmetatable
local pairs, print, tostring, unpack = pairs, print, tostring, unpack
module "luci.lucid.tcpserver"
local cursor = lucid.cursor
local UCINAME = lucid.UCINAME
local tcpsockets = {}
function prepare_daemon(config, server)
nixio.syslog("info", "Preparing TCP-Daemon " .. config[".name"])
if type(config.address) ~= "table" then
config.address = {config.address}
end
local sockets, socket, code, err = {}
local sopts = {reuseaddr = 1}
for _, addr in ipairs(config.address) do
local host, port = addr:match("(.-):?([^:]*)")
if not host then
nixio.syslog("err", "Invalid address: " .. addr)
return nil, -5, "invalid address format"
elseif #host == 0 then
host = nil
end
socket, code, err = prepare_socket(config.family, host, port, sopts)
if socket then
sockets[#sockets+1] = socket
end
end
nixio.syslog("info", "Sockets bound for " .. config[".name"])
if #sockets < 1 then
return nil, -6, "no sockets bound"
end
nixio.syslog("info", "Preparing publishers for " .. config[".name"])
local publisher = {}
for k, pname in ipairs(config.publisher) do
local pdata = cursor:get_all(UCINAME, pname)
if pdata then
publisher[#publisher+1] = pdata
else
nixio.syslog("err", "Publisher " .. pname .. " not found")
end
end
nixio.syslog("info", "Preparing TLS for " .. config[".name"])
local tls = prepare_tls(config.tls)
if not tls and config.encryption == "enable" then
for _, s in ipairs(sockets) do
s:close()
end
return nil, -4, "Encryption requested, but no TLS context given"
end
nixio.syslog("info", "Invoking daemon factory for " .. config[".name"])
local handler, err = config.slave.module.factory(publisher, config)
if not handler then
for _, s in ipairs(sockets) do
s:close()
end
return nil, -3, err
else
local pollin = nixio.poll_flags("in")
for _, s in ipairs(sockets) do
server.register_pollfd({
fd = s,
events = pollin,
revents = 0,
handler = accept,
accept = handler,
config = config,
publisher = publisher,
tls = tls
})
end
return true
end
end
function accept(polle)
local socket, host, port = polle.fd:accept()
if not socket then
return nixio.syslog("warn", "accept() failed: " .. port)
end
socket:setblocking(true)
local function thread()
lucid.close_pollfds()
local inst = setmetatable({
host = host, port = port, interfaces = lucid.get_interfaces()
}, {__index = polle})
if polle.config.encryption then
socket = polle.tls:create(socket)
if not socket:accept() then
socket:close()
return nixio.syslog("warning", "TLS handshake failed: " .. host)
end
end
return polle.accept(socket, inst)
end
local stat = {lucid.create_process(thread)}
socket:close()
return unpack(stat)
end
function prepare_socket(family, host, port, opts, backlog)
nixio.syslog("info", "Preparing socket for port " .. port)
backlog = backlog or 1024
family = family or "inetany"
opts = opts or {}
local inetany = family == "inetany"
family = inetany and "inet6" or family
local socket, code, err = nixio.socket(family, "stream")
if not socket and inetany then
family = "inet"
socket, code, err = nixio.socket(family, "stream")
end
if not socket then
return nil, code, err
end
for k, v in pairs(opts) do
socket:setsockopt("socket", k, v)
end
local stat, code, err = socket:bind(host, port)
if not stat then
return nil, code, err
end
stat, code, err = socket:listen(backlog)
if not stat then
return nil, code, err
end
socket:setblocking(false)
return socket, family
end
function prepare_tls(tlskey)
local tls = nixio.tls()
if tlskey and cursor:get(UCINAME, tlskey) then
local cert = cursor:get(UCINAME, tlskey, "cert")
if cert then
tls:set_cert(cert)
end
local key = cursor:get(UCINAME, tlskey, "key")
if key then
tls:set_key(key)
end
local ciphers = cursor:get(UCINAME, tlskey, "ciphers")
if ciphers then
if type(ciphers) == "table" then
ciphers = table.concat(ciphers, ":")
end
tls:set_ciphers(ciphers)
end
end
return tls
end

View file

@ -0,0 +1,61 @@
config lucid main
option pollinterval 15000
option threadlimit 25
option daemon 1
option debug 1
list supports tcpserver
list supports server
config DirectoryPublisher webroot
option name 'Webserver Share'
option physical /www
option virtual /
option domain ''
list read ':lo'
list read ':br-lan'
list read 'root'
config LuciWebPublisher luciweb
option name 'LuCI Webapplication'
option physical ''
list virtual /luci
option domain ''
list exec ':lo'
list exec ':br-lan'
list exec 'root'
config RPCPublisher mainrpc
option namespace 'luci.lucid.rpc'
list export system
list exec ':lo'
list exec 'root'
config tcpserver httpd
option entrypoint "luci.lucid.http"
list supports DirectoryPublisher
list supports LuciWebPublisher
config tcpserver rpcd
option entrypoint "luci.lucid.rpc"
list supports RPCPublisher
config daemon http
option slave httpd
list address 8080
list publisher webroot
list publisher luciweb
option enabled 1
config daemon https
option slave httpd
list address 4443
list publisher webroot
list publisher luciweb
option enabled 1
option encryption enable
config daemon rpc
option slave rpcd
list address 12900
list publisher mainrpc
option enabled 1

View file

@ -8,3 +8,4 @@ lkc_defs.h
mconf
zconf.tab.h
zconf.tab.c
src/nixio.dll

View file

@ -79,7 +79,7 @@ function copy(src, dest)
end
elseif stat.type == "lnk" then
res, code, msg = nixio.symlink(nixio.readlink(src), dest)
else
elseif stat.type == "reg" then
res, code, msg = datacopy(src, dest)
end

View file

@ -14,7 +14,7 @@ $Id$
local table = require "table"
local nixio = require "nixio"
local getmetatable, assert, pairs = getmetatable, assert, pairs
local getmetatable, assert, pairs, type = getmetatable, assert, pairs, type
module "nixio.util"
@ -106,7 +106,7 @@ function meta.linesource(self, limit)
if flush then
line = buffer:sub(bpos + 1)
buffer = ""
buffer = type(flush) == "string" and flush or ""
bpos = 0
return line
end
@ -189,8 +189,11 @@ end
function meta.copyz(self, fd, size)
local sent, lsent, code, msg = 0
local splicable
if self:is_file() then
if nixio.sendfile and fd:is_socket() and self:stat("type") == "reg" then
local ftype = self:stat("type")
if nixio.sendfile and fd:is_socket() and ftype == "reg" then
repeat
lsent, code, msg = nixio.sendfile(fd, self, size or ZIOBLKSIZE)
if lsent then
@ -202,7 +205,27 @@ function meta.copyz(self, fd, size)
code ~= nixio.const.ENOSYS and code ~= nixio.const.EINVAL) then
return lsent and sent, code, msg, sent
end
end
elseif nixio.splice and not fd:is_tls_socket() and ftype == "fifo" then
splicable = true
end
end
if nixio.splice and fd:is_file() and not splicable then
splicable = not self:is_tls_socket() and fd:stat("type") == "fifo"
end
if splicable then
repeat
lsent, code, msg = nixio.splice(self, fd, size or ZIOBLKSIZE)
if lsent then
sent = sent + lsent
size = size and (size - lsent)
end
until (not lsent or lsent == 0 or (size and size == 0))
if lsent or (not lsent and sent == 0 and
code ~= nixio.const.ENOSYS and code ~= nixio.const.EINVAL) then
return lsent and sent, code, msg, sent
end
end
return self:copy(fd, size)
@ -212,6 +235,24 @@ function tls_socket.close(self)
return self.socket:close()
end
function tls_socket.getsockname(self)
return self.socket:getsockname()
end
function tls_socket.getpeername(self)
return self.socket:getpeername()
end
function tls_socket.getsockopt(self, ...)
return self.socket:getsockopt(...)
end
tls_socket.getopt = tls_socket.getsockopt
function tls_socket.setsockopt(self, ...)
return self.socket:setsockopt(...)
end
tls_socket.setopt = tls_socket.setsockopt
for k, v in pairs(meta) do
file[k] = v
socket[k] = v