* libs/httpd: Introduced keep-alive and pipelining support
[project/luci.git] / libs / httpd / luasrc / httpd.lua
index 773d3c8..a9b1ccb 100644 (file)
 --[[
-LuCI - HTTPD
+
+HTTP server implementation for LuCI - core
+(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$
+
 ]]--
-module("luci.httpd", package.seeall)
-require("luci.copas")
-require("luci.http.protocol")
-require("luci.sys")
 
+module("luci.httpd", package.seeall)
+require("socket")
+require("luci.util")
 
+function Socket(ip, port)
+       local sock, err = socket.bind( ip, port )
 
-function run(config)
-       -- TODO: process config
-       local server = socket.bind("0.0.0.0", 8080)
-       copas.addserver(server, spawnworker)
-       
-       while true do
-               copas.step()
+       if sock then
+               sock:settimeout( 0, "t" )
        end
+
+       return sock, err
 end
 
+Thread = luci.util.class()
 
-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
-       
+function Thread.__init__(self, socket, func)
+       self.socket  = socket
+       self.routine = coroutine.create(func)
+       self.stamp   = os.time()
+       self.waiting = false
+end
+
+function Thread.getidletime(self)
+       return os.difftime(os.time(), self.stamp)
+end
+
+function Thread.iswaiting(self)
+       return self.waiting
+end
+
+function Thread.receive(self, ...)
+       local chunk, err, part
+       self.waiting = true
        
-       local output = socket -- TODO: replace with streamwriter
+       repeat
+               coroutine.yield()
+               chunk, err, part = self.socket:receive(...)
+       until err ~= "timeout"
        
-       -- TODO: detect matching handler
-       local h = luci.httpd.FileHandler.SimpleHandler(luci.sys.libpath() .. "/httpd/httest")
-       h:process(request, output)
+       self.waiting = false
+       return chunk, err, part
 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
+function Thread.resume(self, ...)
+       return coroutine.resume(self.routine, self, ...)
 end
 
-function Response.addheader(self, key, value)
-       self.headers[key] = value
+function Thread.status(self)
+       return coroutine.status(self.routine)
 end
 
-function Response.setstatus(self, status)
-       self.status = status
+function Thread.touch(self)
+       self.stamp = os.time()
 end
 
-function Response.setsource(self, source)
-       self.sourceout = source
+Daemon = luci.util.class()
+
+function Daemon.__init__(self, threadlimit, timeout)
+       self.reading = {}
+       self.threads = {}
+       self.handler = {}
+       self.waiting = {}
+       self.threadc = 0
+       
+       setmetatable(self.waiting, {__mode = "v"})
+       
+       self.debug   = false
+       self.threadlimit = threadlimit
+       self.timeout = timeout or 0.1
 end
 
+function Daemon.dprint(self, msg)
+       if self.debug then
+               io.stderr:write("[daemon] " .. msg .. "\n")
+       end
+end
 
-Handler = luci.util.class()
-function Handler.__init__(self)
-       self.filter = {}
+function Daemon.register(self, sock, clhandler, errhandler)
+       table.insert( self.reading, sock )
+       self.handler[sock] = { clhandler = clhandler, errhandler = errhandler }
 end
 
-function Handler.addfilter(self, filter)
-       table.insert(self.filter, filter)
+function Daemon.run(self)
+       while true do
+               self:step()
+       end
 end
 
-function Handler.process(self, request, output)
-       -- TODO: Process input filters
+function Daemon.step(self)     
+       local input, output, err = socket.select( self.reading, nil, 0 )
+       local working = false
+
+       -- accept new connections
+       for i, connection in ipairs(input) do
+
+               local sock = connection:accept()
+               
+               if sock then
+                       -- check capacity
+                       if not self.threadlimit or self.threadc < self.threadlimit then
+                               
+                               if self.debug then
+                                       self:dprint("Accepted incoming connection from " .. sock:getpeername())
+                               end
+                               
+                               local t = Thread(sock, self.handler[connection].clhandler)
+                               self.threads[sock] = t
+                               self.threadc = self.threadc + 1
+       
+                               if self.debug then
+                                       self:dprint("Created " .. tostring(t))
+                               end
+       
+                       -- reject client
+                       else
+                               if self.debug then
+                                       self:dprint("Rejected incoming connection from " .. sock:getpeername())
+                               end
+       
+                               if self.handler[connection].errhandler then
+                                       self.handler[connection].errhandler( sock )
+                               end
+       
+                               sock:close()
+                       end
+               end
+       end
 
-       local response = self:handle(request)
+       -- create client handler
+       for sock, thread in pairs( self.threads ) do
+
+               -- reap dead clients
+               if thread:status() == "dead" then
+                       if self.debug then
+                               self:dprint("Completed " .. tostring(thread))
+                       end
+                       sock:close()
+                       self.threadc = self.threadc - 1
+                       self.threads[sock] = nil
+               -- resume working threads
+               elseif not thread:iswaiting() then
+                       if self.debug then
+                               self:dprint("Resuming " .. tostring(thread))
+                       end
+
+                       local stat, err = thread:resume()
+                       if stat then
+                               thread:touch()
+                               if not thread:iswaiting() then
+                                       working = true
+                               else
+                                       table.insert(self.waiting, sock)
+                               end
+                       end
+                       
+                       if self.debug then
+                               self:dprint(tostring(thread) .. " returned")
+                               if not stat then
+                                       self:dprint("Error in " .. tostring(thread) .. " " .. err)
+                               end
+                       end
+               end
+       end
        
-       -- TODO: Process output filters
+       -- check for data on waiting threads
+       input, output, err = socket.select( self.waiting, nil, 0 )
        
-       output:send("HTTP/1.0 " .. response.status .. " BLA\r\n")
-       for k, v in pairs(response.headers) do
-               output:send(k .. ": " .. v .. "\r\n")
+       for i, sock in ipairs(input) do         
+               self.threads[sock]:resume()
+               self.threads[sock]:touch()
+               
+               if not self.threads[sock]:iswaiting() then
+                       for i, s in ipairs(self.waiting) do
+                               if s == sock then
+                                       table.remove(self.waiting, i)
+                                       break
+                               end
+                       end
+                       if not working then
+                               working = true
+                       end
+               end
        end
        
-       output:send("\r\n")
-
-       for chunk in response.sourceout do
-               output:send(chunk)
+       if err == "timeout" and not working then
+               socket.sleep(self.timeout)
        end
 end
-