From dfe85d7e518cc35a55e372b0ac31345788a486a8 Mon Sep 17 00:00:00 2001 From: Steven Barth Date: Mon, 16 Jun 2008 19:47:57 +0000 Subject: [PATCH] * Added preliminary HTTPD construct --- libs/httpd/Makefile | 13 + libs/httpd/luasrc/copas.lua | 439 ++++++++++++++++++++++++ libs/httpd/luasrc/httpd.lua | 84 +++++ libs/httpd/luasrc/httpd/FileHandler.lua | 24 ++ libs/web/luasrc/http/protocol.lua | 2 +- 5 files changed, 561 insertions(+), 1 deletion(-) create mode 100644 libs/httpd/Makefile create mode 100644 libs/httpd/luasrc/copas.lua create mode 100644 libs/httpd/luasrc/httpd.lua create mode 100644 libs/httpd/luasrc/httpd/FileHandler.lua diff --git a/libs/httpd/Makefile b/libs/httpd/Makefile new file mode 100644 index 000000000..ee1a40ea8 --- /dev/null +++ b/libs/httpd/Makefile @@ -0,0 +1,13 @@ +include ../../build/module.mk +include ../../build/config.mk +include ../../build/gccconfig.mk + +%.o: %.c + $(COMPILE) $(LUA_CFLAGS) $(FPIC) -c -o $@ $< + +compile: src/fastindex.o + mkdir -p dist$(LUCI_LIBRARYDIR) + $(LINK) $(SHLIB_FLAGS) -o dist$(LUCI_LIBRARYDIR)/fastindex.so src/fastindex.o $(LUA_SHLIBS) + +clean: + rm -f src/*.o diff --git a/libs/httpd/luasrc/copas.lua b/libs/httpd/luasrc/copas.lua new file mode 100644 index 000000000..262096ac0 --- /dev/null +++ b/libs/httpd/luasrc/copas.lua @@ -0,0 +1,439 @@ +------------------------------------------------------------------------------- +-- Copas - Coroutine Oriented Portable Asynchronous Services +-- +-- Offers a dispatcher and socket operations based on coroutines. +-- Usage: +-- copas.addserver(server, handler, timeout) +-- copas.addthread(thread, ...) Create a new coroutine thread and run it with args +-- copas.loop(timeout) - listens infinetely +-- copas.step(timeout) - executes one listening step +-- copas.receive(pattern or number) - receives data from a socket +-- copas.settimeout(client, time) if time=0 copas.receive(bufferSize) - receives partial data from a socket were data<=bufferSize +-- copas.send - sends data through a socket +-- copas.wrap - wraps a LuaSocket socket with Copas methods +-- copas.connect - blocks only the thread until connection completes +-- copas.flush - *deprecated* do nothing +-- +-- Authors: Andre Carregal and Javier Guerra +-- Contributors: Diego Nehab, Mike Pall, David Burgess, Leonardo Godinho, +-- Thomas Harning Jr. and Gary NG +-- +-- Copyright 2005 - Kepler Project (www.keplerproject.org) +-- +-- $Id: copas.lua,v 1.31 2008/05/19 18:57:13 carregal Exp $ +------------------------------------------------------------------------------- +local socket = require "socket" + +require"luci.util" +local copcall = luci.util.copcall + + +local WATCH_DOG_TIMEOUT = 120 + +-- Redefines LuaSocket functions with coroutine safe versions +-- (this allows the use of socket.http from within copas) +local function statusHandler(status, ...) + if status then return ... end + return nil, ... +end +function socket.protect(func) + return function (...) + return statusHandler(copcall(func, ...)) + end +end + +function socket.newtry(finalizer) + return function (...) + local status = (...) or false + if (status==false)then + copcall(finalizer, select(2, ...) ) + error((select(2, ...)), 0) + end + return ... + end +end +-- end of LuaSocket redefinitions + + +module ("copas", package.seeall) + +-- Meta information is public even if begining with an "_" +_COPYRIGHT = "Copyright (C) 2005 Kepler Project" +_DESCRIPTION = "Coroutine Oriented Portable Asynchronous Services" +_VERSION = "Copas 1.1.3" + +------------------------------------------------------------------------------- +-- Simple set implementation based on LuaSocket's tinyirc.lua example +-- adds a FIFO queue for each value in the set +------------------------------------------------------------------------------- +local function newset() + local reverse = {} + local set = {} + local q = {} + setmetatable(set, { __index = { + insert = function(set, value) + if not reverse[value] then + set[#set + 1] = value + reverse[value] = #set + end + end, + + remove = function(set, value) + local index = reverse[value] + if index then + reverse[value] = nil + local top = set[#set] + set[#set] = nil + if top ~= value then + reverse[top] = index + set[index] = top + end + end + end, + + push = function (set, key, itm) + local qKey = q[key] + if qKey == nil then + q[key] = {itm} + else + qKey[#qKey + 1] = itm + end + end, + + pop = function (set, key) + local t = q[key] + if t ~= nil then + local ret = table.remove (t, 1) + if t[1] == nil then + q[key] = nil + end + return ret + end + end + }}) + return set +end + +local _servers = newset() -- servers being handled +local _reading_log = {} +local _writing_log = {} + +local _reading = newset() -- sockets currently being read +local _writing = newset() -- sockets currently being written + +------------------------------------------------------------------------------- +-- Coroutine based socket I/O functions. +------------------------------------------------------------------------------- +-- reads a pattern from a client and yields to the reading set on timeouts +function receive(client, pattern, part) + local s, err + pattern = pattern or "*l" + repeat + s, err, part = client:receive(pattern, part) + if s or err ~= "timeout" then + _reading_log[client] = nil + return s, err, part + end + _reading_log[client] = os.time() + coroutine.yield(client, _reading) + until false +end + +-- same as above but with special treatment when reading chunks, +-- unblocks on any data received. +function receivePartial(client, pattern) + local s, err, part + pattern = pattern or "*l" + repeat + s, err, part = client:receive(pattern) + if s or ( (type(pattern)=="number") and part~="" and part ~=nil ) or + err ~= "timeout" then + _reading_log[client] = nil + return s, err, part + end + _reading_log[client] = os.time() + coroutine.yield(client, _reading) + until false +end + +-- sends data to a client. The operation is buffered and +-- yields to the writing set on timeouts +function send(client,data, from, to) + local s, err,sent + from = from or 1 + local lastIndex = from - 1 + + repeat + s, err, lastIndex = client:send(data, lastIndex + 1, to) + -- adds extra corrotine swap + -- garantees that high throuput dont take other threads to starvation + if (math.random(100) > 90) then + _writing_log[client] = os.time() + coroutine.yield(client, _writing) + end + if s or err ~= "timeout" then + _writing_log[client] = nil + return s, err,lastIndex + end + _writing_log[client] = os.time() + coroutine.yield(client, _writing) + until false +end + +-- waits until connection is completed +function connect(skt,host, port) + skt:settimeout(0) + local ret,err = skt:connect (host, port) + if ret or err ~= "timeout" then + return ret, err + end + _writing_log[skt] = os.time() + coroutine.yield(skt, _writing) + ret,err = skt:connect (host, port) + _writing_log[skt] = nil + if (err=="already connected") then + return 1 + end + return ret, err +end + +-- flushes a client write buffer (deprecated) +function flush(client) +end + +-- wraps a socket to use Copas methods (send, receive, flush and settimeout) +local _skt_mt = {__index = { + send = function (self, data, from, to) + return send (self.socket, data, from, to) + end, + + receive = function (self, pattern) + if (self.timeout==0) then + return receivePartial(self.socket, pattern) + end + return receive (self.socket, pattern) + end, + + flush = function (self) + return flush (self.socket) + end, + + settimeout = function (self,time) + self.timeout=time + return + end, +}} + +function wrap (skt) + return setmetatable ({socket = skt}, _skt_mt) +end + +-------------------------------------------------- +-- Error handling +-------------------------------------------------- + +local _errhandlers = {} -- error handler per coroutine + +function setErrorHandler (err) + local co = coroutine.running() + if co then + _errhandlers [co] = err + end +end + +local function _deferror (msg, co, skt) + print (msg, co, skt) +end + +------------------------------------------------------------------------------- +-- Thread handling +------------------------------------------------------------------------------- + +local function _doTick (co, skt, ...) + if not co then return end + + local ok, res, new_q = coroutine.resume(co, skt, ...) + + if ok and res and new_q then + new_q:insert (res) + new_q:push (res, co) + else + if not ok then copcall (_errhandlers [co] or _deferror, res, co, skt) end + if skt then skt:close() end + _errhandlers [co] = nil + end +end + +-- accepts a connection on socket input +local function _accept(input, handler) + local client = input:accept() + if client then + client:settimeout(0) + local co = coroutine.create(handler) + _doTick (co, client) + --_reading:insert(client) + end + return client +end + +-- handle threads on a queue +local function _tickRead (skt) + _doTick (_reading:pop (skt), skt) +end + +local function _tickWrite (skt) + _doTick (_writing:pop (skt), skt) +end + +------------------------------------------------------------------------------- +-- Adds a server/handler pair to Copas dispatcher +------------------------------------------------------------------------------- +function addserver(server, handler, timeout) + server:settimeout(timeout or 0.1) + _servers[server] = handler + _reading:insert(server) +end + +------------------------------------------------------------------------------- +-- Adds an new courotine thread to Copas dispatcher +------------------------------------------------------------------------------- +function addthread(thread, ...) + local co = coroutine.create(thread) + _doTick (co, nil, ...) +end + +------------------------------------------------------------------------------- +-- tasks registering +------------------------------------------------------------------------------- + +local _tasks = {} + +local function addtaskRead (tsk) + -- lets tasks call the default _tick() + tsk.def_tick = _tickRead + + _tasks [tsk] = true +end + +local function addtaskWrite (tsk) + -- lets tasks call the default _tick() + tsk.def_tick = _tickWrite + + _tasks [tsk] = true +end + +local function tasks () + return next, _tasks +end + +------------------------------------------------------------------------------- +-- main tasks: manage readable and writable socket sets +------------------------------------------------------------------------------- +-- a task to check ready to read events +local _readable_t = { + events = function(self) + local i = 0 + return function () + i = i + 1 + return self._evs [i] + end + end, + + tick = function (self, input) + local handler = _servers[input] + if handler then + input = _accept(input, handler) + else + _reading:remove (input) + self.def_tick (input) + end + end +} + +addtaskRead (_readable_t) + + +-- a task to check ready to write events +local _writable_t = { + events = function (self) + local i = 0 + return function () + i = i+1 + return self._evs [i] + end + end, + + tick = function (self, output) + _writing:remove (output) + self.def_tick (output) + end +} + +addtaskWrite (_writable_t) + +local last_cleansing = 0 +local function _select (timeout) + local err + local readable={} + local writable={} + local r={} + local w={} + local now = os.time() + local duration = os.difftime + + + _readable_t._evs, _writable_t._evs, err = socket.select(_reading, _writing, timeout) + local r_evs, w_evs = _readable_t._evs, _writable_t._evs + + if duration(now, last_cleansing) > WATCH_DOG_TIMEOUT then + last_cleansing = now + for k,v in pairs(_reading_log) do + if not r_evs[k] and duration(now, v) > WATCH_DOG_TIMEOUT then + _reading_log[k] = nil + r_evs[#r_evs + 1] = k + r_evs[k] = #r_evs + end + end + + for k,v in pairs(_writing_log) do + if not w_evs[k] and duration(now, v) > WATCH_DOG_TIMEOUT then + _writing_log[k] = nil + w_evs[#w_evs + 1] = k + w_evs[k] = #w_evs + end + end + end + + if err == "timeout" and #r_evs + #w_evs > 0 then return nil + else return err end +end + + +------------------------------------------------------------------------------- +-- Dispatcher loop step. +-- Listen to client requests and handles them +------------------------------------------------------------------------------- +function step(timeout) + local err = _select (timeout) + if err == "timeout" then return end + + if err then + error(err) + end + + for tsk in tasks() do + for ev in tsk:events () do + tsk:tick (ev) + end + end +end + +------------------------------------------------------------------------------- +-- Dispatcher endless loop. +-- Listen to client requests and handles them forever +------------------------------------------------------------------------------- +function loop(timeout) + while true do + step(timeout) + end +end diff --git a/libs/httpd/luasrc/httpd.lua b/libs/httpd/luasrc/httpd.lua new file mode 100644 index 000000000..773d3c873 --- /dev/null +++ b/libs/httpd/luasrc/httpd.lua @@ -0,0 +1,84 @@ +--[[ +LuCI - HTTPD +]]-- +module("luci.httpd", package.seeall) +require("luci.copas") +require("luci.http.protocol") +require("luci.sys") + + + +function run(config) + -- TODO: process config + local server = socket.bind("0.0.0.0", 8080) + copas.addserver(server, spawnworker) + + while true do + copas.step() + end +end + + +function spawnworker(socket) + socket = copas.wrap(socket) + local request = luci.http.protocol.parse_message_header(socket) + request.input = socket -- TODO: replace with streamreader + request.error = io.stderr + + + local output = socket -- TODO: replace with streamwriter + + -- TODO: detect matching handler + local h = luci.httpd.FileHandler.SimpleHandler(luci.sys.libpath() .. "/httpd/httest") + h:process(request, output) +end + + +Response = luci.util.class() +function Response.__init__(self, sourceout, headers, status) + self.sourceout = sourceout or function() end + self.headers = headers or {} + self.status = status or 200 +end + +function Response.addheader(self, key, value) + self.headers[key] = value +end + +function Response.setstatus(self, status) + self.status = status +end + +function Response.setsource(self, source) + self.sourceout = source +end + + +Handler = luci.util.class() +function Handler.__init__(self) + self.filter = {} +end + +function Handler.addfilter(self, filter) + table.insert(self.filter, filter) +end + +function Handler.process(self, request, output) + -- TODO: Process input filters + + local response = self:handle(request) + + -- TODO: Process output filters + + output:send("HTTP/1.0 " .. response.status .. " BLA\r\n") + for k, v in pairs(response.headers) do + output:send(k .. ": " .. v .. "\r\n") + end + + output:send("\r\n") + + for chunk in response.sourceout do + output:send(chunk) + end +end + diff --git a/libs/httpd/luasrc/httpd/FileHandler.lua b/libs/httpd/luasrc/httpd/FileHandler.lua new file mode 100644 index 000000000..1f3e94802 --- /dev/null +++ b/libs/httpd/luasrc/httpd/FileHandler.lua @@ -0,0 +1,24 @@ +module("luci.httpd.FileHandler", package.seeall) +require("luci.util") +require("luci.fs") +require("ltn12") + +SimpleHandler = luci.util.class(luci.httpd.Handler) + +function SimpleHandler.__init__(self, docroot) + luci.httpd.Handler.__init__(self) + self.docroot = docroot +end + +function SimpleHandler.handle(self, request) + local response = luci.httpd.Response() + local f = self.docroot .. "/" .. request.request_uri:gsub("%.%./", "") + request.error:write("Requested " .. f .. "\n") + local s = luci.fs.stat(f, "size") + if s then + response:addheader("Content-Length", s) + response:setsource(ltn12.source.file(io.open(f))) + else + response:setstatus(404) + end +end \ No newline at end of file diff --git a/libs/web/luasrc/http/protocol.lua b/libs/web/luasrc/http/protocol.lua index 970983d5b..6901291b9 100644 --- a/libs/web/luasrc/http/protocol.lua +++ b/libs/web/luasrc/http/protocol.lua @@ -517,7 +517,7 @@ function _linereader( obj, bufsz ) __read = function() return obj:sub( _pos, _pos + bufsz - #_buf - 1 ) end -- object implements a receive() or read() function - elseif type(obj) == "userdata" and ( type(obj.receive) == "function" or type(obj.read) == "function" ) then + elseif (type(obj) == "userdata" or type(obj) == "table") and ( type(obj.receive) == "function" or type(obj.read) == "function" ) then if type(obj.read) == "function" then __read = function() return obj:read( bufsz - #_buf ) end -- 2.25.1