Merge pull request #75 from marcoskirsch/master

Merge master to dev
This commit is contained in:
Marcos 2017-01-27 21:59:05 -06:00 committed by GitHub
commit 189e4a5c56
13 changed files with 191 additions and 160 deletions

View File

@ -5,7 +5,9 @@
NODEMCU-UPLOADER=../nodemcu-uploader/nodemcu-uploader.py NODEMCU-UPLOADER=../nodemcu-uploader/nodemcu-uploader.py
# Serial port # Serial port
PORT=/dev/cu.SLAB_USBtoUART PORT=/dev/cu.SLAB_USBtoUART
SPEED=9600 SPEED=115200
NODEMCU-COMMAND=$(NODEMCU-UPLOADER) -b $(SPEED) --start_baud $(SPEED) -p $(PORT) upload
###################################################################### ######################################################################
# End of user config # End of user config
@ -33,17 +35,17 @@ usage:
# Upload one files only # Upload one files only
upload: upload:
@python $(NODEMCU-UPLOADER) -b $(SPEED) -p $(PORT) upload $(FILE) @python $(NODEMCU-COMMAND) $(FILE)
# Upload HTTP files only # Upload HTTP files only
upload_http: $(HTTP_FILES) upload_http: $(HTTP_FILES)
@python $(NODEMCU-UPLOADER) -b $(SPEED) -p $(PORT) upload $(foreach f, $^, $(f)) @python $(NODEMCU-COMMAND) $(foreach f, $^, $(f))
# Upload httpserver lua files (init and server module) # Upload httpserver lua files (init and server module)
upload_server: $(LUA_FILES) upload_server: $(LUA_FILES)
@python $(NODEMCU-UPLOADER) -b $(SPEED) -p $(PORT) upload $(foreach f, $^, $(f)) @python $(NODEMCU-COMMAND) $(foreach f, $^, $(f))
# Upload all # Upload all
upload_all: $(LUA_FILES) $(HTTP_FILES) upload_all: $(LUA_FILES) $(HTTP_FILES)
@python $(NODEMCU-UPLOADER) -b $(SPEED) -p $(PORT) upload $(foreach f, $^, $(f)) @python $(NODEMCU-COMMAND) $(foreach f, $^, $(f))

View File

@ -65,12 +65,13 @@ Let the abuse begin.
4. How to use HTTP Basic Authentication. 4. How to use HTTP Basic Authentication.
Enable and configure HTTP Basic Authentication by editing "httpserver-conf.lua" file. Modify variables in configuration file httpserver-conf.lua in order to enable and to configure usernames/passwords.
See comments in that file for more details.
When enabled, HTTP Basic Authentication is global to every file served by the server. When enabled, HTTP Basic Authentication is global to every file served by the server.
Remember that HTTP Basic Authentication is a very basic authentication protocol, and should not be Remember that HTTP Basic Authentication is a very basic authentication protocol, and should not be
considered secure if the server is not using encryption, as your username and password travel considered as secure since the server is not using encryption. Username and passwords travel
in plain text. in plain text.
## How to use server-side scripting using your own Lua scripts ## How to use server-side scripting using your own Lua scripts

View File

@ -1,17 +1,21 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en"> <html lang="en">
<head> <head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"> <meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Nice cars</title> <title>Nice cars</title>
</head> </head>
<body> <body>
<h1>Nice cars!</h1> <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> <p>
<figure><img src="cars-ferrari.jpg" /><figcaption>Ferrari</figcaption></figure> This page loads "large" images of fancy cars. It is meant to serve as a stress test for nodemcu-httpserver.<br>
<figure><img src="cars-lambo.jpg" /><figcaption>Lamborghini</figcaption></figure> It works with three embedded images of cars, but the server crashes with four. Edit this file and try it yourself.<br>
<figure><img src="cars-mas.jpg" /><figcaption>Maserati</figcaption></figure> Whoever manages to modify nodemcu-httpserver to load all four images without crashing wins a prize!
<figure><img src="cars-porsche.jpg" /><figcaption>Porsche</figcaption></figure> </p>
</body> <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> </html>

View File

