Skip to content

Commit

Permalink
Added support for chunked transfer encoding
Browse files Browse the repository at this point in the history
  • Loading branch information
Björn Ritzl committed Mar 15, 2017
1 parent 7495586 commit aab32f8
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 33 deletions.
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,17 @@ Since it's possible to create a TCP socket it's also possible to build more adva

function init(self)
self.hs = http_server.create(PORT)
self.hs.router.get("/foo/(.*)$", function(what)
return self.http_server.html("boo" .. what)
self.hs.router.get("/foo/(.*)$", function(matches)
return self.http_server.html("boo" .. matches[1])
end)
self.hs.router.get("/$", function()
self.hs.router.get("^/$", function()
return self.http_server.html("Hello World")
end)
self.hs.router.get("^/stream$", function(matches, stream)
return function()
stream("some data")
end
end)
self.hs.router.unhandled(function(method, uri)
return self.http_server.html("Oops, couldn't find that one!", http_server.NOT_FOUND)
end)
Expand Down
92 changes: 65 additions & 27 deletions defnet/http_server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@
--
-- function init(self)
-- self.server = http_server.create(9190)
-- self.server.router.get("/", function()
-- self.server.router.get("^/$", function()
-- return http_server.html("Hello World")
-- end)
-- self.server.router.get("/foo", function()
-- return http_server.html("bar")
-- self.server.router.get("^/foo/(.*)$", function(matches)
-- return http_server.html("bar" .. matches[1])
-- end)
-- self.server.router.get("^/stream$", function(matches, stream)
-- return function()
-- stream("some data")
-- end
-- end)
-- self.server.router.unhandled(function(method, uri)
-- return http_server.html("Oops, couldn't find that one!", 404)
Expand Down Expand Up @@ -44,14 +49,16 @@ function M.create(port)
}

local routes = {}

local request_handlers = {}

local unhandled_route_fn = nil

local ss = tcp_server.create(port, function(data, ip)
local ss = tcp_server.create(port, function(data, ip, port, response_fn)
if not data or #data == 0 then
return
end
local ok, response_or_err = pcall(function()
local ok, err = pcall(function()
local request_line = data[1] or ""
local method, uri, protocol_version = request_line:match("^(%S+)%s(%S+)%s(%S+)")
local header_only = (method == "HEAD")
Expand All @@ -64,30 +71,34 @@ function M.create(port)
if not route.method or route.method == method then
local matches = { uri:match(route.pattern) }
if next(matches) then
response = route.fn(unpack(matches))
response = route.fn(matches, response_fn)
break
end
end
end
end
if not response and unhandled_route_fn then
response = unhandled_route_fn(method, uri)
response = unhandled_route_fn(method, uri, response_fn)
end
if response then
if type(response) == "function" then
table.insert(request_handlers, response)
else
response_fn(response)
end
end
--[[if response then
if method == "HEAD" then
local s, e = response:find("\r\n\r\n")
if s and e then
response = response:sub(1, e)
end
end
end
return response or ""
end--]]
end)
if not ok then
print(response_or_err)
return nil
print(err)
end
return response_or_err
end)

-- Replace the underlying socket server's receive function
Expand All @@ -111,21 +122,27 @@ function M.create(port)
instance.router = {}

--- Route HTTP GET requests matching a specific pattern to a
-- provided function. The function will receive any matches from
-- the pattern as it's arguments
-- TODO Add query arg handling
-- provided function.
-- The function will receive a list of matches from the pattern as
-- it's first arguments. The second argument is a stream function in case
-- the response should be streamed.
-- The function must either return the full response or a function that
-- can be called multiple times to get more data to return.
-- @param pattern Standard Lua pattern
-- @param fn Function to call
function instance.router.get(pattern, fn)
assert(pattern, "You must provide a route pattern")
assert(fn, "You must provide a route handler function")
table.insert(routes, { method = "GET", pattern = pattern, fn = fn })
end

--- Route HTTP POST requests matching a specific pattern to a
-- provided function. The function will receive any matches from
-- the pattern as it's arguments
-- TODO Add POST data handling
-- provided function.
-- The function will receive a list of matches from the pattern as
-- it's first arguments. The second argument is a stream function in case
-- the response should be streamed.
-- The function must either return the full response or a function that
-- can be called multiple times to get more data to return.
-- @param pattern Standard Lua pattern
-- @param fn Function to call
function instance.router.post(pattern, fn)
Expand All @@ -135,8 +152,12 @@ function M.create(port)
end

--- Route all HTTP requests matching a specific pattern to a
-- provided function. The function will receive any matches from
-- the pattern as it's arguments
-- provided function.
-- The function will receive a list of matches from the pattern as
-- it's first arguments. The second argument is a stream function in case
-- the response should be streamed.
-- The function must either return the full response or a function that
-- can be called multiple times to get more data to return.
-- @param pattern Standard Lua pattern
-- @param fn Function to call
function instance.router.all(pattern, fn)
Expand Down Expand Up @@ -170,17 +191,24 @@ function M.create(port)
--- Stop the server
function instance.update()
ss.update()
for k,handler in pairs(request_handlers) do
if not handler() then
request_handlers[k] = nil
end
end
end

--- Return a properly formatted HTML response with the
-- appropriate response headers set
-- If the document is omitted the response is assumed to be
-- chunked
instance.html = {
header = function(document, status)
local headers = {
"HTTP/1.1 " .. (status or M.OK),
instance.server_header,
"Content-Type: text/html",
"Content-Length: " .. tostring(#document)
document and ("Content-Length: " .. tostring(#document)) or "Transfer-Encoding: chunked",
}
if instance.access_control then
headers[#headers + 1] = "Access-Control-Allow-Origin: " .. instance.access_control
Expand All @@ -190,20 +218,22 @@ function M.create(port)
return table.concat(headers, "\r\n")
end,
response = function(document, status)
return instance.html.header(document, status) .. document
return instance.html.header(document, status) .. (document or "")
end
}
setmetatable(instance.html, { __call = function(_, document, status) return instance.html.response(document, status) end })

--- Returns a properly formatted JSON response with the
-- appropriate response headers set
-- If the JSON data is omitted the response is assumed to be
-- chunked
instance.json = {
header = function(json, status)
local headers = {
"HTTP/1.1 " .. (status or M.OK),
instance.server_header,
"Content-Type: application/json; charset=utf-8",
"Content-Length: " .. tostring(#json)
json and ("Content-Length: " .. tostring(#json)) or "Transfer-Encoding: chunked",
}
if instance.access_control then
headers[#headers + 1] = "Access-Control-Allow-Origin: " .. instance.access_control
Expand All @@ -213,21 +243,23 @@ function M.create(port)
return table.concat(headers, "\r\n")
end,
response = function(json, status)
return instance.json.header(json, status) .. json
return instance.json.header(json, status) .. (json or "")
end
}
setmetatable(instance.json, { __call = function(_, json, status) return instance.json.response(json, status) end })

--- Returns a properly formatted binary file response
-- with the appropriate headers set
-- If the file contents is omitted the response is assumed to be
-- chunked
instance.file = {
header = function(file, filename, status)
local headers = {
"HTTP/1.1 " .. (status or M.OK),
instance.server_header,
"Content-Type: application/octet-stream",
"Content-Disposition: attachment; filename=" .. filename,
"Content-Length: " .. tostring(#file),
file and ("Content-Length: " .. tostring(#file)) or "Transfer-Encoding: chunked",
}
if instance.access_control then
headers[#headers + 1] = "Access-Control-Allow-Origin: " .. instance.access_control
Expand All @@ -237,11 +269,17 @@ function M.create(port)
return table.concat(headers, "\r\n")
end,
response = function(file, filename, status)
return instance.file.header(file, filename, status) .. file
return instance.file.header(file, filename, status) .. (file or "")
end
}
setmetatable(instance.file, { __call = function(_, file, filename, status) return instance.file.response(file, filename, status) end })

--- Create a properly formatted chunk
-- @param data
-- @return chunk
function instance.to_chunk(data)
return ("%x\r\n%s\r\n"):format(#data, data)
end
return instance
end

Expand Down
8 changes: 7 additions & 1 deletion defnet/tcp_server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,13 @@ function M.create(port, on_data, on_client_connected, on_client_disconnected)
local data, err = server.receive(client)
if data and on_data then
local client_ip, client_port = client:getsockname()
local response = on_data(data, client_ip, client_port)
local response = on_data(data, client_ip, client_port, function(response)
if not queues[client] then
return false
end
queues[client].add(response)
return true
end)
if response then
queues[client].add(response)
end
Expand Down
15 changes: 13 additions & 2 deletions examples/basic/example.gui_script
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,19 @@ function init(self)
gui.set_enabled(self.send_to_clients, false)

self.http_server = http_server.create(HTTP_SERVER_PORT)
self.http_server.router.get("/foo/(.*)$", function(what)
return self.http_server.html("boo" .. what)
self.http_server.router.get("/foo/(.*)$", function(matches)
return self.http_server.html("boo" .. matches[1])
end)
self.http_server.router.get("/lorem$", function(matches, stream)
stream(self.http_server.html())
local chunks = { "Lorem ", "ipsum ", "dolor ", "sit ", "amet, ", "consectetur ", "adipiscing ", "elit." }
return function()
if #chunks > 0 then
return stream(self.http_server.to_chunk(table.remove(chunks, 1)))
else
return stream(self.http_server.to_chunk(""))
end
end
end)
self.http_server.router.get("/$", function()
return self.http_server.html("Hello World")
Expand Down

0 comments on commit aab32f8

Please sign in to comment.