* Introducing LuCI HTTPD as testing environment
authorSteven Barth <steven@midlink.org>
Mon, 23 Jun 2008 18:17:02 +0000 (18:17 +0000)
committerSteven Barth <steven@midlink.org>
Mon, 23 Jun 2008 18:17:02 +0000 (18:17 +0000)
* Several coroutine-safety fixes

Makefile
libs/http/luasrc/http/protocol.lua
libs/httpd/host/runluci [new file with mode: 0755]
libs/httpd/luasrc/httpd.lua
libs/httpd/luasrc/httpd/handler/file.lua
libs/httpd/luasrc/httpd/handler/luci.lua [new file with mode: 0644]
libs/httpd/luasrc/httpd/module.lua
libs/httpd/luasrc/httpd/server.lua
libs/web/luasrc/dispatcher.lua
libs/web/luasrc/http.lua

index 796bb41..8ce59c2 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -34,9 +34,19 @@ hostcopy:
        rm -f host/luci
        ln -s .$(LUCI_MODULEDIR) host/luci
 
-run: host
+runboa: host
        libs/sgi-webuci/host/buildconfig.sh `pwd`/host  > host/etc/boa/boa.conf
        ./host/usr/bin/boa -c ./host/etc/boa -d
 
+runluci: luahost
+       libs/httpd/host/runluci host$(HTDOCS)
+
 hostclean: clean
        rm -rf host
+
+run:
+       #       make run is deprecated                  #
+       #       Please use:                             #
+       #                                               #
+       #       make runluci to use LuCI HTTPD          #
+       #       make runboa  to use Boa / Webuci        #
index 08e8ba2..94a7a82 100644 (file)
@@ -16,7 +16,6 @@ $Id$
 module("luci.http.protocol", package.seeall)
 
 require("ltn12")
-require("luci.util")
 require("luci.http.protocol.filter")
 
 HTTP_MAX_CONTENT      = 1024*4         -- 4 kB maximum content size
@@ -49,7 +48,7 @@ function urldecode_params( url, tbl )
                url = url:gsub( "^.+%?([^?]+)", "%1" )
        end
 
-       for i, pair in ipairs(luci.util.split( url, "[&;]+", nil, true )) do
+       for pair in url:gmatch( "[^&;]+" ) do
 
                -- find key and value
                local key = urldecode( pair:match("^([^=]+)")     )
@@ -501,7 +500,8 @@ process_states['urldecode-value'] = function( msg, chunk, filecb )
                        -- We're somewhere within a data section and our buffer is full
                        if #buffer > #chunk then
                                -- Flush buffered data