@ -7,12 +7,16 @@ return function (connection, req, args)
<h1>Server File Listing</h1> <h1>Server File Listing</h1>
]===]) ]===])
local remaining, used, total=file.fsinfo() local remaining, used, total = file.fsinfo()
connection:send("<b>Total size: </b> " .. total .. " bytes<br/>\n" .. connection:send("<b>Total size: </b> " .. total .. " bytes<br/>\n" ..
"<b>In Use: </b> " .. used .. " bytes<br/>\n" .. "<b>In Use: </b> " .. used .. " bytes<br/>\n" ..
"<b>Free: </b> " .. remaining .. " bytes<br/>\n" .. "<b>Free: </b> " .. remaining .. " bytes<br/>\n")
"<p>\n<b>Files:</b><br/>\n<ul>\n")
local flashAddress, flashSize = file.fscfg ()
connection:send("<b>Flash Address: </b> " .. flashAddress .. " bytes<br/>\n" ..
"<b>Flash Size: </b> " .. flashSize .. " bytes<br/>\n")
connection:send("<p>\n<b>Files:</b><br/>\n<ul>\n")
for name, size in pairs(file.list()) do for name, size in pairs(file.list()) do
local isHttpFile = string.match(name, "(http/)") ~= nil local isHttpFile = string.match(name, "(http/)") ~= nil
if isHttpFile then if isHttpFile then

View File

@ -20,7 +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="cars.html">Nice cars</a>: Stress test, loads several "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>

View File

@ -1,33 +1,44 @@
#!/usr/local/bin/lua #!/usr/local/bin/lua
-- httpserver-b64decode.lua -- httpserver-b64decode.lua
-- Part of nodemcu-httpserver, contains b64 decoding used for HTTP Basic Authentication. -- Part of nodemcu-httpserver, contains b64 decoding used for HTTP Basic Authentication.
-- Modified to use an exponentiation by multiplication method for only applicable for unsigned integers.
-- Based on http://lua-users.org/wiki/BaseSixtyFour by Alex Kloss -- Based on http://lua-users.org/wiki/BaseSixtyFour by Alex Kloss
-- compatible with lua 5.1 -- compatible with lua 5.1
-- http://www.it-rfc.de -- http://www.it-rfc.de
-- Author: Marcos Kirsch -- Author: Marcos Kirsch
local function uipow(a, b)
local ret = 1
if b >= 0 then
for i = 1, b do
ret = ret * a
end
end
return ret
end
-- bitshift functions (<<, >> equivalent) -- bitshift functions (<<, >> equivalent)
-- shift left -- shift left
local function lsh(value,shift) local function lsh(value,shift)
return (value*(2^shift)) % 256 return (value*(uipow(2, shift))) % 256
end end
-- shift right -- shift right
local function rsh(value,shift) local function rsh(value,shift)
-- Lua builds with no floating point don't define math. -- Lua builds with no floating point don't define math.
if math then return math.floor(value/2^shift) % 256 end if math then return math.floor(value/uipow(2, shift)) % 256 end
return (value/2^shift) % 256 return (value/uipow(2, shift)) % 256
end end
-- return single bit (for OR) -- return single bit (for OR)
local function bit(x,b) local function bit(x,b)
return (x % 2^b - x % 2^(b-1) > 0) return (x % uipow(2, b) - x % uipow(2, (b-1)) > 0)
end end
-- logic OR for number values -- logic OR for number values
local function lor(x,y) local function lor(x,y)
result = 0 result = 0
for p=1,8 do result = result + (((bit(x,p) or bit(y,p)) == true) and 2^(p-1) or 0) end for p=1,8 do result = result + (((bit(x,p) or bit(y,p)) == true) and uipow(2, (p-1)) or 0) end
return result return result
end end
@ -62,4 +73,3 @@ return function(data)
end end
return result return result
end end

View File

