commit
989b4c1d36
@ -1,7 +1,7 @@
|
|||||||
# [nodemcu-httpserver](https://github.com/marcoskirsch/nodemcu-httpserver)
|
# [nodemcu-httpserver](https://github.com/marcoskirsch/nodemcu-httpserver)
|
||||||
A (very) simple web server written in Lua for the ESP8266 running the NodeMCU firmware.
|
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
|
> 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
|
> 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?
|
* Need PUT example. How?
|
||||||
* Rename args.lua to get.lua, so it matches post.lua convention.
|
* Rename args.lua to get.lua, so it matches post.lua convention.
|
||||||
* How can I test the whole JSON post thing?
|
* 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>
|
<h3>Serve me some pages!</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="index.html">Index</a>: This page (static)</li>
|
<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">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="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>
|
<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
|
end
|
||||||
|
|
||||||
function newInstance:send(payload)
|
function newInstance:send(payload)
|
||||||
local l = payload:len()
|
local flushthreshold = 1400
|
||||||
if l + self.size > 1024 then
|
|
||||||
-- Send what we have buffered so far, not including payload.
|
local newsize = self.size + payload:len()
|
||||||
if self:flush() then
|
while newsize > flushthreshold do
|
||||||
coroutine.yield()
|
--STEP1: cut out piece from payload to complete threshold bytes in table
|
||||||
end
|
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
|
end
|
||||||
if l > 768 then
|
|
||||||
-- Payload is big. Send it now rather than buffering it for later.
|
--at this point, whatever is left in payload should be <= flushthreshold
|
||||||
self.connection:send(payload)
|
local plen = payload:len()
|
||||||
coroutine.yield()
|
if plen == flushthreshold then
|
||||||
else
|
--case 1: what is left in payload is exactly flushthreshold bytes (boundary case), so flush it
|
||||||
-- Payload is small. Save off payload for later sending.
|
table.insert(self.data, payload)
|
||||||
table.insert(self.data, payload)
|
self.size = self.size + plen
|
||||||
self.size = self.size + l
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
return function (connection, req, args)
|
return function (connection, req, args)
|
||||||
|
|
||||||
|
-- @TODO: would be nice to use httpserver-header.lua
|
||||||
local function getHeader(connection, code, errorString, extraHeaders, mimeType)
|
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"
|
local header = "HTTP/1.0 " .. code .. " " .. errorString .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\n"
|
||||||
for i, extraHeader in ipairs(extraHeaders) do
|
for i, extraHeader in ipairs(extraHeaders) do
|
||||||
@ -15,9 +16,7 @@ return function (connection, req, args)
|
|||||||
|
|
||||||
print("Error " .. args.code .. ": " .. args.errorString)
|
print("Error " .. args.code .. ": " .. args.errorString)
|
||||||
args.headers = args.headers or {}
|
args.headers = args.headers or {}
|
||||||
local html = getHeader(connection, args.code, args.errorString, args.headers, "text/html")
|
connection:send(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><head><title>" .. args.code .. " - " .. args.errorString .. "</title></head><body><h1>" .. args.code .. " - " .. args.errorString .. "</h1></body></html>\r\n")
|
||||||
connection:send(html)
|
|
||||||
html = nil
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@ -19,14 +19,11 @@ return function (connection, code, extension, isGzipped)
|
|||||||
|
|
||||||
local mimeType = getMimeType(extension)
|
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
|
if isGzipped then
|
||||||
header = header .. "Cache-Control: max-age=2592000\r\n"
|
connection:send("Cache-Control: max-age=2592000\r\nContent-Encoding: gzip\r\n")
|
||||||
header = header .. "Content-Encoding: gzip\r\n"
|
|
||||||
end
|
end
|
||||||
header = header .. "Connection: close\r\n\r\n"
|
connection:send("Connection: close\r\n\r\n")
|
||||||
connection:send(header)
|
|
||||||
header = nil
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -4,14 +4,18 @@
|
|||||||
|
|
||||||
return function (connection, req, args)
|
return function (connection, req, args)
|
||||||
--print("Begin sending:", args.file)
|
--print("Begin sending:", args.file)
|
||||||
|
--print("node.heap(): ", node.heap())
|
||||||
dofile("httpserver-header.lc")(connection, 200, args.ext, args.isGzipped)
|
dofile("httpserver-header.lc")(connection, 200, args.ext, args.isGzipped)
|
||||||
-- Send file in little chunks
|
-- Send file in little chunks
|
||||||
local continue = true
|
local continue = true
|
||||||
local size = file.list()[args.file]
|
local size = file.list()[args.file]
|
||||||
local bytesSent = 0
|
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
|
while continue do
|
||||||
collectgarbage()
|
collectgarbage()
|
||||||
|
|
||||||
-- NodeMCU file API lets you open 1 file at a time.
|
-- NodeMCU file API lets you open 1 file at a time.
|
||||||
-- So we need to open, seek, close each time in order
|
-- So we need to open, seek, close each time in order
|
||||||
-- to support multiple simultaneous clients.
|
-- to support multiple simultaneous clients.
|
||||||
|
|||||||
@ -9,17 +9,17 @@ return function (port)
|
|||||||
port,
|
port,
|
||||||
function (connection)
|
function (connection)
|
||||||
|
|
||||||
-- This variable holds the thread used for sending data back to the user.
|
-- 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 yield when sending lots
|
-- We do it in a separate thread because we need to send in little chunks and wait for the onSent event
|
||||||
-- of data in order to avoid overflowing the mcu's buffer.
|
-- before we can send more, or we risk overflowing the mcu's buffer.
|
||||||
local connectionThread
|
local connectionThread
|
||||||
|
|
||||||
local allowStatic = {GET=true, HEAD=true, POST=false, PUT=false, DELETE=false, TRACE=false, OPTIONS=false, CONNECT=false, PATCH=false}
|
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)
|
local function startServing(fileServeFunction, connection, req, args)
|
||||||
|
|
||||||
connectionThread = coroutine.create(function(fileServeFunction, bufferedConnection, req, args)
|
connectionThread = coroutine.create(function(fileServeFunction, bufferedConnection, req, args)
|
||||||
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
|
if not bufferedConnection:flush() then
|
||||||
connection:close()
|
connection:close()
|
||||||
connectionThread = nil
|
connectionThread = nil
|
||||||
@ -32,17 +32,14 @@ return function (port)
|
|||||||
if not status then
|
if not status then
|
||||||
print("Error: ", err)
|
print("Error: ", err)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function onRequest(connection, req)
|
local function handleRequest(connection, req)
|
||||||
collectgarbage()
|
collectgarbage()
|
||||||
local method = req.method
|
local method = req.method
|
||||||
local uri = req.uri
|
local uri = req.uri
|
||||||
local fileServeFunction = nil
|
local fileServeFunction = nil
|
||||||
|
|
||||||
--print("Method: " .. method);
|
|
||||||
|
|
||||||
if #(uri.file) > 32 then
|
if #(uri.file) > 32 then
|
||||||
-- nodemcu-firmware cannot handle long filenames.
|
-- nodemcu-firmware cannot handle long filenames.
|
||||||
uri.args = {code = 400, errorString = "Bad Request"}
|
uri.args = {code = 400, errorString = "Bad Request"}
|
||||||
@ -105,14 +102,14 @@ return function (port)
|
|||||||
|
|
||||||
-- parse payload and decide what to serve.
|
-- parse payload and decide what to serve.
|
||||||
local req = dofile("httpserver-request.lc")(payload)
|
local req = dofile("httpserver-request.lc")(payload)
|
||||||
print("Requested URI: " .. req.request)
|
print(req.method .. ": " .. req.request)
|
||||||
if conf.auth.enabled then
|
if conf.auth.enabled then
|
||||||
auth = dofile("httpserver-basicauth.lc")
|
auth = dofile("httpserver-basicauth.lc")
|
||||||
user = auth.authenticate(payload) -- authenticate returns nil on failed auth
|
user = auth.authenticate(payload) -- authenticate returns nil on failed auth
|
||||||
end
|
end
|
||||||
|
|
||||||
if user and req.methodIsValid and (req.method == "GET" or req.method == "POST" or req.method == "PUT") then
|
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
|
else
|
||||||
local args = {}
|
local args = {}
|
||||||
local fileServeFunction = dofile("httpserver-error.lc")
|
local fileServeFunction = dofile("httpserver-error.lc")
|
||||||
@ -145,14 +142,16 @@ return function (port)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
connection:on("receive", onReceive)
|
local function onDisconnect(connection, payload)
|
||||||
connection:on("sent", onSent)
|
|
||||||
connection:on("disconnection",function(c)
|
|
||||||
if connectionThread then
|
if connectionThread then
|
||||||
connectionThread = nil
|
connectionThread = nil
|
||||||
collectgarbage()
|
collectgarbage()
|
||||||
end
|
end
|
||||||
end)
|
end
|
||||||
|
|
||||||
|
connection:on("receive", onReceive)
|
||||||
|
connection:on("sent", onSent)
|
||||||
|
connection:on("disconnection", onDisconnect)
|
||||||
|
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user