Merge pull request #116 from dvv/master
http module draft, with example, by @dvv
This commit is contained in:
commit
b6a4030102
|
@ -0,0 +1,41 @@
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
-- HTTP server Hello world example
|
||||||
|
--
|
||||||
|
-- LICENCE: http://opensource.org/licenses/MIT
|
||||||
|
-- Vladimir Dronnikov <dronnikov@gmail.com>
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
require("http").createServer(80, function(req, res)
|
||||||
|
-- analyse method and url
|
||||||
|
print("+R", req.method, req.url)
|
||||||
|
-- setup handler of headers, if any
|
||||||
|
--[[
|
||||||
|
req.onheader = function(self, name, value)
|
||||||
|
-- print("+H", name, value)
|
||||||
|
-- E.g. look for "content-type" header,
|
||||||
|
-- setup body parser to particular format
|
||||||
|
-- if name == "content-type" then
|
||||||
|
-- if value == "application/json" then
|
||||||
|
-- req.ondata = function(self, chunk) ... end
|
||||||
|
-- elseif value == "application/x-www-form-urlencoded" then
|
||||||
|
-- req.ondata = function(self, chunk) ... end
|
||||||
|
-- end
|
||||||
|
-- end
|
||||||
|
end
|
||||||
|
-- setup handler of body, if any
|
||||||
|
req.ondata = function(self, chunk)
|
||||||
|
print("+B", chunk and #chunk, node.heap())
|
||||||
|
-- request ended?
|
||||||
|
if not chunk then
|
||||||
|
-- reply
|
||||||
|
--res:finish("")
|
||||||
|
res:send(nil, 200)
|
||||||
|
res:send_header("Connection", "close")
|
||||||
|
res:send("Hello, world!")
|
||||||
|
res:finish()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
]]
|
||||||
|
-- or just do something not waiting till body (if any) comes
|
||||||
|
--res:finish("Hello, world!")
|
||||||
|
res:finish("Salut, monde!")
|
||||||
|
end)
|
|
@ -0,0 +1,216 @@
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
-- 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)
|
||||||
|
local req = {
|
||||||
|
conn = conn,
|
||||||
|
method = method,
|
||||||
|
url = url,
|
||||||
|
}
|
||||||
|
-- return setmetatable(req, {
|
||||||
|
-- })
|
||||||
|
return req
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
-- response methods
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
local send = function(self, data, status)
|
||||||
|
local c = self.conn
|
||||||
|
-- TODO: req.send should take care of response headers!
|
||||||
|
if self.send_header then
|
||||||
|
c:send("HTTP/1.1 ")
|
||||||
|
c:send(tostring(status or 200))
|
||||||
|
-- TODO: real HTTP status code/name table
|
||||||
|
c:send(" 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
|
||||||
|
c:send("\r\n")
|
||||||
|
end
|
||||||
|
-- chunked transfer encoding
|
||||||
|
c:send(("%X\r\n"):format(#data))
|
||||||
|
c:send(data)
|
||||||
|
c:send("\r\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local send_header = function(self, name, value)
|
||||||
|
local c = self.conn
|
||||||
|
-- NB: quite a naive implementation
|
||||||
|
c:send(name)
|
||||||
|
c:send(": ")
|
||||||
|
c:send(value)
|
||||||
|
c:send("\r\n")
|
||||||
|
end
|
||||||
|
-- finalize request, optionally sending data
|
||||||
|
local finish = function(self, data, status)
|
||||||
|
local c = self.conn
|
||||||
|
-- NB: req.send takes care of response headers
|
||||||
|
if data then
|
||||||
|
self:send(data, status)
|
||||||
|
end
|
||||||
|
-- finalize chunked transfer encoding
|
||||||
|
c:send("0\r\n\r\n")
|
||||||
|
-- close connection
|
||||||
|
c:close()
|
||||||
|
end
|
||||||
|
--
|
||||||
|
local make_res = function(conn)
|
||||||
|
local res = {
|
||||||
|
conn = conn,
|
||||||
|
}
|
||||||
|
-- return setmetatable(res, {
|
||||||
|
-- send_header = send_header,
|
||||||
|
-- send = send,
|
||||||
|
-- finish = finish,
|
||||||
|
-- })
|
||||||
|
res.send_header = send_header
|
||||||
|
res.send = send
|
||||||
|
res.finish = finish
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
tmr.wdclr()
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
-- HTTP parser
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
local http_handler = function(handler)
|
||||||
|
return function(conn)
|
||||||
|
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
|
||||||
|
conn:send("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)
|
||||||
|
-- NB: do not reset node in case of lengthy requests
|
||||||
|
tmr.wdclr()
|
||||||
|
-- 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(conn)
|
||||||
|
end
|
||||||
|
-- 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
|
||||||
|
-- spawn request handler
|
||||||
|
-- NB: do not reset in case of lengthy requests
|
||||||
|
tmr.wdclr()
|
||||||
|
handler(req, res)
|
||||||
|
tmr.wdclr()
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
tmr.wdclr()
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
-- 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
|
Loading…
Reference in New Issue