@ -4,6 +4,16 @@
basicAuth = {} basicAuth = {}
-- Returns true if the user/password match one of the users/passwords in httpserver-conf.lua.
-- Returns false otherwise.
function loginIsValid(user, pwd, users)
if user == nil then return false end
if pwd == nil then return false end
if users[user] == nil then return false end
if users[user] ~= pwd then return false end
return true
end
-- Parse basic auth http header. -- Parse basic auth http header.
-- Returns the username if header contains valid credentials, -- Returns the username if header contains valid credentials,
-- nil otherwise. -- nil otherwise.
@ -15,12 +25,13 @@ function basicAuth.authenticate(header)
end end
local credentials = dofile("httpserver-b64decode.lc")(credentials_enc) local credentials = dofile("httpserver-b64decode.lc")(credentials_enc)
local user, pwd = credentials:match("^(.*):(.*)$") local user, pwd = credentials:match("^(.*):(.*)$")
if user ~= conf.auth.user or pwd ~= conf.auth.password then if loginIsValid(user, pwd, conf.auth.users) then
print("httpserver-basicauth: User \"" .. user .. "\": Authenticated.")
return user
else
print("httpserver-basicauth: User \"" .. user .. "\": Access denied.") print("httpserver-basicauth: User \"" .. user .. "\": Access denied.")
return nil return nil
end end
print("httpserver-basicauth: User \"" .. user .. "\": Authenticated.")
return user
end end
function basicAuth.authErrorHeader() function basicAuth.authErrorHeader()

View File

@ -4,12 +4,14 @@
local conf = {} local conf = {}
-- Basic Authentication Conf -- Configure Basic HTTP Authentication.
local auth = {} local auth = {}
-- Set to true if you want to enable.
auth.enabled = false auth.enabled = false
auth.realm = "nodemcu-httpserver" -- displayed in the login dialog users get -- Displayed in the login dialog users see before authenticating.
auth.user = "user" auth.realm = "nodemcu-httpserver"
auth.password = "password" -- PLEASE change this -- Add users and passwords to this table. Do not leave this unchanged if you enable authentication!
conf.auth = auth auth.users = {user1 = "password1", user2 = "password2", user3 = "password3"}
conf.auth = auth
return conf return conf

View File

@ -14,6 +14,7 @@ function BufferedConnection:new(connection)
newInstance.size = 0 newInstance.size = 0
newInstance.data = {} newInstance.data = {}
-- Returns true if there was any data to be sent.
function newInstance:flush() function newInstance:flush()
if self.size > 0 then if self.size > 0 then
self.connection:send(table.concat(self.data, "")) self.connection:send(table.concat(self.data, ""))
@ -25,43 +26,34 @@ function BufferedConnection:new(connection)
end end
function newInstance:send(payload) function newInstance:send(payload)
local flushthreshold = 1400 local flushThreshold = 1400
local newSize = self.size + payload:len()
local newsize = self.size + payload:len() while newSize >= flushThreshold do
while newsize > flushthreshold do --STEP1: cut out piece from payload to complete threshold bytes in table
--STEP1: cut out piece from payload to complete threshold bytes in table local pieceSize = flushThreshold - self.size
local piecesize = flushthreshold - self.size local piece = payload:sub(1, pieceSize)
local piece = payload:sub(1, piecesize) payload = payload:sub(pieceSize + 1, -1)
payload = payload:sub(piecesize + 1, -1) --STEP2: insert piece into table
--STEP2: insert piece into table table.insert(self.data, piece)
table.insert(self.data, piece) piece = nil
self.size = self.size + piecesize --size should be same as flushthreshold self.size = self.size + pieceSize --size should be same as flushThreshold
--STEP3: flush entire table --STEP3: flush entire table
if self:flush() then if self:flush() then
coroutine.yield() coroutine.yield()
end end
--at this point, size should be 0, because the table was just flushed --at this point, size should be 0, because the table was just flushed
newsize = self.size + payload:len() newSize = self.size + payload:len()
end end
--at this point, whatever is left in payload should be <= flushthreshold --at this point, whatever is left in payload should be < flushThreshold
local plen = payload:len() if payload:len() ~= 0 then
if plen == flushthreshold then --leave remaining data in the table
--case 1: what is left in payload is exactly flushthreshold bytes (boundary case), so flush it table.insert(self.data, payload)
table.insert(self.data, payload) self.size = self.size + payload:len()
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
end end
return newInstance return newInstance
end end
return BufferedConnection return BufferedConnection

View File

