commit
989b4c1d36
@ -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
|
||||
|
||||
1
TODO.md
1
TODO.md
@ -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
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
BIN
http/cars-lambo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
http/cars-mas.jpg
Normal file
BIN
http/cars-mas.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
http/cars-porsche.jpg
Normal file
BIN
http/cars-porsche.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
17
http/cars.html
Normal file
17
http/cars.html
Normal 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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user