mirror of
https://github.com/nodemcu/nodemcu-firmware.git
synced 2025-02-06 21:18:25 +08:00
* 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. ;)
201 lines
6.2 KiB
Lua
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
|