* Added preliminary HTTPD construct
authorSteven Barth <steven@midlink.org>
Mon, 16 Jun 2008 19:47:57 +0000 (19:47 +0000)
committerSteven Barth <steven@midlink.org>
Mon, 16 Jun 2008 19:47:57 +0000 (19:47 +0000)
libs/httpd/Makefile [new file with mode: 0644]
libs/httpd/luasrc/copas.lua [new file with mode: 0644]
libs/httpd/luasrc/httpd.lua [new file with mode: 0644]
libs/httpd/luasrc/httpd/FileHandler.lua [new file with mode: 0644]
libs/web/luasrc/http/protocol.lua

diff --git a/libs/httpd/Makefile b/libs/httpd/Makefile
new file mode 100644 (file)
index 0000000..ee1a40e
--- /dev/null
@@ -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 (file)
index 0000000..262096a
--- /dev/null
@@ -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 (file)
index 0000000..773d3c8
--- /dev/null
@@ -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 (file)
index 0000000..1f3e948
--- /dev/null
@@ -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
index 970983d..6901291 100644 (file)
@@ -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