allow running server from LFS (#125)

This commit is contained in:
seregaxvm
2019-11-18 22:32:47 +03:00
committed by Marcos
parent 6c7e451663
commit 6511dad8df
16 changed files with 199 additions and 31 deletions

99
srv/_init.lua Normal file
View File

@@ -0,0 +1,99 @@
--
-- File: _init.lua
--[[
This is a template for the LFS equivalent of the SPIFFS init.lua.
It is a good idea to such an _init.lua module to your LFS and do most of the LFS
module related initialisaion in this. This example uses standard Lua features to
simplify the LFS API.
The first section adds a 'LFS' table to _G and uses the __index metamethod to
resolve functions in the LFS, so you can execute the main function of module
'fred' by executing LFS.fred(params), etc. It also implements some standard
readonly properties:
LFS._time The Unix Timestamp when the luac.cross was executed. This can be
used as a version identifier.
LFS._config This returns a table of useful configuration parameters, hence
print (("0x%6x"):format(LFS._config.lfs_base))
gives you the parameter to use in the luac.cross -a option.
LFS._list This returns a table of the LFS modules, hence
print(table.concat(LFS._list,'\n'))
gives you a single column listing of all modules in the LFS.
---------------------------------------------------------------------------------]]
local index = node.flashindex
local lfs_t = {
__index = function(_, name)
local fn_ut, ba, ma, size, modules = index(name)
if not ba then
return fn_ut
elseif name == '_time' then
return fn_ut
elseif name == '_config' then
local fs_ma, fs_size = file.fscfg()
return {lfs_base = ba, lfs_mapped = ma, lfs_size = size,
fs_mapped = fs_ma, fs_size = fs_size}
elseif name == '_list' then
return modules
else
return nil
end
end,
__newindex = function(_, name, value)
error("LFS is readonly. Invalid write to LFS." .. name, 2)
end,
}
local G=getfenv()
G.LFS = setmetatable(lfs_t,lfs_t)
--[[-------------------------------------------------------------------------------
The second section adds the LFS to the require searchlist, so that you can
require a Lua module 'jean' in the LFS by simply doing require "jean". However
note that this is at the search entry following the FS searcher, so if you also
have jean.lc or jean.lua in SPIFFS, then this SPIFFS version will get loaded into
RAM instead of using. (Useful, for development).
See docs/en/lfs.md and the 'loaders' array in app/lua/loadlib.c for more details.
---------------------------------------------------------------------------------]]
package.loaders[3] = function(module) -- loader_flash
local fn, ba = index(module)
return ba and "Module not in LFS" or fn
end
--[[-------------------------------------------------------------------------------
You can add any other initialisation here, for example a couple of the globals
are never used, so setting them to nil saves a couple of global entries
---------------------------------------------------------------------------------]]
G.module = nil -- disable Lua 5.0 style modules to save RAM
package.seeall = nil
--[[-------------------------------------------------------------------------------
These replaces the builtins loadfile & dofile with ones which preferentially
loads the corresponding module from LFS if present. Flipping the search order
is an exercise left to the reader.-
---------------------------------------------------------------------------------]]
local lf, df = loadfile, dofile
G.loadfile = function(n)
local mod, ext = n:match("(.*)%.(l[uc]a?)");
local fn, ba = index(mod)
if ba or (ext ~= 'lc' and ext ~= 'lua') then return lf(n) else return fn end
end
G.dofile = function(n)
local mod, ext = n:match("(.*)%.(l[uc]a?)");
local fn, ba = index(mod)
if ba or (ext ~= 'lc' and ext ~= 'lua') then return df(n) else return fn() end
end

37
srv/dummy_strings.lua Normal file
View File

@@ -0,0 +1,37 @@
--
-- File: LFS_dummy_strings.lua
--[[
luac.cross -f generates a ROM string table which is part of the compiled LFS
image. This table includes all strings referenced in the loaded modules.
If you want to preload other string constants, then one way to achieve this is
to include a dummy module in the LFS that references the strings that you want
to load. You never need to call this module; it's inclusion in the LFS image is
enough to add the strings to the ROM table. Your application can use any strings
in the ROM table without incuring any RAM or Lua Garbage Collector (LGC)
overhead.
The local preload example is a useful starting point. However, if you call the
following code in your application during testing, then this will provide a
listing of the current RAM string table.
do
local a=debug.getstrings'RAM'
for i =1, #a do a[i] = ('%q'):format(a[i]) end
print ('local preload='..table.concat(a,','))
end
This will exclude any strings already in the ROM table, so the output is the list
of putative strings that you should consider adding to LFS ROM table.
---------------------------------------------------------------------------------]]
local preload = "?.lc;?.lua", "/\n;\n?\n!\n-", "@init.lua", "_G", "_LOADED",
"_LOADLIB", "__add", "__call", "__concat", "__div", "__eq", "__gc", "__index",
"__le", "__len", "__lt", "__mod", "__mode", "__mul", "__newindex", "__pow",
"__sub", "__tostring", "__unm", "collectgarbage", "cpath", "debug", "file",
"file.obj", "file.vol", "flash", "getstrings", "index", "ipairs", "list", "loaded",
"loader", "loaders", "loadlib", "module", "net.tcpserver", "net.tcpsocket",
"net.udpsocket", "newproxy", "package", "pairs", "path", "preload", "reload",
"require", "seeall", "wdclr", "not enough memory", "sjson.decoder","sjson.encoder",
"tmr.timer"

View File

@@ -0,0 +1,75 @@
#!/usr/local/bin/lua
-- httpserver-b64decode.lua
-- 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
-- compatible with lua 5.1
-- http://www.it-rfc.de
-- 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)
-- shift left
local function lsh(value,shift)
return (value*(uipow(2, shift))) % 256
end
-- shift right
local function rsh(value,shift)
-- Lua builds with no floating point don't define math.
if math then return math.floor(value/uipow(2, shift)) % 256 end
return (value/uipow(2, shift)) % 256
end
-- return single bit (for OR)
local function bit(x,b)
return (x % uipow(2, b) - x % uipow(2, (b-1)) > 0)
end
-- logic OR for number values
local function lor(x,y)
local result = 0
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
end
-- Character decoding table
local function toBase64Byte(char)
local ascii = string.byte(char, 1)
if ascii >= string.byte('A', 1) and ascii <= string.byte('Z', 1) then return ascii - string.byte('A', 1)
elseif ascii >= string.byte('a', 1) and ascii <= string.byte('z', 1) then return ascii - string.byte('a', 1) + 26
elseif ascii >= string.byte('0', 1) and ascii <= string.byte('9', 1) then return ascii + 4
elseif ascii == string.byte('-', 1) then return 62
elseif ascii == string.byte('_', 1) then return 63
elseif ascii == string.byte('=', 1) then return nil
else return nil, "ERROR! Char is invalid for Base64 encoding: "..char end
end
-- decode base64 input to string
return function(data)
local chars = {}
local result=""
for dpos=0,string.len(data)-1,4 do
for char=1,4 do chars[char] = toBase64Byte((string.sub(data,(dpos+char),(dpos+char)) or "=")) end
result = string.format(
'%s%s%s%s',
result,
string.char(lor(lsh(chars[1],2), rsh(chars[2],4))),
(chars[3] ~= nil) and string.char(lor(lsh(chars[2],4),
rsh(chars[3],2))) or "",
(chars[4] ~= nil) and string.char(lor(lsh(chars[3],6) % 192,
(chars[4]))) or ""
)
end
return result
end