-                               msg._urldeccallback( buffer:sub( 1, #buffer - #chunk ), false )
+                               -- Send EOF if chunk is empty
+                               msg._urldeccallback( buffer:sub( 1, #buffer - #chunk ), ( #chunk == 0 ) )
 
                                -- Store new data
                                msg._urldeclength = msg._urldeclength + #buffer - #chunk
@@ -769,3 +769,34 @@ function parse_message_body( source, msg, filecb )
                end
        end
 end
+
+
+-- Push a response to a socket
+function push_response(request, response, sourceout, sinkout, sinkerr)
+       local code = response.status
+       sinkout(request.env.SERVER_PROTOCOL .. " " .. code .. " " .. statusmsg[code] .. "\r\n")
+
+       -- FIXME: Add support for keep-alive
+       response.headers["Connection"] = "close"
+
+       for k,v in pairs(response.headers) do
+               sinkout(k .. ": " .. v .. "\r\n")
+       end
+
+       sinkout("\r\n")
+
+       if sourceout then
+               ltn12.pump.all(sourceout, sinkout)
+       end
+end
+
+
+-- Status codes
+statusmsg = {
+       [200] = "OK",
+       [400] = "Bad Request",
+       [403] = "Forbidden",
+       [404] = "Not Found",
+       [500] = "Internal Server Error",
+       [503] = "Server Unavailable",
+}
\ No newline at end of file
diff --git a/libs/httpd/host/runluci b/libs/httpd/host/runluci
new file mode 100755 (executable)
index 0000000..c9c93dd
--- /dev/null
@@ -0,0 +1,32 @@
+#!/usr/bin/lua
+require("luci.httpd")
+require("luci.httpd.server")
+require("luci.httpd.handler.file")
+require("luci.httpd.handler.luci")
+
+DOCROOT = arg[1]
+PORT = 8080
+
+
+serversocket = luci.httpd.Socket("0.0.0.0", PORT)
+
+
+server  = luci.httpd.server.Server()
+vhost   = luci.httpd.server.VHost()
+
+server:set_default_vhost(vhost)
+
+
+filehandler = luci.httpd.handler.file.Simple(DOCROOT)
+vhost:set_default_handler(filehandler)
+
+lucihandler = luci.httpd.handler.luci.Luci()
+vhost:set_handler("/luci", lucihandler)
+
+io.stderr:write("Starting LuCI HTTPD on port " .. PORT .. "...\n")
+io.stderr:write("Point your browser to http://localhost:" .. PORT .. "/luci\n")
+
+daemon = luci.httpd.Daemon()
+daemon.debug = true
+daemon:register(serversocket, server:create_daemon_handlers())
+daemon:run()
index 8cd946f..82f1be9 100644 (file)
@@ -13,23 +13,41 @@ $Id$
 
 ]]--
 
-require("ltn12")
+module("luci.httpd", package.seeall)
 require("socket")
 require("luci.util")
 
+function Socket(ip, port)
+       local sock, err = socket.bind( ip, port )
+
+       if sock then
+               sock:settimeout( 0, "t" )
+       end
+
+       return sock, err
+end
+
 
 Daemon = luci.util.class()
 
-function Daemon.__init__(self, threadlimit)
+function Daemon.__init__(self, threadlimit, timeout)
        self.reading = {}
        self.running = {}
        self.handler = {}
+       self.debug   = false
        self.threadlimit = threadlimit
+       self.timeout = timeout or 0.1
 end
 
-function Daemon.register(self, socket, clhandler, errhandler)
-       table.insert( self.reading, socket )
-       self.handler[socket] = { clhandler = clhandler, errhandler = errhandler }
+function Daemon.dprint(self, msg)
+       if self.debug then
+               io.stderr:write("[daemon] " .. msg .. "\n")
+       end
+end
+
+function Daemon.register(self, sock, clhandler, errhandler)
+       table.insert( self.reading, sock )
+       self.handler[sock] = { clhandler = clhandler, errhandler = errhandler }
 end
 
 function Daemon.run(self)
@@ -39,7 +57,11 @@ function Daemon.run(self)
 end
 
 function Daemon.step(self)     
-       local input = socket.select( self.reading, nil, 0 )
+       local input, output, err = socket.select( self.reading, nil, 0 )
+
+       if err == "timeout" and #self.running == 0 then
+               socket.sleep(self.timeout)
+       end
 
        -- accept new connections
        for i, connection in ipairs(input) do
@@ -47,19 +69,25 @@ function Daemon.step(self)
                local sock = connection:accept()
 
                -- check capacity
-               if self.threadlimit and #running < self.threadlimit then
+               if not self.threadlimit or #self.running < self.threadlimit then
+
+                       self:dprint("Accepted incoming connection from " .. sock:getpeername())
 
                        table.insert( self.running, {
                                coroutine.create( self.handler[connection].clhandler ),
                                sock
                        } )
 
+                       self:dprint("Created " .. tostring(self.running[#self.running][1]))
+
                -- reject client
                else
+                       self:dprint("Rejected incoming connection from " .. sock:getpeername())
+
                        if self.handler[connection].errhandler then
                                self.handler[connection].errhandler( sock )
                        end
-                       
+
                        sock:close()
                end
        end
@@ -69,9 +97,18 @@ function Daemon.step(self)
 
                -- reap dead clients
                if coroutine.status( client[1] ) == "dead" then
+                       self:dprint("Completed " .. tostring(client[1]))
                        table.remove( self.running, i )
-               end
+               else
+                       self:dprint("Resuming " .. tostring(client[1]))
+
+                       local stat, err = coroutine.resume( client[1], client[2] )
+
+                       self:dprint(tostring(client[1]) .. " returned")
 
-               coroutine.resume( client[1], client[2] )
+                       if not stat then
+                               self:dprint("Error in " .. tostring(client[1]) .. " " .. err)
+                       end
+               end
        end
 end
index eb5aafd..83549f3 100644 (file)
@@ -12,11 +12,17 @@ function Simple.__init__(self, docroot)
 end
 
 function Simple.handle(self, request, sourcein, sinkerr)
-       local file = self.docroot .. request.env.REQUEST_URI:gsub("../", "")
-       local size = luci.fs.stat(file, "size")
-       if size then
-               return Response(200, {["Content-Length"] = size}), ltn12.source.file(io.open(file))
+       local uri  = request.env.PATH_INFO
+       local file = self.docroot .. uri:gsub("%.%./", "")
+       local stat = luci.fs.stat(file)
+
+       if stat then
+               if stat.type == "regular" then
+                       return Response(200, {["Content-Length"] = stat.size}), ltn12.source.file(io.open(file))
+               else
+                       return self:failure(403, "Unable to transmit " .. stat.type .. " " .. uri)
+               end
        else
-               return Response(404)
+               return self:failure(404, "No such file: " .. uri)
        end
 end
\ No newline at end of file
diff --git a/libs/httpd/luasrc/httpd/handler/luci.lua b/libs/httpd/luasrc/httpd/handler/luci.lua
new file mode 100644 (file)
index 0000000..e4916bd
--- /dev/null
@@ -0,0 +1,56 @@
+module("luci.httpd.handler.luci", package.seeall)
+require("luci.dispatcher")
+require("luci.http")
+require("ltn12")
+
+Luci = luci.util.class(luci.httpd.module.Handler)
+Response = luci.httpd.module.Response
+
+function Luci.__init__(self)
+       luci.httpd.module.Handler.__init__(self)
+end
+
+function Luci.handle(self, request, sourcein, sinkerr) 
+       local r = luci.http.Request(
+               request.env,
+               sourcein,
+               sinkerr
+       )
+               
+       local res, id, data1, data2 = true, 0, nil, nil
+       local headers = {}
+       local status = 200
+       
+       local x = coroutine.create(luci.dispatcher.httpdispatch)
+       while id < 3 do
+               coroutine.yield()
+               
+               res, id, data1, data2 = coroutine.resume(x, r)
+               
+               if not res then
+                       status = 500
+                       headers["Content-Type"] = "text/plain"
+                       local err = {id}
+                       return status, headers, function() local x = table.remove(err) return x end
+               end
+               
+               if id == 1 then
+                       status = data1
+               elseif id == 2 then
+                       headers[data1] = data2
+               end
+       end
+       
+       local function iter()
+               local res, id, data = coroutine.resume(x)
+               if not res then
+                       return nil, id
+               elseif id == 5 then
+                       return nil
+               else
+                       return data
+               end
+       end
+       
+       return Response(status, headers), iter
+end
\ No newline at end of file
index 28460a1..c321856 100644 (file)
@@ -34,15 +34,9 @@ end
 
 
 -- Creates a failure reply
-function Handler.failure(self, message)
-       response = {
-               status = 500,
-               headers = {
-                       ["Content-Type"] = "text/plain"
-               }
-       }
-       
-       sourceout = ltn12.source.string(message)
+function Handler.failure(self, code, message)
+       local response = Response(code, { ["Content-Type"] = "text/plain" })
+       local sourceout = ltn12.source.string(message)
        
        return response, sourceout 
 end
@@ -70,12 +64,12 @@ function Handler.process(self, request, sourcein, sinkout, sinkerr)
        
        -- Check for any errors
        if not stat then
-               response, sourceout = self:failure(response)
+               response, sourceout = self:failure(500, response)
        end
        
        -- Check data
        if not luci.util.instanceof(response, Response) then
-               response, sourceout = self:failure("Core error: Invalid module response!")
+               response, sourceout = self:failure(500, "Core error: Invalid module response!")
        end
        
        -- Process outgoing filters
@@ -131,12 +125,4 @@ end
 
 function Response.setstatus(self, status)
        self.status = status
-end
-
-
--- Status codes
-statusmsg = {
-       [200] = "OK",
-       [404] = "Not Found",
-       [500] = "Internal Server Error",
-}
\ No newline at end of file
+end
\ No newline at end of file
index 7f973ac..2bb44bd 100644 (file)
@@ -18,6 +18,7 @@ require("luci.util")
 
 READ_BUFSIZE = 1024
 
+
 VHost = luci.util.class()
 
 function VHost.__init__(self, handler)
@@ -25,76 +26,75 @@ function VHost.__init__(self, handler)
        self.dhandler = {}
 end
 
-function VHost.process(self, ...)
-       -- TODO: Dispatch handler
-end
-
-function VHost.sethandler(self, handler, match)
-       if match then
-               self.dhandler[match] = handler
-       else
-               self.handler = handler
-       end
-end
+function VHost.process(self, request, sourcein, sinkout, sinkerr)
+       local handler = self.handler
 
+       local uri = request.env.REQUEST_URI:match("^([^?]*)")
 
+       -- SCRIPT_NAME
+       request.env.SCRIPT_NAME = ""
 
-Server = luci.util.class()
+       -- Call URI part
+       request.env.PATH_INFO = uri
 
-function Server.__init__(self, ip, port, base)
-       self.socket = socket.bind(ip, port)
-       self.socket:settimeout(0, "t")
-       self.clhandler = client_handler
-       self.errhandler = error503
-       self.host = nil
-       self.vhosts = {}
-       
-       -- Clone another server
-       if base then
-               getmetatable(self).__index = base 
+       for k, dhandler in pairs(self.dhandler) do
+               if k == uri or k.."/" == uri:sub(1, #k+1) then
+                       handler = dhandler
+                       request.env.SCRIPT_NAME = k
+                       request.env.PATH_INFO   = uri:sub(#k+1)
+                       break;
+               end
        end
-end
 
--- Sets a vhost
-function Server.setvhost(self, vhost, name)
-       if name then
-               self.vhosts[name] = vhost
+       if handler then
+               handler:process(request, sourcein, sinkout, sinkerr)
+               return true
        else
-               self.host = vhost
+               return false
        end
 end
 
 
-function Server.error400(self, client, msg)
-       client:send( "HTTP/1.0 400 Bad request\r\n" )
-       client:send( "Content-Type: text/plain\r\n\r\n" )
+function VHost.set_default_handler(self, handler)
+       self.handler = handler
+end
 
-       if msg then
-               client:send( msg .. "\r\n" )
-       end
 
-       client:close()
+function VHost.set_handler(self, match, handler)
+       self.dhandler[match] = handler
 end
 
-function Server.error503(self, client)
-       client:send( "HTTP/1.0 503 Server unavailable\r\n" )
-       client:send( "Content-Type: text/plain\r\n\r\n" )
-       client:send( "There are too many clients connected, try again later\r\n" )
-end
 
-function Server.process(self, ...)
-       -- TODO: Dispatch vhost
+
+Server = luci.util.class()
+
+function Server.__init__(self, host)
+       self.clhandler = client_handler
+       self.errhandler = error503
+       self.host = host
+       self.vhosts = {}
 end
 
+function Server.set_default_vhost(self, vhost)
+       self.host = vhost
+end
 
-function Server.client_handler(self, client)
+-- Sets a vhost
+function Server.set_vhost(self, name, vhost)
+       self.vhosts[name] = vhost
+end
 
-       client:settimeout( 0 )
+function Server.create_daemon_handlers(self)
+       return function(...) return self:process(...) end,
+               function(...) return self:error503(...) end
+end
 
+function Server.create_client_sources(self, client)
        -- Create LTN12 block source
        local block_source = function()
 
-               coroutine.yield()
+               -- Yielding here may cause chaos in coroutine based modules, be careful
+               -- coroutine.yield()
 
                local chunk, err, part = client:receive( READ_BUFSIZE )
 
@@ -108,6 +108,7 @@ function Server.client_handler(self, client)
 
        end
 
+
        -- Create LTN12 line source
        local line_source = ltn12.source.simplify( function()
 
@@ -139,14 +140,55 @@ function Server.client_handler(self, client)
                end
        end )
 
-       coroutine.yield(client)
+       return block_source, line_source
+end
 
 
-       -- parse message
-       local message, err = luci.http.protocol.parse_message_header( line_source )
+function Server.error400(self, socket, msg)
+       socket:send( "HTTP/1.0 400 Bad request\r\n" )
+       socket:send( "Content-Type: text/plain\r\n\r\n" )
 
-       if message then
+       if msg then
+               socket:send( msg .. "\r\n" )
+       end
+
+       socket:close()
+end
+
+function Server.error500(self, socket, msg)
+       socket:send( "HTTP/1.0 500 Internal Server Error\r\n" )
+       socket:send( "Content-Type: text/plain\r\n\r\n" )
+
+       if msg then
+               socket:send( msg .. "\r\n" )
+       end
+
+       socket:close()
+end
+
+function Server.error503(self, socket)
+       socket:send( "HTTP/1.0 503 Server unavailable\r\n" )
+       socket:send( "Content-Type: text/plain\r\n\r\n" )
+       socket:send( "There are too many clients connected, try again later\r\n" )
+       socket:close()
+end
 
+
+function Server.process(self, client)
+
+       client:settimeout( 0 )
+       local sourcein, sourcehdr = self:create_client_sources(client)
+       local sinkerr = ltn12.sink.file(io.stderr)
+
+       -- FIXME: Add keep-alive support
+       local sinkout = socket.sink("close-when-done", client)
+
+       coroutine.yield()
+
+       -- parse headers
+       local message, err = luci.http.protocol.parse_message_header( sourcehdr )
+
+       if message then
                -- If we have a HTTP/1.1 client and an Expect: 100-continue header then
                -- respond with HTTP 100 Continue message
                if message.http_version == 1.1 and message.headers['Expect'] and
@@ -155,19 +197,18 @@ function Server.client_handler(self, client)
                        client:send("HTTP/1.1 100 Continue\r\n\r\n")
                end
 
-
-               local s, e = luci.http.protocol.parse_message_body( block_source, message )
-
-               -- XXX: debug
-               luci.util.dumptable( message )
-
-               if not s and e then
-                       self:error400( client, e )
+               local host = self.vhosts[message.env.HTTP_HOST] or self.host
+               if host then
+                       if host:process(message, sourcein, sinkout, sinkerr) then
+                               sinkout()
+                       else
+                               self:error500( client, "No suitable path handler found" )
+                       end
+               else
+                       self:error500( client, "No suitable host handler found" )
                end
        else
                self:error400( client, err )
+               return nil
        end
-
-       -- send response
-       self:error400( client, "Dummy response" )
 end
index f5894bf..6017874 100644 (file)
@@ -267,7 +267,7 @@ function createtree()
        luci.i18n.loadc("default")
        
        local scope = luci.util.clone(_G)
-       for k,v in pairs(_M) do
+       for k,v in pairs(luci.dispatcher) do
                if type(v) == "function" then
                        scope[k] = v
                end
@@ -276,10 +276,10 @@ function createtree()
        for k, v in pairs(index) do
                scope._NAME = k
                setfenv(v, scope)
-               
+
                local stat, err = luci.util.copcall(v)
                if not stat then
-                       error500(err)   
+                       error500("createtree failed: " .. k .. ": " .. err)
                        os.exit(1)
                end
        end
index 2bd9144..5263bfa 100644 (file)
@@ -53,13 +53,14 @@ function Request.__init__(self, env, sourcein, sinkerr)
        
        setmetatable(self.message.params, {__index =
                function(tbl, key)
+                       setmetatable(tbl, nil)
+
                        luci.http.protocol.parse_message_body(
                         self.input,
                         self.message,
                         self.filehandler
                        )
-                       
-                       setmetatable(tbl, nil)
+
                        return rawget(tbl, key)
                end
        })