Merge pull request #60 from marcoskirsch/master

http-server
This commit is contained in:
Marcos 2016-05-04 16:10:31 -05:00
commit 989b4c1d36
13 changed files with 75 additions and 41 deletions

View File

@ -1,7 +1,7 @@
# [nodemcu-httpserver](https://github.com/marcoskirsch/nodemcu-httpserver)
A (very) simple web server written in Lua for the ESP8266 running the NodeMCU firmware.
[From the NodeMCU FAQ](https://nodemcu.readthedocs.org/en/dev/en/faq/#how-do-i-minimise-the-footprint-of-an-application):
[From the NodeMCU FAQ](https://nodemcu.readthedocs.org/en/dev/en/lua-developer-faq/#how-do-i-minimise-the-footprint-of-an-application):
> If you are trying to implement a user-interface or HTTP webserver in your ESP8266 then
> you are really abusing its intended purpose. When it comes to scoping your ESP8266

View File

@ -4,4 +4,3 @@
* Need PUT example. How?
* Rename args.lua to get.lua, so it matches post.lua convention.
* How can I test the whole JSON post thing?
* BufferedConnection seems too clever. Simplify by removing the buffering and only providing yielding. Worth it? Will make server slower.

BIN
http/cars-ferrari.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
http/cars-lambo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
http/cars-mas.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
http/cars-porsche.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

17
http/cars.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Nice cars</title>
</head>
<body>
<h1>Nice cars!</h1>
<p>This page loads "large" images of fancy cars. It is meant to serve as a stress test for nodemcu-httpserver.</p>
<figure><img src="cars-ferrari.jpg" /><figcaption>Ferrari</figcaption></figure>
<figure><img src="cars-lambo.jpg" /><figcaption>Lamborghini</figcaption></figure>
<figure><img src="cars-mas.jpg" /><figcaption>Maserati</figcaption></figure>
<figure><img src="cars-porsche.jpg" /><figcaption>Porsche</figcaption></figure>
</body>
</html>

View File

@ -20,6 +20,7 @@
<h3>Serve me some pages!</h3>
<ul>
<li><a href="index.html">Index</a>: This page (static)</li>
<li><a href="cars.html">Nice cars</a>: Stress test, loads 4 "large" images. Makes the chip panic and restart :( (static)</li>
<li><a href="zipped.html">Zipped</a>: File is actually saved as zipped.html.gz. A compressed file! (static but gzipped)</li>
<li><a href="zipped.html.gz">Zipped</a>: Same exact file as served above. Server is smart enough to treat the .gz extension correctly (static but gzipped)</li>
<li><a href="args.lua">Arguments</a>: Parses arguments passed in the URL and prints them. (Lua)</li>

View File

@ -25,21 +25,39 @@ function BufferedConnection:new(connection)
end
function newInstance:send(payload)
local l = payload:len()
if l + self.size > 1024 then
-- Send what we have buffered so far, not including payload.
if self:flush() then
coroutine.yield()
end
local flushthreshold = 1400
local newsize = self.size + payload:len()
while newsize > flushthreshold do
--STEP1: cut out piece from payload to complete threshold bytes in table
local piecesize = flushthreshold - self.size
local piece = payload:sub(1, piecesize)
payload = payload:sub(piecesize + 1, -1)
--STEP2: insert piece into table
table.insert(self.data, piece)
self.size = self.size + piecesize --size should be same as flushthreshold
--STEP3: flush entire table
if self:flush() then
coroutine.yield()
end
--at this point, size should be 0, because the table was just flushed
newsize = self.size + payload:len()
end
if l > 768 then
-- Payload is big. Send it now rather than buffering it for later.
self.connection:send(payload)
coroutine.yield()
else
-- Payload is small. Save off payload for later sending.
table.insert(self.data, payload)
self.size = self.size + l
--at this point, whatever is left in payload should be <= flushthreshold
local plen = payload:len()
if plen == flushthreshold then
--case 1: what is left in payload is exactly flushthreshold bytes (boundary case), so flush it
table.insert(self.data, payload)
self.size = self.size + plen
if self:flush() then
coroutine.yield()
end
elseif payload:len() then
--case 2: what is left in payload is less than flushthreshold, so just leave it in the table
table.insert(self.data, payload)
self.size = self.size + plen
--else, case 3: nothing left in payload, so do nothing
end
end

View File

@ -4,6 +4,7 @@
return function (connection, req, args)
-- @TODO: would be nice to use httpserver-header.lua
local function getHeader(connection, code, errorString, extraHeaders, mimeType)
local header = "HTTP/1.0 " .. code .. " " .. errorString .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\n"
for i, extraHeader in ipairs(extraHeaders) do
@ -15,9 +16,7 @@ return function (connection, req, args)
print("Error " .. args.code .. ": " .. args.errorString)
args.headers = args.headers or {}
local html = getHeader(connection, args.code, args.errorString, args.headers, "text/html")
html = html .. "<html><head><title>" .. args.code .. " - " .. args.errorString .. "</title></head><body><h1>" .. args.code .. " - " .. args.errorString .. "</h1></body></html>\r\n"
connection:send(html)
html = nil
connection:send(getHeader(connection, args.code, args.errorString, args.headers, "text/html"))
connection:send("<html><head><title>" .. args.code .. " - " .. args.errorString .. "</title></head><body><h1>" .. args.code .. " - " .. args.errorString .. "</h1></body></html>\r\n")
end

View File

@ -19,14 +19,11 @@ return function (connection, code, extension, isGzipped)
local mimeType = getMimeType(extension)
local header = "HTTP/1.0 " .. code .. " " .. getHTTPStatusString(code) .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\nnCache-Control: private, no-store\r\n"
connection:send("HTTP/1.0 " .. code .. " " .. getHTTPStatusString(code) .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\nnCache-Control: private, no-store\r\n")
if isGzipped then
header = header .. "Cache-Control: max-age=2592000\r\n"
header = header .. "Content-Encoding: gzip\r\n"
connection:send("Cache-Control: max-age=2592000\r\nContent-Encoding: gzip\r\n")
end
header = header .. "Connection: close\r\n\r\n"
connection:send(header)
header = nil
connection:send("Connection: close\r\n\r\n")
end

View File

@ -4,14 +4,18 @@
return function (connection, req, args)
--print("Begin sending:", args.file)
--print("node.heap(): ", node.heap())
dofile("httpserver-header.lc")(connection, 200, args.ext, args.isGzipped)
-- Send file in little chunks
local continue = true
local size = file.list()[args.file]
local bytesSent = 0
local chunkSize = 1024 -- @TODO: can chunkSize be larger?
-- Chunks larger than 1024 don't work.
-- https://github.com/nodemcu/nodemcu-firmware/issues/1075
local chunkSize = 1024
while continue do
collectgarbage()
-- NodeMCU file API lets you open 1 file at a time.
-- So we need to open, seek, close each time in order
-- to support multiple simultaneous clients.

View File

@ -9,17 +9,17 @@ return function (port)
port,
function (connection)
-- This variable holds the thread used for sending data back to the user.
-- We do it in a separate thread because we need to yield when sending lots
-- of data in order to avoid overflowing the mcu's buffer.
-- This variable holds the thread (actually a Lua coroutine) used for sending data back to the user.
-- We do it in a separate thread because we need to send in little chunks and wait for the onSent event
-- before we can send more, or we risk overflowing the mcu's buffer.
local connectionThread
local allowStatic = {GET=true, HEAD=true, POST=false, PUT=false, DELETE=false, TRACE=false, OPTIONS=false, CONNECT=false, PATCH=false}
local function startServing(fileServeFunction, connection, req, args)
connectionThread = coroutine.create(function(fileServeFunction, bufferedConnection, req, args)
fileServeFunction(bufferedConnection, req, args)
-- The bufferedConnection may still hold some data that hasn't been sent. Flush it before closing.
if not bufferedConnection:flush() then
connection:close()
connectionThread = nil
@ -32,17 +32,14 @@ return function (port)
if not status then
print("Error: ", err)
end
end
local function onRequest(connection, req)
local function handleRequest(connection, req)
collectgarbage()
local method = req.method
local uri = req.uri
local fileServeFunction = nil
--print("Method: " .. method);
if #(uri.file) > 32 then
-- nodemcu-firmware cannot handle long filenames.
uri.args = {code = 400, errorString = "Bad Request"}
@ -105,14 +102,14 @@ return function (port)
-- parse payload and decide what to serve.
local req = dofile("httpserver-request.lc")(payload)
print("Requested URI: " .. req.request)
print(req.method .. ": " .. req.request)
if conf.auth.enabled then
auth = dofile("httpserver-basicauth.lc")
user = auth.authenticate(payload) -- authenticate returns nil on failed auth
end
if user and req.methodIsValid and (req.method == "GET" or req.method == "POST" or req.method == "PUT") then
onRequest(connection, req)
handleRequest(connection, req)
else
local args = {}
local fileServeFunction = dofile("httpserver-error.lc")
@ -145,14 +142,16 @@ return function (port)
end
end
connection:on("receive", onReceive)
connection:on("sent", onSent)
connection:on("disconnection",function(c)
local function onDisconnect(connection, payload)
if connectionThread then
connectionThread = nil
collectgarbage()
end
end)
end
connection:on("receive", onReceive)
connection:on("sent", onSent)
connection:on("disconnection", onDisconnect)
end
)