View File

@@ -0,0 +1,42 @@
-- httpserver-basicauth.lua
-- Part of nodemcu-httpserver, authenticates a user using http basic auth.
-- Author: Sam Dieck
local 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.
-- Returns the username if header contains valid credentials,
-- nil otherwise.
function basicAuth.authenticate(header)
local conf = dofile("httpserver-conf.lua")
local credentials_enc = header:match("Authorization: Basic ([A-Za-z0-9+/=]+)")
if not credentials_enc then
return nil
end
local credentials = dofile("httpserver-b64decode.lc")(credentials_enc)
local user, pwd = credentials:match("^(.*):(.*)$")
if loginIsValid(user, pwd, conf.auth.users) then
print("httpserver-basicauth: User \"" .. user .. "\": Authenticated.")
return user
else
print("httpserver-basicauth: User \"" .. user .. "\": Access denied.")
return nil
end
end
function basicAuth.authErrorHeader()
local conf = dofile("httpserver-conf.lua")
return "WWW-Authenticate: Basic realm=\"" .. conf.auth.realm .. "\""
end
return basicAuth

48
srv/httpserver-buffer.lua Normal file
View File

@@ -0,0 +1,48 @@
-- httpserver-buffer
-- Part of nodemcu-httpserver, provides a buffer that behaves like a connection object
-- that can handle multiple consecutive send() calls, and buffers small payloads up to 1400 bytes.
-- This is primarily user to collect the send requests done by the head script.
-- The owner is responsible to call getBuffer and send its result
-- Author: Gregor Hartmann
local Buffer = {}
-- parameter is the nodemcu-firmware connection
function Buffer:new()
local newInstance = {}
newInstance.size = 0
newInstance.data = {}
-- Returns true if there was any data to be sent.
function newInstance:getBuffer()
local buffer = table.concat(self.data, "")
self.data = {}
self.size = 0
return buffer
end
function newInstance:getpeer()
return "no peer"
end
function newInstance:send(payload)
local flushThreshold = 1400
if (not payload) then print("nop payload") end
local newSize = self.size + payload:len()
if newSize >= flushThreshold then
print("Buffer is full. Cutting off "..newSize-flushThreshold.." chars")
--STEP1: cut out piece from payload to complete threshold bytes in table
local pieceSize = flushThreshold - self.size
if pieceSize then
payload = payload:sub(1, pieceSize)
end
end
table.insert(self.data, payload)
self.size = self.size + #payload
end
return newInstance
end
return Buffer

