Nathaniel Wesley Filardo dcc1ea2a49 A generic fifo and fifosock wrapper, under telnet and http server (#2650)
* lua_modules/fifo: a generic queue & socket wrapper

One occasionally wants a generic fifo, so here's a plausible
implementation that's reasonably flexible in its usage.

One possible consumer of this is a variant of TerryE's two-level fifo
trick currently in the telnetd example.  Factor that out to fifosock for
more general use.

* lua_examples/telnet: use factored out fifosock

* lua_modules/http: improve implementation

Switch to fifosock for in-order sending and waiting for everything to be
sent before closing.

Fix header callback by moving the invocation of the handler higher

* fifosock: optimistically cork and delay tx

If we just pushed a little bit of data into a fifosock that had idled,
wait a tick (1 ms) before transmitting.  Hopefully, this means that
we let the rest of the system push more data in before we send the first
packet.  But in a high-throughput situation, where we are streaming data
without idling the fifo, there won't be any additional delay and we'll
coalesce during operation as usual.

The fifosocktest mocks up enough of tmr for this to run, but assumes
an arbitrarily slow processor. ;)
2019-02-16 13:51:40 +01:00

201 lines
6.2 KiB
Lua

------------------------------------------------------------------------------
-- HTTP server module
--
-- LICENCE: http://opensource.org/licenses/MIT
-- Vladimir Dronnikov <dronnikov@gmail.com>
------------------------------------------------------------------------------
local collectgarbage, tonumber, tostring = collectgarbage, tonumber, tostring
local http
do
------------------------------------------------------------------------------
-- request methods
------------------------------------------------------------------------------
local make_req = function(conn, method, url)
return {
conn = conn,
method = method,
url = url,
}
end
------------------------------------------------------------------------------
-- response methods
------------------------------------------------------------------------------
local make_res = function(csend, cfini)
local send = function(self, data, status)
-- TODO: req.send should take care of response headers!
if self.send_header then
csend("HTTP/1.1 ")
csend(tostring(status or 200))
-- TODO: real HTTP status code/name table
csend(" OK\r\n")
-- we use chunked transfer encoding, to not deal with Content-Length:
-- response header
self:send_header("Transfer-Encoding", "chunked")
-- TODO: send standard response headers, such as Server:, Date:
end
if data then
-- NB: no headers allowed after response body started
if self.send_header then
self.send_header = nil
-- end response headers
csend("\r\n")
end
-- chunked transfer encoding
csend(("%X\r\n"):format(#data))
csend(data)
csend("\r\n")
end
end
local send_header = function(self, name, value)
-- NB: quite a naive implementation
csend(name)
csend(": ")
csend(value)
csend("\r\n")
end
-- finalize request, optionally sending data
local finish = function(self, data, status)
-- NB: res.send takes care of response headers
if data then
self:send(data, status)
end
-- finalize chunked transfer encoding
csend("0\r\n\r\n")
-- close connection
cfini()
end
--
local res = { }
res.send_header = send_header
res.send = send
res.finish = finish
return res
end
------------------------------------------------------------------------------
-- HTTP parser
------------------------------------------------------------------------------
local http_handler = function(handler)
return function(conn)
local csend = (require "fifosock")(conn)
local cfini = function()
conn:on("receive", nil)
conn:on("disconnection", nil)
csend(function() conn:on("sent", nil) conn:close() end)
end
local req, res
local buf = ""
local method, url
local ondisconnect = function(conn)
collectgarbage("collect")
end
-- header parser
local cnt_len = 0
local onheader = function(conn, k, v)
-- TODO: look for Content-Type: header
-- to help parse body
-- parse content length to know body length
if k == "content-length" then
cnt_len = tonumber(v)
end
if k == "expect" and v == "100-continue" then
csend("HTTP/1.1 100 Continue\r\n")
end
-- delegate to request object
if req and req.onheader then
req:onheader(k, v)
end
end
-- body data handler
local body_len = 0
local ondata = function(conn, chunk)
-- feed request data to request handler
if not req or not req.ondata then return end
req:ondata(chunk)
-- NB: once length of seen chunks equals Content-Length:
-- onend(conn) is called
body_len = body_len + #chunk
-- print("-B", #chunk, body_len, cnt_len, node.heap())
if body_len >= cnt_len then
req:ondata()
end
end
local onreceive = function(conn, chunk)
-- merge chunks in buffer
if buf then
buf = buf .. chunk
else
buf = chunk
end
-- consume buffer line by line
while #buf > 0 do
-- extract line
local e = buf:find("\r\n", 1, true)
if not e then break end
local line = buf:sub(1, e - 1)
buf = buf:sub(e + 2)
-- method, url?
if not method then
local i
-- NB: just version 1.1 assumed
_, i, method, url = line:find("^([A-Z]+) (.-) HTTP/1.1$")
if method then
-- make request and response objects
req = make_req(conn, method, url)
res = make_res(csend, cfini)
end
-- spawn request handler
handler(req, res)
-- header line?
elseif #line > 0 then
-- parse header
local _, _, k, v = line:find("^([%w-]+):%s*(.+)")
-- header seems ok?
if k then
k = k:lower()
onheader(conn, k, v)
end
-- headers end
else
-- NB: we feed the rest of the buffer as starting chunk of body
ondata(conn, buf)
-- buffer no longer needed
buf = nil
-- NB: we explicitly reassign receive handler so that
-- next received chunks go directly to body handler
conn:on("receive", ondata)
-- parser done
break
end
end
end
conn:on("receive", onreceive)
conn:on("disconnection", ondisconnect)
end
end
------------------------------------------------------------------------------
-- HTTP server
------------------------------------------------------------------------------
local srv
local createServer = function(port, handler)
-- NB: only one server at a time
if srv then srv:close() end
srv = net.createServer(net.TCP, 15)
-- listen
srv:listen(port, http_handler(handler))
return srv
end
------------------------------------------------------------------------------
-- HTTP server methods
------------------------------------------------------------------------------
http = {
createServer = createServer,
}
end
return http