@ -13,15 +13,16 @@ return function(connection, code, extension, isGzipped)
local function getMimeType(ext) local function getMimeType(ext)
-- A few MIME types. Keep list short. If you need something that is missing, let's add it. -- A few MIME types. Keep list short. If you need something that is missing, let's add it.
local mt = { css = "text/css", gif = "image/gif", html = "text/html", ico = "image/x-icon", jpeg = "image/jpeg", jpg = "image/jpeg", js = "application/javascript", json = "application/json", png = "image/png", xml = "text/xml" } local mt = {css = "text/css", gif = "image/gif", html = "text/html", ico = "image/x-icon", jpeg = "image/jpeg",
jpg = "image/jpeg", js = "application/javascript", json = "application/json", png = "image/png", xml = "text/xml"}
if mt[ext] then return mt[ext] else return "text/plain" end if mt[ext] then return mt[ext] else return "text/plain" end
end end
local mimeType = getMimeType(extension) local mimeType = getMimeType(extension)
connection:send("HTTP/1.0 " .. code .. " " .. getHTTPStatusString(code) .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\nCache-Control: private, no-store\r\n") connection:send("HTTP/1.0 " .. code .. " " .. getHTTPStatusString(code) .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\n")
if isGzipped then if isGzipped then
connection:send("Cache-Control: max-age=2592000\r\nContent-Encoding: gzip\r\n") connection:send("Cache-Control: private, max-age=2592000\r\nContent-Encoding: gzip\r\n")
end end
connection:send("Connection: close\r\n\r\n") connection:send("Connection: close\r\n\r\n")
end end

View File

@ -3,32 +3,25 @@
-- Author: Marcos Kirsch -- Author: Marcos Kirsch
return function (connection, req, args) 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) dofile("httpserver-header.lc")(connection, 200, args.ext, args.isGzipped)
-- Send file in little chunks -- Send file in little chunks
local continue = true local bytesRemaining = file.list()[args.file]
local size = file.list()[args.file]
local bytesSent = 0
-- Chunks larger than 1024 don't work. -- Chunks larger than 1024 don't work.
-- https://github.com/nodemcu/nodemcu-firmware/issues/1075 -- https://github.com/nodemcu/nodemcu-firmware/issues/1075
local chunkSize = 1024 local chunkSize = 1024
while continue do local fileHandle = file.open(args.file)
collectgarbage() while bytesRemaining > 0 do
local bytesToRead = 0
-- NodeMCU file API lets you open 1 file at a time. if bytesRemaining > chunkSize then bytesToRead = chunkSize else bytesToRead = bytesRemaining end
-- So we need to open, seek, close each time in order local chunk = fileHandle:read(bytesToRead)
-- to support multiple simultaneous clients.
file.open(args.file)
file.seek("set", bytesSent)
local chunk = file.read(chunkSize)
file.close()
connection:send(chunk) connection:send(chunk)
bytesSent = bytesSent + #chunk bytesRemaining = bytesRemaining - #chunk
--print(args.file .. ": Sent "..#chunk.. " bytes, " .. bytesRemaining .. " to go.")
chunk = nil chunk = nil
--print("Sent: " .. bytesSent .. " of " .. size) collectgarbage()
if bytesSent == size then continue = false end
end end
--print("Finished sending: ", args.file) print("Finished sending: ", args.file)
fileHandle:close()
fileHandle = nil
collectgarbage()
end end

View File

@ -155,11 +155,6 @@ return function (port)
end end
) )
-- false and nil evaluate as false
local ip = wifi.sta.getip()
if not ip then ip = wifi.ap.getip() end
if not ip then ip = "unknown IP" end
print("nodemcu-httpserver running at http://" .. ip .. ":" .. port)
return s return s
end end

View File

