diff --git a/lua_modules/http/http-example.lua b/lua_modules/http/http-example.lua new file mode 100644 index 00000000..bd7df187 --- /dev/null +++ b/lua_modules/http/http-example.lua @@ -0,0 +1,41 @@ +------------------------------------------------------------------------------ +-- HTTP server Hello world example +-- +-- LICENCE: http://opensource.org/licenses/MIT +-- Vladimir Dronnikov +------------------------------------------------------------------------------ +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) diff --git a/lua_modules/http/http.lua b/lua_modules/http/http.lua new file mode 100644 index 00000000..9c52ce45 --- /dev/null +++ b/lua_modules/http/http.lua @@ -0,0 +1,216 @@ +------------------------------------------------------------------------------ +-- HTTP server module +-- +-- LICENCE: http://opensource.org/licenses/MIT +-- Vladimir Dronnikov +------------------------------------------------------------------------------ +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