View File

@@ -0,0 +1,63 @@
-- httpserver-connection
-- Part of nodemcu-httpserver, provides a buffered connection object that can handle multiple
-- consecutive send() calls, and buffers small payloads to send once they get big.
-- For this to work, it must be used from a coroutine and owner is responsible for the final
-- flush() and for closing the connection.
-- Author: Philip Gladstone, Marcos Kirsch
local BufferedConnection = {}
-- parameter is the nodemcu-firmware connection
function BufferedConnection:new(connection)
local newInstance = {}
newInstance.connection = connection
newInstance.size = 0
newInstance.data = {}
-- Returns true if there was any data to be sent.
function newInstance:flush()
if self.size > 0 then
self.connection:send(table.concat(self.data, ""))
self.data = {}
self.size = 0
return true
end
return false
end
function newInstance:getpeer()
return self.connection:getpeer()
end
function newInstance:send(payload)
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)
piece = nil
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
--at this point, whatever is left in payload should be < flushThreshold
if payload:len() ~= 0 then
--leave remaining data in the table
table.insert(self.data, payload)
self.size = self.size + payload:len()
end
end
return newInstance
end
return BufferedConnection

8
srv/httpserver-error.lua Normal file
View File

@@ -0,0 +1,8 @@
-- httpserver-error.lua
-- Part of nodemcu-httpserver, handles sending error pages to client.
-- Author: Marcos Kirsch, Gregor Hartmann
return function (connection, req, args)
local statusString = dofile("httpserver-header.lc")(connection, args.code, "html", false, args.headers)
connection:send("<html><head><title>" .. args.code .. " - " .. statusString .. "</title></head><body><h1>" .. args.code .. " - " .. statusString .. "</h1></body></html>\r\n")
end

37
srv/httpserver-header.lua Normal file
View File