@ -5,7 +5,7 @@ local wifiConfig = {}
-- wifi.STATION -- station: join a WiFi network -- wifi.STATION -- station: join a WiFi network
-- wifi.SOFTAP -- access point: create a WiFi network -- wifi.SOFTAP -- access point: create a WiFi network
-- wifi.wifi.STATIONAP -- both station and access point -- wifi.wifi.STATIONAP -- both station and access point
wifiConfig.mode = wifi.STATIONAP -- both station and access point wifiConfig.mode = wifi.STATION
wifiConfig.accessPointConfig = {} wifiConfig.accessPointConfig = {}
wifiConfig.accessPointConfig.ssid = "ESP-"..node.chipid() -- Name of the SSID you want to create wifiConfig.accessPointConfig.ssid = "ESP-"..node.chipid() -- Name of the SSID you want to create
@ -23,7 +23,7 @@ wifiConfig.stationPointConfig.pwd = "" -- Password for the WiFi
-- Tell the chip to connect to the access point -- Tell the chip to connect to the access point
wifi.setmode(wifiConfig.mode) wifi.setmode(wifiConfig.mode)
print('set (mode='..wifi.getmode()..')') --print('set (mode='..wifi.getmode()..')')
if (wifiConfig.mode == wifi.SOFTAP) or (wifiConfig.mode == wifi.STATIONAP) then if (wifiConfig.mode == wifi.SOFTAP) or (wifiConfig.mode == wifi.STATIONAP) then
print('AP MAC: ',wifi.ap.getmac()) print('AP MAC: ',wifi.ap.getmac())
@ -44,7 +44,7 @@ collectgarbage()
-- End WiFi configuration -- End WiFi configuration
-- Compile server code and remove original .lua files. -- Compile server code and remove original .lua files.
-- This only happens the first time afer the .lua files are uploaded. -- This only happens the first time after server .lua files are uploaded.
local compileAndRemoveIfNeeded = function(f) local compileAndRemoveIfNeeded = function(f)
if file.open(f) then if file.open(f) then
@ -74,32 +74,48 @@ serverFiles = nil
collectgarbage() collectgarbage()
-- Connect to the WiFi access point. -- Connect to the WiFi access point.
-- Once the device is connected, you may start the HTTP server. -- Once the device is connected, start the HTTP server.
startServer = function(ip, hostname)
local serverPort = 80
if (dofile("httpserver.lc")(serverPort)) then
print("nodemcu-httpserver running at:")
print(" http://" .. ip .. ":" .. serverPort)
if (mdns) then
mdns.register(hostname, { description="A tiny server", service="http", port=serverPort, location='Earth' })
print (' http://' .. hostname .. '.local.:' .. serverPort)
end
end
end
if (wifi.getmode() == wifi.STATION) or (wifi.getmode() == wifi.STATIONAP) then if (wifi.getmode() == wifi.STATION) or (wifi.getmode() == wifi.STATIONAP) then
local joinCounter = 0 local joinCounter = 0
local joinMaxAttempts = 5 local joinMaxAttempts = 5
tmr.alarm(0, 3000, 1, function() tmr.alarm(0, 3000, 1, function()
local ip = wifi.sta.getip() local ip = wifi.sta.getip()
if ip == nil and joinCounter < joinMaxAttempts then if (not ip) then ip = wifi.ap.getip() end
print('Connecting to WiFi Access Point ...') if ip == nil and joinCounter < joinMaxAttempts then
joinCounter = joinCounter +1 print('Connecting to WiFi Access Point ...')
else joinCounter = joinCounter + 1
if joinCounter == joinMaxAttempts then else
print('Failed to connect to WiFi Access Point.') if joinCounter == joinMaxAttempts then
else print('Failed to connect to WiFi Access Point.')
print('IP: ',ip) else
end print("IP = " .. ip)
tmr.stop(0) if (not not wifi.sta.getip()) or (not not wifi.ap.getip()) then
joinCounter = nil -- Second parameter is for mDNS (aka Zeroconf aka Bonjour) registration.
joinMaxAttempts = nil -- If the mdns module is compiled in the firmware, advertise the server with this name.
collectgarbage() -- If no mdns is compiled, then parameter is ignored.
end startServer(ip, "nodemcu")
end) end
end end
tmr.stop(0)
-- Uncomment to automatically start the server in port 80 joinCounter = nil
if (not not wifi.sta.getip()) or (not not wifi.ap.getip()) then joinMaxAttempts = nil
--dofile("httpserver.lc")(80) collectgarbage()
end
end)
else
startServer()
end end