@@ -0,0 +1,37 @@
-- httpserver-header.lua
-- Part of nodemcu-httpserver, knows how to send an HTTP header.
-- Author: Marcos Kirsch
return function(connection, code, extension, isGzipped, extraHeaders)
local function getHTTPStatusString(code)
local codez = { [200] = "OK", [400] = "Bad Request", [401] = "Unauthorized", [404] = "Not Found", [405] = "Method Not Allowed", [500] = "Internal Server Error", [501] = "Not Implemented", }
local myResult = codez[code]
-- enforce returning valid http codes all the way throughout?
if myResult then return myResult else return "Not Implemented" end
end
local function getMimeType(ext)
-- 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"}
if mt[ext] then return mt[ext] else return "application/octet-stream" end
end
local mimeType = getMimeType(extension)
local statusString = getHTTPStatusString(code)
connection:send("HTTP/1.0 " .. code .. " " .. statusString .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\n")
if isGzipped then
connection:send("Cache-Control: private, max-age=2592000\r\nContent-Encoding: gzip\r\n")
end
if (extraHeaders) then
for i, extraHeader in ipairs(extraHeaders) do
connection:send(extraHeader .. "\r\n")
end
end
connection:send("Connection: close\r\n\r\n")
return statusString
end

53
srv/httpserver-init.lua Normal file
View File

@@ -0,0 +1,53 @@
-- httpserver-init.lua
-- Part of nodemcu-httpserver, launches the server.
-- Author: Marcos Kirsch
-- Function for starting the server.
-- If you compiled the mdns module, then it will also register with mDNS.
local startServer = function(ip)
local conf = dofile('httpserver-conf.lua')
if (dofile("httpserver.lc")(conf['general']['port'])) then
print("nodemcu-httpserver running at:")
print(" http://" .. ip .. ":" .. conf['general']['port'])
if (mdns) then
mdns.register(conf['mdns']['hostname'], { description=conf['mdns']['description'], service="http", port=conf['general']['port'], location=conf['mdns']['location'] })
print (' http://' .. conf['mdns']['hostname'] .. '.local.:' .. conf['general']['port'])
end
end
conf = nil
end
if (wifi.getmode() == wifi.STATION) or (wifi.getmode() == wifi.STATIONAP) then
-- Connect to the WiFi access point and start server once connected.
-- If the server loses connectivity, server will restart.
wifi.eventmon.register(wifi.eventmon.STA_GOT_IP, function(args)
print("Connected to WiFi Access Point. Got IP: " .. args["IP"])
startServer(args["IP"])
wifi.eventmon.register(wifi.eventmon.STA_DISCONNECTED, function(args)
print("Lost connectivity! Restarting...")
node.restart()
end)
end)
-- What if after a while (30 seconds) we didn't connect? Restart and keep trying.
local watchdogTimer = tmr.create()
watchdogTimer:register(30000, tmr.ALARM_SINGLE, function (watchdogTimer)
local ip = wifi.sta.getip()
if (not ip) then ip = wifi.ap.getip() end
if ip == nil then
print("No IP after a while. Restarting...")
node.restart()
else
--print("Successfully got IP. Good, no need to restart.")
watchdogTimer:unregister()
end
end)
watchdogTimer:start()
else
startServer(wifi.ap.getip())
end

130
srv/httpserver-request.lua Normal file
View File

@@ -0,0 +1,130 @@
-- httpserver-request
-- Part of nodemcu-httpserver, parses incoming client requests.
-- Author: Marcos Kirsch
local function validateMethod(method)
local httpMethods = {GET=true, HEAD=true, POST=true, PUT=true, DELETE=true, TRACE=true, OPTIONS=true, CONNECT=true, PATCH=true}
-- default for non-existent attributes returns nil, which evaluates to false
return httpMethods[method]
end
local function uriToFilename(uri)
return "http/" .. string.sub(uri, 2, -1)
end
local function hex_to_char(x)
return string.char(tonumber(x, 16))
end
local function uri_decode(input)
return input:gsub("%+", " "):gsub("%%(%x%x)", hex_to_char)
end
local function parseArgs(args)
local r = {}
local i = 1
if args == nil or args == "" then return r end
for arg in string.gmatch(args, "([^&]+)") do
local name, value = string.match(arg, "(.*)=(.*)")
if name ~= nil then r[name] = uri_decode(value) end
i = i + 1
end
return r
end
local function parseFormData(body)
local data = {}
--print("Parsing Form Data")
for kv in body.gmatch(body, "%s*&?([^=]+=[^&]+)") do
local key, value = string.match(kv, "(.*)=(.*)")
--print("Parsed: " .. key .. " => " .. value)
data[key] = uri_decode(value)
end
return data
end
local function getRequestData(payload)
local requestData
return function ()
--print("Getting Request Data")
-- for backward compatibility before v2.1
if (sjson == nil) then
sjson = cjson
end
if requestData then
return requestData
else
--print("payload = [" .. payload .. "]")
local mimeType = string.match(payload, "Content%-Type: ([%w/-]+)")
local bodyStart = payload:find("\r\n\r\n", 1, true)
local body = payload:sub(bodyStart, #payload)
payload = nil
collectgarbage()
--print("mimeType = [" .. mimeType .. "]")
--print("bodyStart = [" .. bodyStart .. "]")
--print("body = [" .. body .. "]")
if mimeType == "application/json" then
--print("JSON: " .. body)
requestData = sjson.decode(body)
elseif mimeType == "application/x-www-form-urlencoded" then
requestData = parseFormData(body)
else
requestData = {}
end
return requestData
end
end
end
local function parseUri(uri)
local r = {}
local filename
local ext
local fullExt = {}
if uri == nil then return r end
if uri == "/" then uri = "/index.html" end
local questionMarkPos, b, c, d, e, f = uri:find("?")
if questionMarkPos == nil then
r.file = uri:sub(1, questionMarkPos)
r.args = {}
else
r.file = uri:sub(1, questionMarkPos - 1)
r.args = parseArgs(uri:sub(questionMarkPos+1, #uri))
end
filename = r.file
while filename:match("%.") do
filename,ext = filename:match("(.+)%.(.+)")
table.insert(fullExt,1,ext)
end
if #fullExt > 1 and fullExt[#fullExt] == 'gz' then
r.ext = fullExt[#fullExt-1]
r.isGzipped = true
elseif #fullExt >= 1 then
r.ext = fullExt[#fullExt]
end
r.isScript = r.ext == "lua" or r.ext == "lc"
r.file = uriToFilename(r.file)
return r
end
-- Parses the client's request. Returns a dictionary containing pretty much everything
-- the server needs to know about the uri.
return function (request)
--print("Request: \n", request)
local e = request:find("\r\n", 1, true)
if not e then return nil end
local line = request:sub(1, e - 1)
local r = {}
local _, i
_, i, r.method, r.request = line:find("^([A-Z]+) (.-) HTTP/[1-9]+.[0-9]+$")
if not (r.method and r.request) then
--print("invalid request: ")
--print(request)
return nil
end
r.methodIsValid = validateMethod(r.method)
r.uri = parseUri(r.request)
r.getRequestData = getRequestData(request)
return r
end

13
srv/httpserver-static.lua Normal file
View File

@@ -0,0 +1,13 @@
-- httpserver-static.lua
-- Part of nodemcu-httpserver, handles sending static files to client.
-- Author: Gregor Hartmann
return function (connection, req, args)
local buffer = dofile("httpserver-buffer.lc"):new()
dofile("httpserver-header.lc")(buffer, req.code or 200, args.ext, args.isGzipped)
-- Send header and return fileInfo
connection:send(buffer:getBuffer())
return { file = args.file, sent = 0}
end

26
srv/httpserver-wifi.lua Normal file
View File

@@ -0,0 +1,26 @@
-- httpserver-wifi.lua
-- Part of nodemcu-httpserver, configures NodeMCU's WiFI in boot.
-- Author: Marcos Kirsch
local conf = dofile("httpserver-conf.lua")
wifi.setmode(conf.wifi.mode)
if (conf.wifi.mode == wifi.SOFTAP) or (conf.wifi.mode == wifi.STATIONAP) then
print('AP MAC: ',wifi.ap.getmac())
wifi.ap.config(conf.wifi.accessPoint.config)
wifi.ap.setip(conf.wifi.accessPoint.net)
end
if (conf.wifi.mode == wifi.STATION) or (conf.wifi.mode == wifi.STATIONAP) then
print('Client MAC: ',wifi.sta.getmac())
wifi.sta.config(conf.wifi.station)
end
print('chip: ',node.chipid())
print('heap: ',node.heap())
conf = nil
collectgarbage()
-- End WiFi configuration

217
srv/httpserver.lua Normal file
View File

@@ -0,0 +1,217 @@
-- httpserver
-- Author: Marcos Kirsch
-- Starts web server in the specified port.
return function (port)
local s = net.createServer(net.TCP, 10) -- 10 seconds client timeout
s:listen(
port,
function (connection)
-- 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 fileInfo
local allowStatic = {GET=true, HEAD=true, POST=false, PUT=false, DELETE=false, TRACE=false, OPTIONS=false, CONNECT=false, PATCH=false}
-- Pretty log function.
local function log(connection, msg, optionalMsg)
local port, ip = connection:getpeer()
if(optionalMsg == nil) then
print(ip .. ":" .. port, msg)
else
print(ip .. ":" .. port, msg, optionalMsg)
end
end
local function startServingStatic(connection, req, args)
fileInfo = dofile("httpserver-static.lc")(connection, req, args)
end
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
log(connection, "closing connection", "no (more) data")
connection:close()
connectionThread = nil
collectgarbage()
end
end)
local BufferedConnectionClass = dofile("httpserver-connection.lc")
local bufferedConnection = BufferedConnectionClass:new(connection)
BufferedConnectionClass = nil
local status, err = coroutine.resume(connectionThread, fileServeFunction, bufferedConnection, req, args)
if not status then
log(connection, "Error: "..err)
log(connection, "closing connection", "error")
connection:close()
connectionThread = nil
collectgarbage()
end
end
local function handleRequest(connection, req, handleError)
collectgarbage()
local method = req.method
local uri = req.uri
local fileServeFunction = nil
if #(uri.file) > 32 then
-- nodemcu-firmware cannot handle long filenames.
uri.args = {code = 400, errorString = "Bad Request", logFunction = log}
fileServeFunction = dofile("httpserver-error.lc")
else
local fileExists = false
if not file.exists(uri.file) then
-- print(uri.file .. " not found, checking gz version...")
-- gzip check
if file.exists(uri.file .. ".gz") then
-- print("gzip variant exists, serving that one")
uri.file = uri.file .. ".gz"
uri.isGzipped = true
fileExists = true
end
else
fileExists = true
end
if not fileExists then
uri.args = {code = 404, errorString = "Not Found", logFunction = log}
fileServeFunction = dofile("httpserver-error.lc")
elseif uri.isScript then
fileServeFunction = dofile(uri.file)
else
if allowStatic[method] then
uri.args = {file = uri.file, ext = uri.ext, isGzipped = uri.isGzipped}
startServingStatic(connection, req, uri.args)
return
else
uri.args = {code = 405, errorString = "Method not supported", logFunction = log}
fileServeFunction = dofile("httpserver-error.lc")
end
end
end
startServing(fileServeFunction, connection, req, uri.args)
end
local function onReceive(connection, payload)
-- collectgarbage()
local conf = dofile("httpserver-conf.lua")
local auth
local user = "Anonymous"
-- as suggest by anyn99 (https://github.com/marcoskirsch/nodemcu-httpserver/issues/36#issuecomment-167442461)
-- Some browsers send the POST data in multiple chunks.
-- Collect data packets until the size of HTTP body meets the Content-Length stated in header
if payload:find("Content%-Length:") or bBodyMissing then
if fullPayload then fullPayload = fullPayload .. payload else fullPayload = payload end
if (tonumber(string.match(fullPayload, "%d+", fullPayload:find("Content%-Length:")+16)) > #fullPayload:sub(fullPayload:find("\r\n\r\n", 1, true)+4, #fullPayload)) then
bBodyMissing = true
return
else
--print("HTTP packet assembled! size: "..#fullPayload)
payload = fullPayload
fullPayload, bBodyMissing = nil
end
end
collectgarbage()
-- parse payload and decide what to serve.
local req = dofile("httpserver-request.lc")(payload)
log(connection, 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
req.user = user
handleRequest(connection, req, handleError)
else
local args = {}
local fileServeFunction = dofile("httpserver-error.lc")
if not user then
args = {code = 401, errorString = "Not Authorized", headers = {auth.authErrorHeader()}, logFunction = log}
elseif req.methodIsValid then
args = {code = 501, errorString = "Not Implemented", logFunction = log}
else
args = {code = 400, errorString = "Bad Request", logFunction = log}
end
startServing(fileServeFunction, connection, req, args)
end
end
local function onSent(connection, payload)
collectgarbage()
if connectionThread then
local connectionThreadStatus = coroutine.status(connectionThread)
if connectionThreadStatus == "suspended" then
-- Not finished sending file, resume.
local status, err = coroutine.resume(connectionThread)
if not status then
log(connection, "Error: "..err)
log(connection, "closing connection", "error")
connection:close()
connectionThread = nil
collectgarbage()
end
elseif connectionThreadStatus == "dead" then
-- We're done sending file.
log(connection, "closing connection","thread is dead")
connection:close()
connectionThread = nil
collectgarbage()
end
elseif fileInfo then
local fileSize = file.list()[fileInfo.file]
-- Chunks larger than 1024 don't work.
-- https://github.com/nodemcu/nodemcu-firmware/issues/1075
local chunkSize = 512
local fileHandle = file.open(fileInfo.file)
if fileSize > fileInfo.sent then
fileHandle:seek("set", fileInfo.sent)
local chunk = fileHandle:read(chunkSize)
fileHandle:close()
fileHandle = nil
fileInfo.sent = fileInfo.sent + #chunk
connection:send(chunk)
-- print(fileInfo.file .. ": Sent "..#chunk.. " bytes, " .. fileSize - fileInfo.sent .. " to go.")
chunk = nil
else
log(connection, "closing connetion", "Finished sending: "..fileInfo.file)
connection:close()
fileInfo = nil
end
collectgarbage()
end
end
local function onDisconnect(connection, payload)
-- this should rather be a log call, but log is not available here
-- print("disconnected")
if connectionThread then
connectionThread = nil
collectgarbage()
end
if fileInfo then
fileInfo = nil
collectgarbage()
end
end
connection:on("receive", onReceive)
connection:on("sent", onSent)
connection:on("disconnection", onDisconnect)
end
)
return s
end