├── .gitignore ├── http ├── cars-mas.jpg ├── cars-lambo.jpg ├── zipped.html.gz ├── cars-bugatti.jpg ├── cars-ferrari.jpg ├── cars-mercedes.jpg ├── cars-porsche.jpg ├── apple-touch-icon.png ├── underconstruction.gif ├── hello_world.txt ├── counter.html ├── cars.html ├── upload.html ├── garage_door.html ├── args.lua ├── led.lua ├── post.lua ├── file_list.lua ├── node_info.lua ├── led.html ├── cars.lua ├── garage_door_control.css ├── index.html ├── garage_door_control.html ├── upload.lua ├── garage_door.lua ├── upload.css └── upload.js ├── TODO.md ├── srv ├── httpserver-error.lua ├── httpserver-static.lua ├── httpserver-wifi.lua ├── httpserver-basicauth.lua ├── httpserver-buffer.lua ├── httpserver-header.lua ├── dummy_strings.lua ├── httpserver-init.lua ├── httpserver-connection.lua ├── httpserver-b64decode.lua ├── _init.lua ├── httpserver-request.lua └── httpserver.lua ├── init.lua ├── httpserver-compile.lua ├── httpserver-conf.lua ├── Makefile ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /http/cars-mas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcoskirsch/nodemcu-httpserver/HEAD/http/cars-mas.jpg -------------------------------------------------------------------------------- /http/cars-lambo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcoskirsch/nodemcu-httpserver/HEAD/http/cars-lambo.jpg -------------------------------------------------------------------------------- /http/zipped.html.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcoskirsch/nodemcu-httpserver/HEAD/http/zipped.html.gz -------------------------------------------------------------------------------- /http/cars-bugatti.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcoskirsch/nodemcu-httpserver/HEAD/http/cars-bugatti.jpg -------------------------------------------------------------------------------- /http/cars-ferrari.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcoskirsch/nodemcu-httpserver/HEAD/http/cars-ferrari.jpg -------------------------------------------------------------------------------- /http/cars-mercedes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcoskirsch/nodemcu-httpserver/HEAD/http/cars-mercedes.jpg -------------------------------------------------------------------------------- /http/cars-porsche.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcoskirsch/nodemcu-httpserver/HEAD/http/cars-porsche.jpg -------------------------------------------------------------------------------- /http/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcoskirsch/nodemcu-httpserver/HEAD/http/apple-touch-icon.png -------------------------------------------------------------------------------- /http/underconstruction.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcoskirsch/nodemcu-httpserver/HEAD/http/underconstruction.gif -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | #TODO 2 | 3 | * Change how POST parameters are passed to better match GET (use args variable). 4 | * Need PUT example. How? 5 | * Rename args.lua to get.lua, so it matches post.lua convention. 6 | * How can I test the whole JSON post thing? 7 | -------------------------------------------------------------------------------- /http/hello_world.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | If server were sending this as HTML, your browser wouldn't render this in its own line. 3 | Here are some translations. This file is encoded as UTF-8. Let's see if they look ok: 4 | ¡Hola mundo! 5 | שלום עולם 6 | สวัสดีโลก! 7 | أهلاً بالعالم 8 | 9 | -------------------------------------------------------------------------------- /srv/httpserver-error.lua: -------------------------------------------------------------------------------- 1 | -- httpserver-error.lua 2 | -- Part of nodemcu-httpserver, handles sending error pages to client. 3 | -- Author: Marcos Kirsch, Gregor Hartmann 4 | 5 | return function (connection, req, args) 6 | local statusString = dofile("httpserver-header.lc")(connection, args.code, "html", false, args.headers) 7 | connection:send("" .. args.code .. " - " .. statusString .. "

" .. args.code .. " - " .. statusString .. "

\r\n") 8 | end 9 | -------------------------------------------------------------------------------- /srv/httpserver-static.lua: -------------------------------------------------------------------------------- 1 | -- httpserver-static.lua 2 | -- Part of nodemcu-httpserver, handles sending static files to client. 3 | -- Author: Gregor Hartmann 4 | 5 | return function (connection, req, args) 6 | 7 | local buffer = dofile("httpserver-buffer.lc"):new() 8 | dofile("httpserver-header.lc")(buffer, req.code or 200, args.ext, args.isGzipped) 9 | -- Send header and return fileInfo 10 | connection:send(buffer:getBuffer()) 11 | 12 | return { file = args.file, sent = 0} 13 | end 14 | -------------------------------------------------------------------------------- /http/counter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Counter 5 | 16 | 17 | 18 |
This page reloads itself as fast as it can to test the server.
19 |
It is meant as a stress test to see when and if the server fails.
20 |

-

21 | 22 | 23 | -------------------------------------------------------------------------------- /srv/httpserver-wifi.lua: -------------------------------------------------------------------------------- 1 | -- httpserver-wifi.lua 2 | -- Part of nodemcu-httpserver, configures NodeMCU's WiFI in boot. 3 | -- Author: Marcos Kirsch 4 | 5 | local conf = dofile("httpserver-conf.lua") 6 | 7 | wifi.setmode(conf.wifi.mode) 8 | 9 | if (conf.wifi.mode == wifi.SOFTAP) or (conf.wifi.mode == wifi.STATIONAP) then 10 | print('AP MAC: ',wifi.ap.getmac()) 11 | wifi.ap.config(conf.wifi.accessPoint.config) 12 | wifi.ap.setip(conf.wifi.accessPoint.net) 13 | end 14 | 15 | if (conf.wifi.mode == wifi.STATION) or (conf.wifi.mode == wifi.STATIONAP) then 16 | print('Client MAC: ',wifi.sta.getmac()) 17 | wifi.sta.config(conf.wifi.station) 18 | end 19 | 20 | print('chip: ',node.chipid()) 21 | print('heap: ',node.heap()) 22 | 23 | conf = nil 24 | collectgarbage() 25 | 26 | -- End WiFi configuration 27 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | -- check/flash/use LFS support, if possible 2 | if node.getpartitiontable().lfs_size > 0 then 3 | if file.exists("lfs.img") then 4 | if file.exists("lfs_lock") then 5 | file.remove("lfs_lock") 6 | file.remove("lfs.img") 7 | else 8 | local f = file.open("lfs_lock", "w") 9 | f:flush() 10 | f:close() 11 | file.remove("httpserver-compile.lua") 12 | node.LFS.reload("lfs.img") 13 | end 14 | end 15 | pcall(node.flashindex("_init")) 16 | end 17 | 18 | -- Compile freshly uploaded nodemcu-httpserver lua files. 19 | if file.exists("httpserver-compile.lua") then 20 | dofile("httpserver-compile.lua") 21 | file.remove("httpserver-compile.lua") 22 | end 23 | 24 | 25 | -- Set up NodeMCU's WiFi 26 | dofile("httpserver-wifi.lc") 27 | 28 | -- Start nodemcu-httpsertver 29 | dofile("httpserver-init.lc") 30 | -------------------------------------------------------------------------------- /http/cars.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Nice cars 6 | 7 | 8 |

Nice cars!

9 |

10 | This page loads "large" images of fancy cars. It is meant to serve as a stress test for nodemcu-httpserver.
11 | It works with three embedded images of cars, but the server crashes with four. Edit this file and try it yourself.
12 | Whoever manages to modify nodemcu-httpserver to load all four images without crashing wins a prize! 13 |

14 |
Ferrari
15 |
Lamborghini
16 |
Maserati
17 |
Porsche
18 | 19 | 20 | -------------------------------------------------------------------------------- /http/upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Upload 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 | / 13 | /http 14 |
selected Directory:
bla
15 |
16 | Drop Here 17 | 18 | Browse 19 | Upload 20 |
21 |
22 | 23 | 25 |
Files on device:
26 | 28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /http/garage_door.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Garage control 5 | 10 | 11 | 12 |
13 |

Door 1

14 |
15 | 16 | 17 | 18 | 19 | 20 |
21 |

Door 2

22 |
23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /http/args.lua: -------------------------------------------------------------------------------- 1 | return function (connection, req, args) 2 | dofile("httpserver-header.lc")(connection, 200, 'html') 3 | connection:send([===[ 4 | Arguments by GET

Arguments by GET

5 | ]===]) 6 | 7 | if args.submit == nil then 8 | connection:send([===[ 9 |
10 | First name:

11 | Last name:

12 | MaleFemale
13 | 14 |
15 | ]===]) 16 | else 17 | connection:send("

Received the following values:

\n") 22 | end 23 | connection:send("") 24 | end 25 | 26 | -------------------------------------------------------------------------------- /http/led.lua: -------------------------------------------------------------------------------- 1 | local function sendResponse(connection, httpCode, status) 2 | connection:send("HTTP/1.0 "..httpCode.." OK\r\nContent-Type: application/json\r\nCache-Control: private, no-store\r\n\r\n") 3 | connection:send('{"on":'..tostring(status)..'}') 4 | end 5 | 6 | return function (connection, req, args) 7 | ONBOARD_LED = 0 8 | 9 | if _G.led_value == nil then 10 | _G.led_value = gpio.HIGH 11 | gpio.mode(ONBOARD_LED, gpio.OUTPUT) 12 | gpio.write(ONBOARD_LED, _G.led_value) 13 | end 14 | 15 | if req.method == "GET" then 16 | sendResponse(connection, 200, _G.led_value == gpio.HIGH and "false" or "true") 17 | return 18 | end 19 | 20 | if req.method == "POST" then 21 | if args.on ~= nil then 22 | _G.led_value = args.on == "true" and gpio.LOW or gpio.HIGH 23 | gpio.write(ONBOARD_LED, _G.led_value) 24 | sendResponse(connection, 200, _G.led_value == gpio.HIGH and "false" or "true") 25 | return 26 | end 27 | end 28 | 29 | sendResponse(connection, 400, _G.led_value == gpio.HIGH and "false" or "true") 30 | 31 | end 32 | -------------------------------------------------------------------------------- /httpserver-compile.lua: -------------------------------------------------------------------------------- 1 | -- httpserver-compile.lua 2 | -- Part of nodemcu-httpserver, compiles server code after upload. 3 | -- Author: Marcos Kirsch 4 | 5 | local compileAndRemoveIfNeeded = function(f) 6 | if file.exists(f) then 7 | local newf = f:gsub("%w+/", "") 8 | file.rename(f, newf) 9 | print('Compiling:', newf) 10 | node.compile(newf) 11 | file.remove(newf) 12 | collectgarbage() 13 | end 14 | end 15 | 16 | local serverFiles = { 17 | 'srv/httpserver.lua', 18 | 'srv/httpserver-b64decode.lua', 19 | 'srv/httpserver-basicauth.lua', 20 | 'srv/httpserver-buffer.lua', 21 | 'srv/httpserver-connection.lua', 22 | 'srv/httpserver-error.lua', 23 | 'srv/httpserver-header.lua', 24 | 'srv/httpserver-init.lua', 25 | 'srv/httpserver-request.lua', 26 | 'srv/httpserver-static.lua', 27 | 'srv/httpserver-wifi.lua', 28 | } 29 | 30 | local lfsFiles = { 31 | 'srv/_init.lua', 32 | 'srv/dummy_strings.lua', 33 | } 34 | for i, f in ipairs(serverFiles) do compileAndRemoveIfNeeded(f) end 35 | for i, f in ipairs(lfsFiles) do file.remove(f) end 36 | 37 | compileAndRemoveIfNeeded = nil 38 | serverFiles = nil 39 | lfsFiles = nil 40 | collectgarbage() 41 | -------------------------------------------------------------------------------- /http/post.lua: -------------------------------------------------------------------------------- 1 | return function (connection, req, args) 2 | dofile("httpserver-header.lc")(connection, 200, 'html') 3 | connection:send([===[ 4 | Arguments by POST

Arguments by POST

5 | ]===]) 6 | 7 | if req.method == "GET" then 8 | connection:send([===[ 9 |
10 | First name:

11 | Last name:

12 | MaleFemale
13 | 14 |
15 | ]===]) 16 | elseif req.method == "POST" then 17 | local rd = req.getRequestData() 18 | connection:send('

Received the following values:

') 19 | connection:send("\n") 24 | else 25 | connection:send("ERROR WTF req.method is ", req.method) 26 | end 27 | 28 | connection:send('') 29 | end 30 | -------------------------------------------------------------------------------- /http/file_list.lua: -------------------------------------------------------------------------------- 1 | return function (connection, req, args) 2 | dofile("httpserver-header.lc")(connection, 200, 'html') 3 | 4 | connection:send([===[ 5 | Server File Listing 6 | 7 |

Server File Listing

8 | ]===]) 9 | 10 | local remaining, used, total = file.fsinfo() 11 | connection:send("Total size: " .. total .. " bytes
\n" .. 12 | "In Use: " .. used .. " bytes
\n" .. 13 | "Free: " .. remaining .. " bytes
\n") 14 | 15 | local flashAddress, flashSize = file.fscfg () 16 | connection:send("Flash Address: " .. flashAddress .. " bytes
\n" .. 17 | "Flash Size: " .. flashSize .. " bytes
\n") 18 | 19 | connection:send("Files:
\n\n") 28 | end 29 | 30 | -------------------------------------------------------------------------------- /srv/httpserver-basicauth.lua: -------------------------------------------------------------------------------- 1 | -- httpserver-basicauth.lua 2 | -- Part of nodemcu-httpserver, authenticates a user using http basic auth. 3 | -- Author: Sam Dieck 4 | 5 | local basicAuth = {} 6 | 7 | -- Returns true if the user/password match one of the users/passwords in httpserver-conf.lua. 8 | -- Returns false otherwise. 9 | function loginIsValid(user, pwd, users) 10 | if user == nil then return false end 11 | if pwd == nil then return false end 12 | if users[user] == nil then return false end 13 | if users[user] ~= pwd then return false end 14 | return true 15 | end 16 | 17 | -- Parse basic auth http header. 18 | -- Returns the username if header contains valid credentials, 19 | -- nil otherwise. 20 | function basicAuth.authenticate(header) 21 | local conf = dofile("httpserver-conf.lua") 22 | local credentials_enc = header:match("Authorization: Basic ([A-Za-z0-9+/=]+)") 23 | if not credentials_enc then 24 | return nil 25 | end 26 | local credentials = dofile("httpserver-b64decode.lc")(credentials_enc) 27 | local user, pwd = credentials:match("^(.*):(.*)$") 28 | if loginIsValid(user, pwd, conf.auth.users) then 29 | print("httpserver-basicauth: User \"" .. user .. "\": Authenticated.") 30 | return user 31 | else 32 | print("httpserver-basicauth: User \"" .. user .. "\": Access denied.") 33 | return nil 34 | end 35 | end 36 | 37 | function basicAuth.authErrorHeader() 38 | local conf = dofile("httpserver-conf.lua") 39 | return "WWW-Authenticate: Basic realm=\"" .. conf.auth.realm .. "\"" 40 | end 41 | 42 | return basicAuth 43 | -------------------------------------------------------------------------------- /srv/httpserver-buffer.lua: -------------------------------------------------------------------------------- 1 | -- httpserver-buffer 2 | -- Part of nodemcu-httpserver, provides a buffer that behaves like a connection object 3 | -- that can handle multiple consecutive send() calls, and buffers small payloads up to 1400 bytes. 4 | -- This is primarily user to collect the send requests done by the head script. 5 | -- The owner is responsible to call getBuffer and send its result 6 | -- Author: Gregor Hartmann 7 | 8 | local Buffer = {} 9 | 10 | -- parameter is the nodemcu-firmware connection 11 | function Buffer:new() 12 | local newInstance = {} 13 | newInstance.size = 0 14 | newInstance.data = {} 15 | 16 | -- Returns true if there was any data to be sent. 17 | function newInstance:getBuffer() 18 | local buffer = table.concat(self.data, "") 19 | self.data = {} 20 | self.size = 0 21 | return buffer 22 | end 23 | 24 | function newInstance:getpeer() 25 | return "no peer" 26 | end 27 | 28 | function newInstance:send(payload) 29 | local flushThreshold = 1400 30 | if (not payload) then print("nop payload") end 31 | local newSize = self.size + payload:len() 32 | if newSize >= flushThreshold then 33 | print("Buffer is full. Cutting off "..newSize-flushThreshold.." chars") 34 | --STEP1: cut out piece from payload to complete threshold bytes in table 35 | local pieceSize = flushThreshold - self.size 36 | if pieceSize then 37 | payload = payload:sub(1, pieceSize) 38 | end 39 | end 40 | table.insert(self.data, payload) 41 | self.size = self.size + #payload 42 | end 43 | 44 | return newInstance 45 | 46 | end 47 | 48 | return Buffer 49 | -------------------------------------------------------------------------------- /http/node_info.lua: -------------------------------------------------------------------------------- 1 | local function sendAttr(connection, attr, val, unit) 2 | --Avoid error when Nil is in atrib=val pair. 3 | if not attr or not val then 4 | return 5 | else 6 | if unit then 7 | unit = ' ' .. unit 8 | else 9 | unit = '' 10 | end 11 | connection:send("
  • ".. attr .. ": " .. val .. unit .. "
  • \n") 12 | end 13 | end 14 | 15 | return function (connection, req, args) 16 | dofile("httpserver-header.lc")(connection, 200, 'html') 17 | connection:send('A Lua script sample

    Node info

    ') 33 | end 34 | -------------------------------------------------------------------------------- /srv/httpserver-header.lua: -------------------------------------------------------------------------------- 1 | -- httpserver-header.lua 2 | -- Part of nodemcu-httpserver, knows how to send an HTTP header. 3 | -- Author: Marcos Kirsch 4 | 5 | return function(connection, code, extension, isGzipped, extraHeaders) 6 | 7 | local function getHTTPStatusString(code) 8 | local codez = { [200] = "OK", [400] = "Bad Request", [401] = "Unauthorized", [404] = "Not Found", [405] = "Method Not Allowed", [500] = "Internal Server Error", [501] = "Not Implemented", } 9 | local myResult = codez[code] 10 | -- enforce returning valid http codes all the way throughout? 11 | if myResult then return myResult else return "Not Implemented" end 12 | end 13 | 14 | local function getMimeType(ext) 15 | -- A few MIME types. Keep list short. If you need something that is missing, let's add it. 16 | local mt = {css = "text/css", gif = "image/gif", html = "text/html", ico = "image/x-icon", jpeg = "image/jpeg", 17 | jpg = "image/jpeg", js = "application/javascript", json = "application/json", png = "image/png", xml = "text/xml"} 18 | if mt[ext] then return mt[ext] else return "application/octet-stream" end 19 | end 20 | 21 | local mimeType = getMimeType(extension) 22 | local statusString = getHTTPStatusString(code) 23 | 24 | connection:send("HTTP/1.0 " .. code .. " " .. statusString .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\n") 25 | if isGzipped then 26 | connection:send("Cache-Control: private, max-age=2592000\r\nContent-Encoding: gzip\r\n") 27 | end 28 | if (extraHeaders) then 29 | for i, extraHeader in ipairs(extraHeaders) do 30 | connection:send(extraHeader .. "\r\n") 31 | end 32 | end 33 | 34 | connection:send("Connection: close\r\n\r\n") 35 | return statusString 36 | end 37 | 38 | -------------------------------------------------------------------------------- /http/led.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Led control 6 | 26 | 54 | 55 | 56 | 57 |
    58 |

    On board LED

    59 | 60 |
    61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /srv/dummy_strings.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- File: LFS_dummy_strings.lua 3 | --[[ 4 | luac.cross -f generates a ROM string table which is part of the compiled LFS 5 | image. This table includes all strings referenced in the loaded modules. 6 | 7 | If you want to preload other string constants, then one way to achieve this is 8 | to include a dummy module in the LFS that references the strings that you want 9 | to load. You never need to call this module; it's inclusion in the LFS image is 10 | enough to add the strings to the ROM table. Your application can use any strings 11 | in the ROM table without incuring any RAM or Lua Garbage Collector (LGC) 12 | overhead. 13 | 14 | The local preload example is a useful starting point. However, if you call the 15 | following code in your application during testing, then this will provide a 16 | listing of the current RAM string table. 17 | 18 | do 19 | local a=debug.getstrings'RAM' 20 | for i =1, #a do a[i] = ('%q'):format(a[i]) end 21 | print ('local preload='..table.concat(a,',')) 22 | end 23 | 24 | This will exclude any strings already in the ROM table, so the output is the list 25 | of putative strings that you should consider adding to LFS ROM table. 26 | 27 | ---------------------------------------------------------------------------------]] 28 | 29 | local preload = "?.lc;?.lua", "/\n;\n?\n!\n-", "@init.lua", "_G", "_LOADED", 30 | "_LOADLIB", "__add", "__call", "__concat", "__div", "__eq", "__gc", "__index", 31 | "__le", "__len", "__lt", "__mod", "__mode", "__mul", "__newindex", "__pow", 32 | "__sub", "__tostring", "__unm", "collectgarbage", "cpath", "debug", "file", 33 | "file.obj", "file.vol", "flash", "getstrings", "index", "ipairs", "list", "loaded", 34 | "loader", "loaders", "loadlib", "module", "net.tcpserver", "net.tcpsocket", 35 | "net.udpsocket", "newproxy", "package", "pairs", "path", "preload", "reload", 36 | "require", "seeall", "wdclr", "not enough memory", "sjson.decoder","sjson.encoder", 37 | "tmr.timer" 38 | -------------------------------------------------------------------------------- /srv/httpserver-init.lua: -------------------------------------------------------------------------------- 1 | -- httpserver-init.lua 2 | -- Part of nodemcu-httpserver, launches the server. 3 | -- Author: Marcos Kirsch 4 | 5 | -- Function for starting the server. 6 | -- If you compiled the mdns module, then it will also register with mDNS. 7 | local startServer = function(ip) 8 | local conf = dofile('httpserver-conf.lua') 9 | if (dofile("httpserver.lc")(conf['general']['port'])) then 10 | print("nodemcu-httpserver running at:") 11 | print(" http://" .. ip .. ":" .. conf['general']['port']) 12 | if (mdns) then 13 | mdns.register(conf['mdns']['hostname'], { description=conf['mdns']['description'], service="http", port=conf['general']['port'], location=conf['mdns']['location'] }) 14 | print (' http://' .. conf['mdns']['hostname'] .. '.local.:' .. conf['general']['port']) 15 | end 16 | end 17 | conf = nil 18 | end 19 | 20 | if (wifi.getmode() == wifi.STATION) or (wifi.getmode() == wifi.STATIONAP) then 21 | 22 | -- Connect to the WiFi access point and start server once connected. 23 | -- If the server loses connectivity, server will restart. 24 | wifi.eventmon.register(wifi.eventmon.STA_GOT_IP, function(args) 25 | print("Connected to WiFi Access Point. Got IP: " .. args["IP"]) 26 | startServer(args["IP"]) 27 | wifi.eventmon.register(wifi.eventmon.STA_DISCONNECTED, function(args) 28 | print("Lost connectivity! Restarting...") 29 | node.restart() 30 | end) 31 | end) 32 | 33 | -- What if after a while (30 seconds) we didn't connect? Restart and keep trying. 34 | local watchdogTimer = tmr.create() 35 | watchdogTimer:register(30000, tmr.ALARM_SINGLE, function (watchdogTimer) 36 | local ip = wifi.sta.getip() 37 | if (not ip) then ip = wifi.ap.getip() end 38 | if ip == nil then 39 | print("No IP after a while. Restarting...") 40 | node.restart() 41 | else 42 | --print("Successfully got IP. Good, no need to restart.") 43 | watchdogTimer:unregister() 44 | end 45 | end) 46 | watchdogTimer:start() 47 | 48 | 49 | else 50 | 51 | startServer(wifi.ap.getip()) 52 | 53 | end 54 | -------------------------------------------------------------------------------- /httpserver-conf.lua: -------------------------------------------------------------------------------- 1 | -- httpserver-conf.lua 2 | -- Part of nodemcu-httpserver, contains static configuration for httpserver. 3 | -- Edit your server's configuration below. 4 | -- Author: Sam Dieck 5 | 6 | local conf = {} 7 | 8 | -- General server configuration. 9 | conf.general = {} 10 | -- TCP port in which to listen for incoming HTTP requests. 11 | conf.general.port = 80 12 | 13 | -- WiFi configuration 14 | conf.wifi = {} 15 | -- Can be wifi.STATION, wifi.SOFTAP, or wifi.STATIONAP 16 | conf.wifi.mode = wifi.STATION 17 | -- Theses apply only when configured as Access Point (wifi.SOFTAP or wifi.STATIONAP) 18 | if (conf.wifi.mode == wifi.SOFTAP) or (conf.wifi.mode == wifi.STATIONAP) then 19 | conf.wifi.accessPoint = {} 20 | conf.wifi.accessPoint.config = {} 21 | conf.wifi.accessPoint.config.ssid = "ESP-"..node.chipid() -- Name of the WiFi network to create. 22 | conf.wifi.accessPoint.config.pwd = "ESP-"..node.chipid() -- WiFi password for joining - at least 8 characters 23 | conf.wifi.accessPoint.net = {} 24 | conf.wifi.accessPoint.net.ip = "192.168.111.1" 25 | conf.wifi.accessPoint.net.netmask="255.255.255.0" 26 | conf.wifi.accessPoint.net.gateway="192.168.111.1" 27 | end 28 | -- These apply only when connecting to a router as a client 29 | if (conf.wifi.mode == wifi.STATION) or (conf.wifi.mode == wifi.STATIONAP) then 30 | conf.wifi.station = {} 31 | conf.wifi.station.ssid = "Internet" -- Name of the WiFi network you want to join 32 | conf.wifi.station.pwd = "" -- Password for the WiFi network 33 | end 34 | 35 | -- mDNS, applies if you compiled the mdns module in your firmware. 36 | conf.mdns = {} 37 | conf.mdns.hostname = 'nodemcu' -- You will be able to access your server at "http://nodemcu.local." 38 | conf.mdns.location = 'Earth' 39 | conf.mdns.description = 'A tiny HTTP server' 40 | 41 | -- Basic HTTP Authentication. 42 | conf.auth = {} 43 | -- Set to true if you want to enable. 44 | conf.auth.enabled = false 45 | -- Displayed in the login dialog users see before authenticating. 46 | conf.auth.realm = "nodemcu" 47 | -- Add users and passwords to this table. Do not leave this unchanged if you enable authentication! 48 | conf.auth.users = {user1 = "password1", user2 = "password2", user3 = "password3"} 49 | 50 | return conf 51 | -------------------------------------------------------------------------------- /http/cars.lua: -------------------------------------------------------------------------------- 1 | return function (connection, req, args) 2 | 3 | local function showCars(nr) 4 | if not nr then return end 5 | connection:send([===[
    Ferrari
    ]===]) 6 | if nr == "1" then return end 7 | connection:send([===[
    Lamborghini
    ]===]) 8 | if nr == "2" then return end 9 | connection:send([===[
    Maserati
    ]===]) 10 | if nr == "3" then return end 11 | connection:send([===[
    Porsche
    ]===]) 12 | if nr == "4" then return end 13 | connection:send([===[
    Bugatti
    ]===]) 14 | if nr == "5" then return end 15 | connection:send([===[
    Mercedes
    ]===]) 16 | end 17 | 18 | 19 | dofile("httpserver-header.lc")(connection, 200, 'html') 20 | connection:send([===[ 21 | 22 | 23 | 24 | 25 | Nice cars 26 | 27 | 28 |

    Nice cars!

    29 |

    30 | This page loads "large" images of fancy cars. It is meant to serve as a stress test for nodemcu-httpserver.
    31 | It works with three embedded images of cars, but the server crashes with four. Select the number of cars you want to see below.
    32 | Whoever manages to modify nodemcu-httpserver to load all four images without crashing wins a prize! 33 |

    34 |

    35 | OK I guess I win the prize, as now you can load five cars.
    36 | Cheers HHHartmann 37 |

    38 |

    39 | choose: show one car 40 | show two cars 41 | show three cars 42 | show four cars 43 | show five cars 44 | show six cars 45 |

    46 | ]===]) 47 | 48 | showCars(args.n) 49 | 50 | connection:send([===[ 51 | 52 | 53 | ]===]) 54 | end 55 | 56 | -------------------------------------------------------------------------------- /srv/httpserver-connection.lua: -------------------------------------------------------------------------------- 1 | -- httpserver-connection 2 | -- Part of nodemcu-httpserver, provides a buffered connection object that can handle multiple 3 | -- consecutive send() calls, and buffers small payloads to send once they get big. 4 | -- For this to work, it must be used from a coroutine and owner is responsible for the final 5 | -- flush() and for closing the connection. 6 | -- Author: Philip Gladstone, Marcos Kirsch 7 | 8 | local BufferedConnection = {} 9 | 10 | -- parameter is the nodemcu-firmware connection 11 | function BufferedConnection:new(connection) 12 | local newInstance = {} 13 | newInstance.connection = connection 14 | newInstance.size = 0 15 | newInstance.data = {} 16 | 17 | -- Returns true if there was any data to be sent. 18 | function newInstance:flush() 19 | if self.size > 0 then 20 | self.connection:send(table.concat(self.data, "")) 21 | self.data = {} 22 | self.size = 0 23 | return true 24 | end 25 | return false 26 | end 27 | 28 | function newInstance:getpeer() 29 | return self.connection:getpeer() 30 | end 31 | 32 | function newInstance:send(payload) 33 | local flushThreshold = 1400 34 | local newSize = self.size + payload:len() 35 | while newSize >= flushThreshold do 36 | --STEP1: cut out piece from payload to complete threshold bytes in table 37 | local pieceSize = flushThreshold - self.size 38 | local piece = payload:sub(1, pieceSize) 39 | payload = payload:sub(pieceSize + 1, -1) 40 | --STEP2: insert piece into table 41 | table.insert(self.data, piece) 42 | piece = nil 43 | self.size = self.size + pieceSize --size should be same as flushThreshold 44 | --STEP3: flush entire table 45 | if self:flush() then 46 | coroutine.yield() 47 | end 48 | --at this point, size should be 0, because the table was just flushed 49 | newSize = self.size + payload:len() 50 | end 51 | 52 | --at this point, whatever is left in payload should be < flushThreshold 53 | if payload:len() ~= 0 then 54 | --leave remaining data in the table 55 | table.insert(self.data, payload) 56 | self.size = self.size + payload:len() 57 | end 58 | end 59 | return newInstance 60 | 61 | end 62 | 63 | return BufferedConnection 64 | -------------------------------------------------------------------------------- /http/garage_door_control.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height:100%; 3 | margin: 0; 4 | overflow: hidden; 5 | } 6 | 7 | body { 8 | 9 | text-align: center; 10 | background-color: black; 11 | min-height: 100%; 12 | color: black; 13 | } 14 | 15 | 16 | #remote { 17 | background-color: #666; 18 | width: 90%; 19 | border-radius: 30px; 20 | margin: 5% 5% 0; 21 | height: 90%; 22 | padding: 0; 23 | } 24 | 25 | #spacer { 26 | clear: both; 27 | border-top: 1px solid rgba(0, 0, 0, 0.5); 28 | -moz-box-shadow: 1px 1px 1px; 29 | box-shadow: 1px 1px 1px; 30 | margin-right: 30px; 31 | margin-left: 30px; 32 | } 33 | 34 | 35 | .button { 36 | display: inline-block; 37 | width: 43%; 38 | margin: 20px 0 30px; 39 | padding: 40px 0; 40 | border-style: none; 41 | color: rgba(192, 192, 192, 0.5); 42 | text-decoration: none; 43 | border-radius: 20px; 44 | text-shadow: 0 0 1px rgba(0, 0, 0, 0.5); 45 | font-size: 130px; 46 | font-weight: bold; 47 | background-color: #CCC; 48 | -moz-box-shadow: 0 10px rgba(0, 0, 0, 0.25); 49 | box-shadow: 0 10px rgba(0, 0, 0, 0.25); 50 | position: relative; 51 | } 52 | 53 | 54 | .button-1 { 55 | float: left; 56 | margin-left: 5%; 57 | } 58 | 59 | .button-2 { 60 | float: right; 61 | margin-right: 5%; 62 | } 63 | 64 | 65 | 66 | .button span { 67 | 68 | 69 | } 70 | 71 | 72 | 73 | .button:hover span { 74 | 75 | 76 | } 77 | 78 | 79 | .button:active, .button:focus { 80 | 81 | 82 | } 83 | 84 | 85 | 86 | .button:active span { 87 | 88 | 89 | } 90 | 91 | 92 | #label { 93 | font-family: "Lucida Grande", Lucida, Verdana, sans-serif; 94 | background-color: rgba(0, 0, 0, 0.1); 95 | width: 12px; 96 | height: 12px; 97 | display: block; 98 | margin: 20px auto; 99 | -webkit-border-radius: 20px; 100 | -moz-border-radius: 20px; 101 | border-radius: 20px; 102 | text-indent: -99999px; 103 | top: 20px; 104 | position: relative; 105 | } 106 | 107 | #label.start { 108 | 109 | } 110 | 111 | #label.initalizing { 112 | 113 | } 114 | 115 | #label.connection { 116 | background-color: orange; 117 | } 118 | 119 | #label.received { 120 | background-color: orange; 121 | } 122 | 123 | #label.processing { 124 | background-color: orange; 125 | } 126 | 127 | #label.ok { 128 | background-color: green; 129 | } 130 | 131 | #label.bad { 132 | background-color: red; 133 | } 134 | 135 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | # Makefile user configuration 3 | ###################################################################### 4 | 5 | # Path to nodemcu-uploader (https://github.com/kmpm/nodemcu-uploader) 6 | NODEMCU-UPLOADER?=python ../nodemcu-uploader/nodemcu-uploader.py 7 | 8 | # Path to LUA cross compiler (part of the nodemcu firmware; only needed to compile the LFS image yourself) 9 | LUACC?=../nodemcu-firmware/luac.cross 10 | 11 | # Serial port 12 | PORT?=$(shell ls /dev/cu.SLAB_USBtoUART /dev/ttyUSB* 2>/dev/null|head -n1) 13 | SPEED?=115200 14 | 15 | define _upload 16 | @$(NODEMCU-UPLOADER) -b $(SPEED) --start_baud $(SPEED) -p $(PORT) upload $^ 17 | endef 18 | 19 | ###################################################################### 20 | 21 | LFS_IMAGE ?= lfs.img 22 | HTTP_FILES := $(wildcard http/*) 23 | WIFI_CONFIG := $(wildcard *conf*.lua) 24 | SERVER_FILES := $(filter-out $(WIFI_CONFIG), $(wildcard srv/*.lua) $(wildcard *.lua)) 25 | LFS_FILES := $(LFS_IMAGE) $(filter-out $(WIFI_CONFIG), $(wildcard *.lua)) 26 | FILE ?= 27 | 28 | # Print usage 29 | usage: 30 | @echo "make upload FILE:= to upload a specific file (i.e make upload FILE:=init.lua)" 31 | @echo "make upload_http to upload files to be served" 32 | @echo "make upload_server to upload the server code and init.lua" 33 | @echo "make upload_all to upload all" 34 | @echo "make upload_lfs to upload lfs based server code" 35 | @echo "make upload_all_lfs to upload all (LFS based)" 36 | 37 | # Upload one file only 38 | upload: $(FILE) 39 | $(_upload) 40 | 41 | # Upload HTTP files only 42 | upload_http: $(HTTP_FILES) 43 | $(_upload) 44 | 45 | # Upload httpserver lua files 46 | upload_server: $(SERVER_FILES) 47 | $(_upload) 48 | 49 | # Upload wifi configuration 50 | upload_wifi_config: $(WIFI_CONFIG) 51 | $(_upload) 52 | 53 | # Upload lfs image 54 | upload_lfs: $(LFS_FILES) 55 | $(_upload) 56 | 57 | $(LFS_IMAGE): 58 | $(LUACC) -f -o $(LFS_IMAGE) srv/*.lua 59 | 60 | # Upload all non-lfs files 61 | upload_all: $(HTTP_FILES) $(SERVER_FILES) $(WIFI_CONFIG) 62 | $(_upload) 63 | 64 | # Upload all lfs files 65 | upload_all_lfs: $(HTTP_FILES) $(LFS_FILES) $(WIFI_CONFIG) 66 | $(_upload) 67 | 68 | .ENTRY: usage 69 | .PHONY: usage upload_http upload_server upload_wifi_config \ 70 | upload_lfs upload_all upload_all_lfs 71 | -------------------------------------------------------------------------------- /srv/httpserver-b64decode.lua: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/lua 2 | -- httpserver-b64decode.lua 3 | -- Part of nodemcu-httpserver, contains b64 decoding used for HTTP Basic Authentication. 4 | -- Modified to use an exponentiation by multiplication method for only applicable for unsigned integers. 5 | -- Based on http://lua-users.org/wiki/BaseSixtyFour by Alex Kloss 6 | -- compatible with lua 5.1 7 | -- http://www.it-rfc.de 8 | -- Author: Marcos Kirsch 9 | 10 | local function uipow(a, b) 11 | local ret = 1 12 | if b >= 0 then 13 | for i = 1, b do 14 | ret = ret * a 15 | end 16 | end 17 | return ret 18 | end 19 | 20 | -- bitshift functions (<<, >> equivalent) 21 | -- shift left 22 | local function lsh(value,shift) 23 | return (value*(uipow(2, shift))) % 256 24 | end 25 | 26 | -- shift right 27 | local function rsh(value,shift) 28 | -- Lua builds with no floating point don't define math. 29 | if math then return math.floor(value/uipow(2, shift)) % 256 end 30 | return (value/uipow(2, shift)) % 256 31 | end 32 | 33 | -- return single bit (for OR) 34 | local function bit(x,b) 35 | return (x % uipow(2, b) - x % uipow(2, (b-1)) > 0) 36 | end 37 | 38 | -- logic OR for number values 39 | local function lor(x,y) 40 | local result = 0 41 | for p=1,8 do result = result + (((bit(x,p) or bit(y,p)) == true) and uipow(2, (p-1)) or 0) end 42 | return result 43 | end 44 | 45 | -- Character decoding table 46 | local function toBase64Byte(char) 47 | local ascii = string.byte(char, 1) 48 | if ascii >= string.byte('A', 1) and ascii <= string.byte('Z', 1) then return ascii - string.byte('A', 1) 49 | elseif ascii >= string.byte('a', 1) and ascii <= string.byte('z', 1) then return ascii - string.byte('a', 1) + 26 50 | elseif ascii >= string.byte('0', 1) and ascii <= string.byte('9', 1) then return ascii + 4 51 | elseif ascii == string.byte('-', 1) then return 62 52 | elseif ascii == string.byte('_', 1) then return 63 53 | elseif ascii == string.byte('=', 1) then return nil 54 | else return nil, "ERROR! Char is invalid for Base64 encoding: "..char end 55 | end 56 | 57 | 58 | -- decode base64 input to string 59 | return function(data) 60 | local chars = {} 61 | local result="" 62 | for dpos=0,string.len(data)-1,4 do 63 | for char=1,4 do chars[char] = toBase64Byte((string.sub(data,(dpos+char),(dpos+char)) or "=")) end 64 | result = string.format( 65 | '%s%s%s%s', 66 | result, 67 | string.char(lor(lsh(chars[1],2), rsh(chars[2],4))), 68 | (chars[3] ~= nil) and string.char(lor(lsh(chars[2],4), 69 | rsh(chars[3],2))) or "", 70 | (chars[4] ~= nil) and string.char(lor(lsh(chars[3],6) % 192, 71 | (chars[4]))) or "" 72 | ) 73 | end 74 | return result 75 | end 76 | -------------------------------------------------------------------------------- /http/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Served by an ESP8266 6 | 11 | 12 | 13 |

    Hello World!

    14 |
    15 | under construction 16 |
    17 |

    18 | This page is served by nodemcu-httpserver running on an ESP8266 that uses the NodeMCU firmware. 19 | NodeMCU puts a Lua interpreter inside the ESP8266. This is surely one of the smallest web servers to date! 20 |

    21 | 22 |

    Where's the source code?

    23 |

    You can find the Lua code for nodemcu-httpserver in GitHub

    24 | 25 |

    Serve me some pages!

    26 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /http/garage_door_control.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Garage Remote 8 | 70 | 75 | 76 | 77 |
    78 |
    79 | 80 | I 81 | 82 | 83 | II 84 | 85 |
    86 |
    87 | 88 | 89 | -------------------------------------------------------------------------------- /http/upload.lua: -------------------------------------------------------------------------------- 1 | return function (connection, req, args) 2 | dofile("httpserver-header.lc")(connection, 200, 'json') 3 | connection:send('{') 4 | 5 | local mbOffset = nil 6 | local mbLen = nil 7 | local mbData = nil 8 | local mbCmd = nil 9 | local mbFilename = nil 10 | local fieldsCount = 0 11 | local fileSize = 0 12 | local i = 0 13 | local binaryData = '' 14 | local currentByte = nil 15 | 16 | for name, value in pairs(args) do 17 | if (name == "offset") then 18 | mbOffset = tonumber(value, 10) 19 | 20 | fieldsCount = fieldsCount + 1 21 | end 22 | if (name == "len") then 23 | mbLen = tonumber(value, 10) 24 | 25 | fieldsCount = fieldsCount + 1 26 | end 27 | if (name == "data") then 28 | mbData = value 29 | 30 | fieldsCount = fieldsCount + 1 31 | end 32 | if (name == "filename") then 33 | mbFilename = value 34 | 35 | fieldsCount = fieldsCount + 1 36 | end 37 | if (name == "filesize") then 38 | fileSize = tonumber(value, 10) 39 | 40 | fieldsCount = fieldsCount + 1 41 | end 42 | if (name == "cmd") then 43 | mbCmd = value 44 | 45 | fieldsCount = fieldsCount + 1 46 | end 47 | end 48 | 49 | if (mbCmd == 'upload') then 50 | if (fieldsCount > 5) then 51 | if (mbFilename ~= 'http/upload.lua') then 52 | connection:send('"offset":"' .. mbOffset .. '",') 53 | connection:send('"len":"' .. mbLen .. '",') 54 | connection:send('"filename":"' .. mbFilename .. '"') 55 | 56 | for i=1,string.len(mbData),2 do 57 | currentByte = tonumber(string.sub(mbData, i, i + 1), 16) 58 | binaryData = binaryData .. string.char(currentByte) 59 | end 60 | 61 | local mbTmpFilename = string.sub(mbFilename, 0, 27) .. '.dnl' 62 | if (mbOffset > 0) then 63 | file.open(mbTmpFilename,'a+') 64 | else 65 | file.remove(mbTmpFilename) 66 | file.open(mbTmpFilename,'w+') 67 | end 68 | file.seek("set", mbOffset) 69 | file.write(binaryData) 70 | file.close() 71 | 72 | binaryData = nil 73 | 74 | if (fileSize == mbLen + mbOffset) then 75 | file.remove(mbFilename) 76 | file.rename(mbTmpFilename, mbFilename) 77 | file.remove(mbTmpFilename) 78 | 79 | if (string.sub(mbFilename, -4) == '.lua') then 80 | file.remove(string.sub(mbFilename, 0, -3) .. "lc") 81 | node.compile(mbFilename) 82 | file.remove(mbFilename) 83 | end 84 | end 85 | end 86 | end 87 | elseif (mbCmd == 'list') then 88 | local remaining, used, total=file.fsinfo() 89 | 90 | local headerExist = 0 91 | 92 | connection:send('"files":{') 93 | 94 | for name, size in pairs(file.list()) do 95 | if (headerExist > 0) then 96 | connection:send(',') 97 | end 98 | 99 | local url = string.match(name, ".*/(.*)") 100 | url = name 101 | connection:send('"' .. url .. '":"' .. size .. '"') 102 | 103 | headerExist = 1 104 | end 105 | 106 | connection:send('},') 107 | 108 | connection:send('"total":"' .. total .. '",') 109 | connection:send('"used":"' .. used .. '",') 110 | connection:send('"free":"' .. remaining .. '"') 111 | elseif (mbCmd == 'remove') then 112 | if (fieldsCount > 1) then 113 | if (mbFilename ~= 'http/upload.lua') and (mbFilename ~= 'http/upload.lc') and (mbFilename ~= 'http/upload.html.gz') then 114 | file.remove(mbFilename) 115 | end 116 | end 117 | end 118 | 119 | connection:send('}') 120 | collectgarbage() 121 | end 122 | -------------------------------------------------------------------------------- /srv/_init.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- File: _init.lua 3 | --[[ 4 | 5 | This is a template for the LFS equivalent of the SPIFFS init.lua. 6 | 7 | It is a good idea to such an _init.lua module to your LFS and do most of the LFS 8 | module related initialisaion in this. This example uses standard Lua features to 9 | simplify the LFS API. 10 | 11 | For Lua 5.1, the first section adds a 'LFS' table to _G and uses the __index 12 | metamethod to resolve functions in the LFS, so you can execute the main 13 | function of module 'fred' by executing LFS.fred(params), etc. 14 | It also implements some standard readonly properties: 15 | 16 | LFS._time The Unix Timestamp when the luac.cross was executed. This can be 17 | used as a version identifier. 18 | 19 | LFS._config This returns a table of useful configuration parameters, hence 20 | print (("0x%6x"):format(LFS._config.lfs_base)) 21 | gives you the parameter to use in the luac.cross -a option. 22 | 23 | LFS._list This returns a table of the LFS modules, hence 24 | print(table.concat(LFS._list,'\n')) 25 | gives you a single column listing of all modules in the LFS. 26 | 27 | For Lua 5.3 LFS table is populated by the LFS implementation in C so this part 28 | of the code is skipped. 29 | ---------------------------------------------------------------------------------]] 30 | 31 | local lfsindex = node.LFS and node.LFS.get or node.flashindex 32 | local G=_ENV or getfenv() 33 | local lfs_t 34 | if _VERSION == 'Lua 5.1' then 35 | lfs_t = { 36 | __index = function(_, name) 37 | local fn_ut, ba, ma, size, modules = lfsindex(name) 38 | if not ba then 39 | return fn_ut 40 | elseif name == '_time' then 41 | return fn_ut 42 | elseif name == '_config' then 43 | local fs_ma, fs_size = file.fscfg() 44 | return {lfs_base = ba, lfs_mapped = ma, lfs_size = size, 45 | fs_mapped = fs_ma, fs_size = fs_size} 46 | elseif name == '_list' then 47 | return modules 48 | else 49 | return nil 50 | end 51 | end, 52 | 53 | __newindex = function(_, name, value) -- luacheck: no unused 54 | error("LFS is readonly. Invalid write to LFS." .. name, 2) 55 | end, 56 | } 57 | 58 | setmetatable(lfs_t,lfs_t) 59 | G.module = nil -- disable Lua 5.0 style modules to save RAM 60 | package.seeall = nil 61 | else 62 | lfs_t = node.LFS 63 | end 64 | G.LFS = lfs_t 65 | 66 | --[[------------------------------------------------------------------------------- 67 | The second section adds the LFS to the require searchlist, so that you can 68 | require a Lua module 'jean' in the LFS by simply doing require "jean". However 69 | note that this is at the search entry following the FS searcher, so if you also 70 | have jean.lc or jean.lua in SPIFFS, then this SPIFFS version will get loaded into 71 | RAM instead of using. (Useful, for development). 72 | 73 | See docs/en/lfs.md and the 'loaders' array in app/lua/loadlib.c for more details. 74 | 75 | ---------------------------------------------------------------------------------]] 76 | 77 | package.loaders[3] = function(module) -- loader_flash 78 | return lfs_t[module] 79 | end 80 | 81 | --[[---------------------------------------------------------------------------- 82 | These replace the builtins loadfile & dofile with ones which preferentially 83 | load from the filesystem and fall back to LFS. Flipping the search order 84 | is an exercise left to the reader.- 85 | ------------------------------------------------------------------------------]] 86 | 87 | local lf = loadfile 88 | G.loadfile = function(n) 89 | if file.exists(n) then return lf(n) end 90 | local mod = n:match("(.*)%.l[uc]a?$") 91 | local fn = mod and lfsindex(mod) 92 | return (fn or error (("Cannot find '%s' in FS or LFS"):format(n))) and fn 93 | end 94 | 95 | -- Lua's dofile (luaB_dofile) reaches directly for luaL_loadfile; shim instead 96 | G.dofile = function(n) return assert(loadfile(n))() end 97 | -------------------------------------------------------------------------------- /srv/httpserver-request.lua: -------------------------------------------------------------------------------- 1 | -- httpserver-request 2 | -- Part of nodemcu-httpserver, parses incoming client requests. 3 | -- Author: Marcos Kirsch 4 | 5 | local function validateMethod(method) 6 | local httpMethods = {GET=true, HEAD=true, POST=true, PUT=true, DELETE=true, TRACE=true, OPTIONS=true, CONNECT=true, PATCH=true} 7 | -- default for non-existent attributes returns nil, which evaluates to false 8 | return httpMethods[method] 9 | end 10 | 11 | local function uriToFilename(uri) 12 | return "http/" .. string.sub(uri, 2, -1) 13 | end 14 | 15 | local function hex_to_char(x) 16 | return string.char(tonumber(x, 16)) 17 | end 18 | 19 | local function uri_decode(input) 20 | return input:gsub("%+", " "):gsub("%%(%x%x)", hex_to_char) 21 | end 22 | 23 | local function parseArgs(args) 24 | local r = {} 25 | local i = 1 26 | if args == nil or args == "" then return r end 27 | for arg in string.gmatch(args, "([^&]+)") do 28 | local name, value = string.match(arg, "(.*)=(.*)") 29 | if name ~= nil then r[name] = uri_decode(value) end 30 | i = i + 1 31 | end 32 | return r 33 | end 34 | 35 | local function parseFormData(body) 36 | local data = {} 37 | --print("Parsing Form Data") 38 | for kv in body.gmatch(body, "%s*&?([^=]+=[^&]+)") do 39 | local key, value = string.match(kv, "(.*)=(.*)") 40 | --print("Parsed: " .. key .. " => " .. value) 41 | data[key] = uri_decode(value) 42 | end 43 | return data 44 | end 45 | 46 | local function getRequestData(payload) 47 | local requestData 48 | return function () 49 | --print("Getting Request Data") 50 | -- for backward compatibility before v2.1 51 | if (sjson == nil) then 52 | sjson = cjson 53 | end 54 | if requestData then 55 | return requestData 56 | else 57 | --print("payload = [" .. payload .. "]") 58 | local mimeType = string.match(payload, "Content%-Type: ([%w/-]+)") 59 | local bodyStart = payload:find("\r\n\r\n", 1, true) 60 | local body = payload:sub(bodyStart, #payload) 61 | payload = nil 62 | collectgarbage() 63 | --print("mimeType = [" .. mimeType .. "]") 64 | --print("bodyStart = [" .. bodyStart .. "]") 65 | --print("body = [" .. body .. "]") 66 | if mimeType == "application/json" then 67 | --print("JSON: " .. body) 68 | requestData = sjson.decode(body) 69 | elseif mimeType == "application/x-www-form-urlencoded" then 70 | requestData = parseFormData(body) 71 | else 72 | requestData = {} 73 | end 74 | return requestData 75 | end 76 | end 77 | end 78 | 79 | local function parseUri(uri) 80 | local r = {} 81 | local filename 82 | local ext 83 | local fullExt = {} 84 | 85 | if uri == nil then return r end 86 | if uri == "/" then uri = "/index.html" end 87 | local questionMarkPos, b, c, d, e, f = uri:find("?") 88 | if questionMarkPos == nil then 89 | r.file = uri:sub(1, questionMarkPos) 90 | r.args = {} 91 | else 92 | r.file = uri:sub(1, questionMarkPos - 1) 93 | r.args = parseArgs(uri:sub(questionMarkPos+1, #uri)) 94 | end 95 | filename = r.file 96 | while filename:match("%.") do 97 | filename,ext = filename:match("(.+)%.(.+)") 98 | table.insert(fullExt,1,ext) 99 | end 100 | if #fullExt > 1 and fullExt[#fullExt] == 'gz' then 101 | r.ext = fullExt[#fullExt-1] 102 | r.isGzipped = true 103 | elseif #fullExt >= 1 then 104 | r.ext = fullExt[#fullExt] 105 | end 106 | r.isScript = r.ext == "lua" or r.ext == "lc" 107 | r.file = uriToFilename(r.file) 108 | return r 109 | end 110 | 111 | -- Parses the client's request. Returns a dictionary containing pretty much everything 112 | -- the server needs to know about the uri. 113 | return function (request) 114 | --print("Request: \n", request) 115 | local e = request:find("\r\n", 1, true) 116 | if not e then return nil end 117 | local line = request:sub(1, e - 1) 118 | local r = {} 119 | local _, i 120 | _, i, r.method, r.request = line:find("^([A-Z]+) (.-) HTTP/[1-9]+.[0-9]+$") 121 | if not (r.method and r.request) then 122 | --print("invalid request: ") 123 | --print(request) 124 | return nil 125 | end 126 | r.methodIsValid = validateMethod(r.method) 127 | r.uri = parseUri(r.request) 128 | r.getRequestData = getRequestData(request) 129 | return r 130 | end 131 | -------------------------------------------------------------------------------- /http/garage_door.lua: -------------------------------------------------------------------------------- 1 | -- garage_door_open.lua 2 | -- Part of nodemcu-httpserver, example. 3 | -- Author: Marcos Kirsch 4 | 5 | --[[ 6 | This example assumed you have a Wemos D1 Pro to control a two-door garage. 7 | For each garage door, a Wemos relay shield is used to simulate a button (connect relay in 8 | parallel with the actual physical button) and a reed switch is used in order to know 9 | whether a door is currently open or closed (install switch so that it is in the closed 10 | position when your garage door is closed). 11 | 12 | You can configure which GPIO pins you use for each function by modifying variable 13 | pinConfig below. 14 | ]]-- 15 | 16 | local function pushTheButton(connection, pinConfig) 17 | -- push the button! 18 | gpio.write(pinConfig["controlPin"], gpio.HIGH) 19 | gpio.mode(pinConfig["controlPin"], gpio.OUTPUT, gpio.FLOAT) 20 | tmr.delay(300000) -- in microseconds 21 | gpio.mode(pinConfig["controlPin"], gpio.INPUT, gpio.FLOAT) 22 | gpio.write(pinConfig["controlPin"], gpio.LOW) 23 | end 24 | 25 | 26 | local function readDoorStatus(pinConfig) 27 | -- When the garage door is closed, the reed relay closes, grounding the pin and causing us to read low (0). 28 | -- When the garage door is open, the reed relay is open, so due to pullup we read high (1). 29 | gpio.write(pinConfig["statusPin"], gpio.HIGH) 30 | gpio.mode(pinConfig["statusPin"], gpio.INPUT, gpio.PULLUP) 31 | if gpio.read(pinConfig["statusPin"]) == 1 then return 'open' else return 'closed' end 32 | end 33 | 34 | 35 | local function sendResponse(connection, httpCode, errorCode, action, pinConfig, message) 36 | 37 | -- Handle nil inputs 38 | if action == nil then action = '' end 39 | if pinConfig == nil then 40 | pinConfig = {} 41 | pinConfig["door"] = 0 42 | pinConfig["controlPin"] = 0 43 | pinConfig["statusPin"] = 0 44 | end 45 | if message == nil then message = '' end 46 | 47 | connection:send("HTTP/1.0 "..httpCode.." OK\r\nContent-Type: application/json\r\nCache-Control: private, no-store\r\n\r\n") 48 | connection:send('{"error":'..errorCode..', "door":'..pinConfig["door"]..', "controlPin":'..pinConfig["controlPin"]..', "statusPin":'..pinConfig["statusPin"]..', "action":"'..action..'", "message":"'..message..'"}') 49 | end 50 | 51 | 52 | local function sendStatus(connection, pinConfig) 53 | connection:send("HTTP/1.0 200 OK\r\nContent-Type: application/json\r\nCache-Control: private, no-store\r\n\r\n") 54 | connection:send('{"error":0, "door":'..pinConfig["door"]..', "controlPin":'..pinConfig["controlPin"]..', "statusPin":'..pinConfig["statusPin"]..', "action":"status"'..', "status":"'..readDoorStatus(pinConfig)..'"}') 55 | end 56 | 57 | 58 | local function openDoor(connection, pinConfig) 59 | -- errors if door is already open. 60 | local doorStatus = readDoorStatus(pinConfig) 61 | if doorStatus == 'open' then 62 | return false 63 | else 64 | pushTheButton(connection, pinConfig) 65 | return true 66 | end 67 | end 68 | 69 | 70 | local function closeDoor(connection, pinConfig) 71 | -- errors if door is already closed. 72 | local doorStatus = readDoorStatus(pinConfig) 73 | if doorStatus == 'closed' then 74 | return false 75 | else 76 | pushTheButton(connection, pinConfig) 77 | return true 78 | end 79 | end 80 | 81 | 82 | return function (connection, req, args) 83 | 84 | -- The values for pinConfig depend on how your Wemo D1 mini Pro is wired. 85 | -- Adjust as needed. 86 | pinConfig = {} 87 | pinConfig["1"] = {} 88 | pinConfig["1"]["door"] = 1 89 | pinConfig["1"]["controlPin"] = 2 90 | pinConfig["1"]["statusPin"] = 5 91 | pinConfig["2"] = {} 92 | pinConfig["2"]["door"] = 2 93 | pinConfig["2"]["controlPin"] = 1 94 | pinConfig["2"]["statusPin"] = 6 95 | 96 | -- Make this work with both GET and POST methods. 97 | -- In the POST case, we need to extract the arguments. 98 | print("method is " .. req.method) 99 | if req.method == "POST" then 100 | local rd = req.getRequestData() 101 | for name, value in pairs(rd) do 102 | args[name] = value 103 | end 104 | end 105 | 106 | -- validate door input 107 | 108 | if args.door == nil then 109 | sendResponse(connection, 400, -1, args.action, pinConfig[args.door], "No door specified") 110 | return 111 | end 112 | 113 | if pinConfig[args.door] == nil then 114 | sendResponse(connection, 400, -2, args.action, pinConfig[args.door], "Bad door specified") 115 | return 116 | end 117 | 118 | -- perform action 119 | 120 | if args.action == "open" then 121 | if(openDoor(connection, pinConfig[args.door])) then 122 | sendResponse(connection, 200, 0, args.action, pinConfig[args.door], "Door opened") 123 | else 124 | sendResponse(connection, 400, -3, args.action, pinConfig[args.door], "Door is already open") 125 | end 126 | return 127 | end 128 | 129 | if args.action == "close" then 130 | if(closeDoor(connection, pinConfig[args.door])) then 131 | sendResponse(connection, 200, 0, args.action, pinConfig[args.door], "Door closed") 132 | else 133 | sendResponse(connection, 400, -4, args.action, pinConfig[args.door], "Door is already closed") 134 | end 135 | return 136 | end 137 | 138 | if args.action == "toggle" then 139 | pushTheButton(connection, pinConfig[args.door]) 140 | sendResponse(connection, 200, 0, args.action, pinConfig[args.door], "Pushed the button") 141 | return 142 | end 143 | 144 | if args.action == "status" then 145 | sendStatus(connection, pinConfig[args.door]) 146 | return 147 | end 148 | 149 | -- everything else is error 150 | 151 | sendResponse(connection, 400, -5, args.action, pinConfig[args.door], "Bad action") 152 | 153 | end 154 | -------------------------------------------------------------------------------- /http/upload.css: -------------------------------------------------------------------------------- 1 | html{ 2 | background-color:#ebebec; 3 | 4 | background-image:-webkit-radial-gradient(center, #ebebec, #b4b4b4); 5 | background-image:-moz-radial-gradient(center, #ebebec, #b4b4b4); 6 | background-image:radial-gradient(center, #ebebec, #b4b4b4); 7 | } 8 | 9 | body{ 10 | font:15px/1.3 Arial, sans-serif; 11 | color: #4f4f4f; 12 | margin:0; 13 | padding:0; 14 | overflow-x:hidden; 15 | } 16 | 17 | a, a:visited { 18 | outline:none; 19 | color:#389dc1; 20 | } 21 | 22 | a:hover{ 23 | text-decoration:none; 24 | } 25 | 26 | section, footer, header, aside{ 27 | display: block; 28 | } 29 | 30 | .dropBox {width:100vw; height:100vh; margin-top: -200px; padding-top: 200px;} 31 | 32 | #uploaddir{ 33 | background-color: #2E3134; 34 | font-size:16px; 35 | font-weight:bold; 36 | color:#7f858a; 37 | padding: 40px 50px; 38 | margin-bottom: 30px; 39 | } 40 | 41 | #uploaddir a{ 42 | background-color:#007a96; 43 | padding:12px 26px; 44 | color:#fff; 45 | font-size:14px; 46 | border-radius:2px; 47 | cursor:pointer; 48 | margin-top:12px; 49 | line-height:1; 50 | margin-left: 10px; 51 | } 52 | #selectedDir { 53 | margin-top:20px; 54 | } 55 | #upload{ 56 | font-family:'PT Sans Narrow', sans-serif; 57 | background-color:#373a3d; 58 | 59 | background-image:-webkit-linear-gradient(top, #373a3d, #313437); 60 | background-image:-moz-linear-gradient(top, #373a3d, #313437); 61 | background-image:linear-gradient(top, #373a3d, #313437); 62 | 63 | width:450px; 64 | padding:30px; 65 | border-radius:3px; 66 | 67 | margin:10px auto 10px; 68 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); 69 | } 70 | 71 | #drop{ 72 | background-color: #2E3134; 73 | padding: 40px 50px; 74 | margin-bottom: 30px; 75 | border: 20px solid rgba(0, 0, 0, 0); 76 | border-radius: 3px; 77 | /* no external files */ 78 | border-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2RpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo0MUU5RTNGRDk4QjFFMjExODE0NkUyMUJBNERDNDk0NyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo0OEZGM0JBREI3RTcxMUUyODFDRkE4MTU1MDRCRkVBRCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo0OEZGM0JBQ0I3RTcxMUUyODFDRkE4MTU1MDRCRkVBRCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0MUU5RTNGRDk4QjFFMjExODE0NkUyMUJBNERDNDk0NyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0MUU5RTNGRDk4QjFFMjExODE0NkUyMUJBNERDNDk0NyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PmuiwCwAAAF7SURBVHja7NpRSsMwAIDhZJbdoNLhdXwSvIBPg11CUBS9h7AnbyYr9hAxfRmIfRDdlqx8P2SwPqwjH01gS7y+uV2GEJ7yWOdxFf5RSikMfb9/H2MMbdeFYzWj+33ksc3jtckvL3ncB5VsfBAe8rhY5JeN+aimzQjSmodqahfmoK6aqYufu93BNsFDfdYc73e5Wv245gmpLCBABASIgAARECACAkRABASI/lRzyp+r9b2pufeEWLIE5Jz2kKm/Ee0rp8lfuJYsAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiILPM2d6COdtryRKQc99DnO0tl7O9liwBASIgQAQEiIAICBABASIgQAQEiIAICBABASIgs8zZ3oI522vJEpBz30Oc7S2Xs72WLAEBIiBABASIgAgIEAEBIiBzrZm6OPWz8G9KKYWh7/fvY4yh7bqjffk53s8TUuGSNZiGahpGkDfzUE3bcQ95zmOZx10enTkp0rgxvefx+CXAAFXjX+IoV9pGAAAAAElFTkSuQmCC') 25 repeat; 79 | text-align: center; 80 | text-transform: uppercase; 81 | 82 | font-size:16px; 83 | font-weight:bold; 84 | color:#7f858a; 85 | } 86 | 87 | #drop a{ 88 | background-color:#007a96; 89 | padding:12px 26px; 90 | color:#fff; 91 | font-size:14px; 92 | border-radius:2px; 93 | cursor:pointer; 94 | display:block; 95 | margin-top:12px; 96 | line-height:1; 97 | width:100px; 98 | margin-left: 75px; 99 | } 100 | 101 | #drop a:hover{ 102 | background-color:#0986a3; 103 | } 104 | 105 | #drop input{ 106 | display:none; 107 | } 108 | 109 | #upload ul{ 110 | list-style:none; 111 | margin:0 -30px; 112 | border-top:1px solid #2b2e31; 113 | border-bottom:1px solid #3d4043; 114 | padding: 0; 115 | } 116 | 117 | #upload ul li{ 118 | 119 | background-color:#333639; 120 | 121 | background-image:-webkit-linear-gradient(top, #333639, #303335); 122 | background-image:-moz-linear-gradient(top, #333639, #303335); 123 | background-image:linear-gradient(top, #333639, #303335); 124 | 125 | border-top:1px solid #3d4043; 126 | border-bottom:1px solid #2b2e31; 127 | padding:15px; 128 | height: 52px; 129 | 130 | position: relative; 131 | } 132 | 133 | #upload ul li input{ 134 | display: none; 135 | } 136 | 137 | #upload ul li p{ 138 | width: 300px; 139 | overflow: hidden; 140 | white-space: nowrap; 141 | color: #EEE; 142 | font-size: 16px; 143 | font-weight: bold; 144 | position: absolute; 145 | top: 8px; 146 | left: 100px; 147 | } 148 | 149 | #upload ul li i{ 150 | font-weight: normal; 151 | font-style:normal; 152 | color:#7f7f7f; 153 | display:block; 154 | } 155 | 156 | #upload ul li canvas{ 157 | top: 5px; 158 | left: 20px; 159 | position: absolute; 160 | } 161 | 162 | .delete:after{ 163 | color: #ff0000; 164 | content: "\2718"; 165 | } 166 | 167 | .uploaded:after{ 168 | color: #00ff00; 169 | content: "\2714"; 170 | } 171 | 172 | #upload ul li span{ 173 | width: 15px; 174 | height: 12px; 175 | cursor:pointer; 176 | position: absolute; 177 | top: 34px; 178 | right: 33px; 179 | font-size:18px; 180 | } 181 | 182 | #upload ul li.working span{ 183 | height: 16px; 184 | background-position: 0 -12px; 185 | } 186 | 187 | #upload ul li.error p{ 188 | color:red; 189 | } 190 | 191 | .chart { 192 | position:relative; 193 | margin:0px; 194 | width:48px; height:48px; 195 | } 196 | 197 | .fileInfo { 198 | text-align: center; 199 | font-size: 16px; 200 | font-weight: bold; 201 | color: #7f858a; 202 | margin-top: 24px; 203 | margin-bottom: 24px; 204 | text-transform: uppercase; 205 | } 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [nodemcu-httpserver](https://github.com/marcoskirsch/nodemcu-httpserver) 2 | A (very) simple web server written in Lua for the ESP8266 running the NodeMCU firmware. 3 | 4 | [From the NodeMCU FAQ](https://nodemcu.readthedocs.org/en/dev/en/lua-developer-faq/#how-do-i-minimise-the-footprint-of-an-application): 5 | 6 | > If you are trying to implement a user-interface or HTTP webserver in your ESP8266 then 7 | > you are really abusing its intended purpose. When it comes to scoping your ESP8266 8 | > applications, the adage Keep It Simple Stupid truly applies. 9 | > 10 | > -- [Terry Ellison](https://github.com/TerryE), nodemcu-firmware maintainer 11 | 12 | Let the abuse begin. 13 | 14 | ## Features 15 | 16 | * GET, POST, PUT (other methods can be supported with minor changes) 17 | * Multiple MIME types 18 | * Error pages (404 and others) 19 | * *Server-side execution of Lua scripts* 20 | * Query string argument parsing with decoding of arguments 21 | * Serving .gz compressed files 22 | * HTTP Basic Authentication 23 | * Decoding of request bodies in both application/x-www-form-urlencoded and application/json (if cjson is available) 24 | 25 | ## How to use 26 | 27 | 1. Modify your local copy of the configuration file httpserver-conf.lua. 28 | 29 | 2. Upload server files using [nodemcu-uploader](https://github.com/kmpm/nodemcu-uploader). 30 | The easiest is to use GNU Make with the bundled Makefile. Open the Makefile and modify the 31 | user configuration to point to your nodemcu-uploader script and your serial port. 32 | Type the following to upload the server code, init.lua (which you may want to modify), 33 | and some example files: 34 | 35 | make upload_all 36 | 37 | If you only want to upload just the server code, then type: 38 | 39 | make upload_server 40 | 41 | If you only want to update wifi configuration, type: 42 | 43 | make upload_wifi_config 44 | 45 | And if you only want to upload just the files that can be served: 46 | 47 | make upload_http 48 | 49 | Restart the server. This will execute included init.lua which will compile the server code, 50 | configure WiFi, and start the server. 51 | 52 | 3. Want to serve your own files? Put them under the http/ folder and upload to the chip. 53 | For example, assuming you want to serve myfile.html, upload by typing: 54 | 55 | make upload FILE:=http/myfile.html 56 | 57 | Notice that while NodeMCU's filesystem does not support folders, filenames *can* contain slashes. 58 | We take advantage of that and only files that begin with "http/" will be accessible through the server. 59 | 60 | 3. Visit your server from a web browser. 61 | 62 | __Example:__ Say the IP for your ESP8266 is 2.2.2.2 and the server is 63 | running in the default port 80. Go to using your web browser. 64 | The ESP8266 will serve you with the contents of the file "http/index.html" (if it exists). If you visit the root (/) 65 | then index.html is served. By the way, unlike most HTTP servers, nodemcu_httpserver treats the URLs in a 66 | case-sensitive manner. 67 | 68 | ## HTTP Basic Authentication. 69 | 70 | It's supported. Turn it on in httpserver-conf.lua. 71 | 72 | Use it with care and don't fall into a false sense of security: HTTP Basic Authentication should not be 73 | considered secure since the server is not using encryption. Username and passwords travel 74 | in the clear. 75 | 76 | ## Server-side scripting using your own Lua scripts 77 | 78 | Yes, you can upload your own Lua scripts! This is pretty powerful. 79 | Just put it under http/ and upload it. Make sure it has a .lua extension. 80 | Your script should return a function that takes three parameters: 81 | 82 | return function (connection, req, args) 83 | -- code goes here 84 | end 85 | 86 | Use the _connection_ parameter to send the response back to the client. 87 | Note that you are in charge of sending the HTTP header, but you can use the bundled httpserver-header.lua 88 | script for that. See how other examples do it. 89 | The _req_ parameter contains information about the request. 90 | The _args_ parameter is a Lua table that contains any arguments sent by the client in the GET request. 91 | 92 | For example, if the client requests _http://2.2.2.2/foo.lua?color=red_ then the server will execute the function 93 | in your Lua script _foo.lua_ and pass in _connection_ and _args_, where _args.color = "red"_. 94 | 95 | ## [LFS](https://nodemcu.readthedocs.io/en/master/lfs/) support. 96 | 97 | NodeMCU allows to run Lua scripts from LFS in order to save RAM resources. 98 | *nodemcu-httpserver* makes it easy to move your code to LFS. 99 | In order to run *nodemcu-httpserver* from LFS: 100 | 101 | 1. move your code to `srv` folder (if you want it to be included in LFS image) 102 | 103 | 1. Compile contents of `srv` into LFS image. There's a [cloud service](https://blog.ellisons.org.uk/article/nodemcu/a-lua-cross-compile-web-service/) and [docker image](https://github.com/marcelstoer/docker-nodemcu-build) that will help you with that. 104 | 105 | 1. Upload image file under `lfs.img` name. You may use Makefile rules `upload_lfs` and `upload_all_lfs` for this. 106 | 107 | 1. Reboot you NodeMCU. Init script will pick up image and apply it for you. 108 | 109 | ### Example: Garage door opener 110 | 111 | #### Purpose 112 | 113 | This is a bundled example that shows how to use nodemcu-httpserver 114 | together with server-side scripting to control something with the 115 | ESP8266. In this example, we will pretend to open one of two garage doors. 116 | 117 | Your typical [garage door opener](http://en.wikipedia.org/wiki/Garage_door_opener) 118 | has a wired remote with a single button. The button simply connects to 119 | two terminals on the electric motor and when pushed, the terminals are 120 | shorted. This causes the motor to open or close. 121 | 122 | #### Hardware description 123 | 124 | This example assumes that you are using a [Wemos D1 Pro](https://wiki.wemos.cc/products:d1:d1_mini_pro) 125 | with two relay shields and two reed switches. 126 | The relays are controlled by the microcontroller and act as the push button, 127 | and can actually be connected in parallel with the existing mechanical button. 128 | The switches are wired so that the ESP8266 can tell whether the doors are open 129 | or closed at any given time. 130 | 131 | #### Software description 132 | 133 | This example consists of the following files: 134 | 135 | * **garage_door.html**: Static HTML displays a form with all options for controlling the 136 | two garage doors. 137 | * **garage_door_control.html**: Looks like a garage door remote, how neat! 138 | * **garage_door_control.css**: Provides styling for garage_door_control.html. 139 | * **garage_door.lua**: Does the actual work. The script performs the desired action on 140 | the requested door and returns the results as JSON. 141 | * **apple-touch-icon.png**: This is optional. Provides an icon that 142 | will be used if you "Add to Home Screen" garage_door_control.html on an iPhone. 143 | Now it looks like an app! 144 | 145 | #### Security implications 146 | 147 | Be careful permanently installing something like this in your home. The server provides 148 | no encryption. Your only layers of security are the WiFi network's password and simple 149 | HTTP authentication (if you enable it) which sends your password unencrypted. 150 | 151 | This script is provided for educational purposes. You've been warned. 152 | 153 | ## Not supported 154 | 155 | * Other methods: HEAD, DELETE, TRACE, OPTIONS, CONNECT, PATCH 156 | * Encryption / SSL 157 | * Old nodemcu-firmware versions prior to January 2021) because I don't bother to test them. 158 | 159 | ## Contributing 160 | 161 | Since this is a project maintained in my free time, I am pretty lenient on contributions. 162 | I trust you to make sure you didn't break existing functionality nor the shipping examples 163 | and that you add examples for new features. I won't test all your changes myself but I 164 | am very grateful of improvements and fixes. Open issues in GitHub too, that's useful. 165 | 166 | Please keep your PRs focused on one thing. I don't mind lots of PRs. I mind PRs that fix multiple unrelated things. 167 | 168 | Follow the coding style as closely as possible: 169 | 170 | * No tabs, indent with 3 spaces 171 | * Unix (LF) line endings 172 | * Variables are camelCase 173 | * Follow file naming conventions 174 | * Use best judgement 175 | 176 | ## Notes on memory usage. 177 | 178 | The chip is very, very memory constrained. 179 | 180 | * Use a recent nodemcu-firmware. They've really improved memory usage and fixed leaks. 181 | * Use only the modules you need. 182 | * Use a firmware build without floating point support if you can. 183 | * Any help reducing the memory needs of the server without crippling its functionality is much appreciated! 184 | * Compile your Lua scripts in order to reduce their memory usage. The server knows to serve 185 | both .lua and .lc files as scripts. 186 | -------------------------------------------------------------------------------- /srv/httpserver.lua: -------------------------------------------------------------------------------- 1 | -- httpserver 2 | -- Author: Marcos Kirsch 3 | 4 | -- Starts web server in the specified port. 5 | return function (port) 6 | 7 | local s = net.createServer(net.TCP, 10) -- 10 seconds client timeout 8 | s:listen( 9 | port, 10 | function (connection) 11 | 12 | -- This variable holds the thread (actually a Lua coroutine) used for sending data back to the user. 13 | -- We do it in a separate thread because we need to send in little chunks and wait for the onSent event 14 | -- before we can send more, or we risk overflowing the mcu's buffer. 15 | local connectionThread 16 | local fileInfo 17 | 18 | local allowStatic = {GET=true, HEAD=true, POST=false, PUT=false, DELETE=false, TRACE=false, OPTIONS=false, CONNECT=false, PATCH=false} 19 | 20 | -- Pretty log function. 21 | local function log(connection, msg, optionalMsg) 22 | local port, ip = connection:getpeer() 23 | if(optionalMsg == nil) then 24 | print(ip .. ":" .. port, msg) 25 | else 26 | print(ip .. ":" .. port, msg, optionalMsg) 27 | end 28 | end 29 | 30 | local function startServingStatic(connection, req, args) 31 | fileInfo = dofile("httpserver-static.lc")(connection, req, args) 32 | end 33 | 34 | local function startServing(fileServeFunction, connection, req, args) 35 | connectionThread = coroutine.create(function(fileServeFunction, bufferedConnection, req, args) 36 | fileServeFunction(bufferedConnection, req, args) 37 | -- The bufferedConnection may still hold some data that hasn't been sent. Flush it before closing. 38 | if not bufferedConnection:flush() then 39 | log(connection, "closing connection", "no (more) data") 40 | connection:close() 41 | connectionThread = nil 42 | collectgarbage() 43 | end 44 | end) 45 | 46 | local BufferedConnectionClass = dofile("httpserver-connection.lc") 47 | local bufferedConnection = BufferedConnectionClass:new(connection) 48 | BufferedConnectionClass = nil 49 | local status, err = coroutine.resume(connectionThread, fileServeFunction, bufferedConnection, req, args) 50 | if not status then 51 | log(connection, "Error: "..err) 52 | log(connection, "closing connection", "error") 53 | connection:close() 54 | connectionThread = nil 55 | collectgarbage() 56 | end 57 | end 58 | 59 | local function handleRequest(connection, req, handleError) 60 | collectgarbage() 61 | local method = req.method 62 | local uri = req.uri 63 | local fileServeFunction = nil 64 | 65 | if #(uri.file) > 32 then 66 | -- nodemcu-firmware cannot handle long filenames. 67 | uri.args = {code = 400, errorString = "Bad Request", logFunction = log} 68 | fileServeFunction = dofile("httpserver-error.lc") 69 | else 70 | local fileExists = false 71 | 72 | if not file.exists(uri.file) then 73 | -- print(uri.file .. " not found, checking gz version...") 74 | -- gzip check 75 | if file.exists(uri.file .. ".gz") then 76 | -- print("gzip variant exists, serving that one") 77 | uri.file = uri.file .. ".gz" 78 | uri.isGzipped = true 79 | fileExists = true 80 | end 81 | else 82 | fileExists = true 83 | end 84 | 85 | if not fileExists then 86 | uri.args = {code = 404, errorString = "Not Found", logFunction = log} 87 | fileServeFunction = dofile("httpserver-error.lc") 88 | elseif uri.isScript then 89 | fileServeFunction = dofile(uri.file) 90 | else 91 | if allowStatic[method] then 92 | uri.args = {file = uri.file, ext = uri.ext, isGzipped = uri.isGzipped} 93 | startServingStatic(connection, req, uri.args) 94 | return 95 | else 96 | uri.args = {code = 405, errorString = "Method not supported", logFunction = log} 97 | fileServeFunction = dofile("httpserver-error.lc") 98 | end 99 | end 100 | end 101 | startServing(fileServeFunction, connection, req, uri.args) 102 | end 103 | 104 | local function onReceive(connection, payload) 105 | -- collectgarbage() 106 | local conf = dofile("httpserver-conf.lua") 107 | local auth 108 | local user = "Anonymous" 109 | 110 | -- as suggest by anyn99 (https://github.com/marcoskirsch/nodemcu-httpserver/issues/36#issuecomment-167442461) 111 | -- Some browsers send the POST data in multiple chunks. 112 | -- Collect data packets until the size of HTTP body meets the Content-Length stated in header 113 | if payload:find("Content%-Length:") or bBodyMissing then 114 | if fullPayload then fullPayload = fullPayload .. payload else fullPayload = payload end 115 | if (tonumber(string.match(fullPayload, "%d+", fullPayload:find("Content%-Length:")+16)) > #fullPayload:sub(fullPayload:find("\r\n\r\n", 1, true)+4, #fullPayload)) then 116 | bBodyMissing = true 117 | return 118 | else 119 | --print("HTTP packet assembled! size: "..#fullPayload) 120 | payload = fullPayload 121 | fullPayload, bBodyMissing = nil 122 | end 123 | end 124 | collectgarbage() 125 | 126 | -- parse payload and decide what to serve. 127 | local req = dofile("httpserver-request.lc")(payload) 128 | log(connection, req.method, req.request) 129 | if conf.auth.enabled then 130 | auth = dofile("httpserver-basicauth.lc") 131 | user = auth.authenticate(payload) -- authenticate returns nil on failed auth 132 | end 133 | 134 | if user and req.methodIsValid and (req.method == "GET" or req.method == "POST" or req.method == "PUT") then 135 | req.user = user 136 | handleRequest(connection, req, handleError) 137 | else 138 | local args = {} 139 | local fileServeFunction = dofile("httpserver-error.lc") 140 | if not user then 141 | args = {code = 401, errorString = "Not Authorized", headers = {auth.authErrorHeader()}, logFunction = log} 142 | elseif req.methodIsValid then 143 | args = {code = 501, errorString = "Not Implemented", logFunction = log} 144 | else 145 | args = {code = 400, errorString = "Bad Request", logFunction = log} 146 | end 147 | startServing(fileServeFunction, connection, req, args) 148 | end 149 | end 150 | 151 | local function onSent(connection, payload) 152 | collectgarbage() 153 | if connectionThread then 154 | local connectionThreadStatus = coroutine.status(connectionThread) 155 | if connectionThreadStatus == "suspended" then 156 | -- Not finished sending file, resume. 157 | local status, err = coroutine.resume(connectionThread) 158 | if not status then 159 | log(connection, "Error: "..err) 160 | log(connection, "closing connection", "error") 161 | connection:close() 162 | connectionThread = nil 163 | collectgarbage() 164 | end 165 | elseif connectionThreadStatus == "dead" then 166 | -- We're done sending file. 167 | log(connection, "closing connection","thread is dead") 168 | connection:close() 169 | connectionThread = nil 170 | collectgarbage() 171 | end 172 | elseif fileInfo then 173 | local fileSize = file.list()[fileInfo.file] 174 | -- Chunks larger than 1024 don't work. 175 | -- https://github.com/nodemcu/nodemcu-firmware/issues/1075 176 | local chunkSize = 512 177 | local fileHandle = file.open(fileInfo.file) 178 | if fileSize > fileInfo.sent then 179 | fileHandle:seek("set", fileInfo.sent) 180 | local chunk = fileHandle:read(chunkSize) 181 | fileHandle:close() 182 | fileHandle = nil 183 | fileInfo.sent = fileInfo.sent + #chunk 184 | connection:send(chunk) 185 | -- print(fileInfo.file .. ": Sent "..#chunk.. " bytes, " .. fileSize - fileInfo.sent .. " to go.") 186 | chunk = nil 187 | else 188 | log(connection, "closing connetion", "Finished sending: "..fileInfo.file) 189 | connection:close() 190 | fileInfo = nil 191 | end 192 | collectgarbage() 193 | end 194 | end 195 | 196 | local function onDisconnect(connection, payload) 197 | -- this should rather be a log call, but log is not available here 198 | -- print("disconnected") 199 | if connectionThread then 200 | connectionThread = nil 201 | collectgarbage() 202 | end 203 | if fileInfo then 204 | fileInfo = nil 205 | collectgarbage() 206 | end 207 | end 208 | 209 | connection:on("receive", onReceive) 210 | connection:on("sent", onSent) 211 | connection:on("disconnection", onDisconnect) 212 | 213 | end 214 | ) 215 | return s 216 | 217 | end 218 | -------------------------------------------------------------------------------- /http/upload.js: -------------------------------------------------------------------------------- 1 | var files = []; 2 | var sendingOffset = 0; 3 | var lastRequest = ''; 4 | var dataView; 5 | var filesCount = 0; 6 | var currentUploadingFile = 0; 7 | var uploadOrder = []; 8 | var uploadingInProgress = 0; 9 | var fileUploadRequest; 10 | 11 | var chunkSize = 128; 12 | var totalUploaded = 0; 13 | 14 | var tpl = '
  • %filename%%filesize%

  • '; 15 | 16 | document.addEventListener("DOMContentLoaded", function() { 17 | var dropbox; 18 | 19 | dropbox = document.getElementById("dropbox"); 20 | dropbox.addEventListener("dragenter", dragenter, false); 21 | dropbox.addEventListener("dragover", dragover, false); 22 | dropbox.addEventListener("drop", drop, false); 23 | 24 | UpdateFileList(); 25 | 26 | UploadDir("http"); 27 | }); 28 | 29 | function dragenter(e) { 30 | e.stopPropagation(); 31 | e.preventDefault(); 32 | } 33 | 34 | function dragover(e) { 35 | e.stopPropagation(); 36 | e.preventDefault(); 37 | } 38 | 39 | function drop(e) { 40 | e.stopPropagation(); 41 | e.preventDefault(); 42 | 43 | var dt = e.dataTransfer; 44 | 45 | handleFiles(dt.files); 46 | } 47 | 48 | function handleFiles(tfiles) { 49 | var filesCount = tfiles.length; 50 | files = tfiles; 51 | currentUploadingFile = 0; 52 | uploadOrder = []; 53 | 54 | sendingOffset = 0; 55 | lastRequest = ''; 56 | 57 | document.getElementById('fileList').innerHTML = ''; 58 | 59 | var fileNames = {}; 60 | 61 | for (var i = 0; i < filesCount; i++) { 62 | fileNames[uploadDir + tfiles[i].name] = i; 63 | } 64 | 65 | Keys(fileNames).sort(function(a,b){var c=a.toLowerCase(),d=b.toLowerCase();return cd?1:0}).forEach(function(item) { 66 | var i = fileNames[item]; 67 | 68 | var append = tpl.replace(/%filename%/g, uploadDir + tfiles[i].name); 69 | append = append.replace(/%filesize%/g, formatFileSize(tfiles[i].size)); 70 | append = append.replace(/%filenum%/g, i); 71 | 72 | document.getElementById('fileList').insertAdjacentHTML('beforeend', append); 73 | 74 | UpdateGraph(0, i); 75 | 76 | uploadOrder.push(i); 77 | }); 78 | } 79 | 80 | function DeleteFiles(filenum) { 81 | var elem = document.getElementById('file' + filenum.toString()); 82 | elem.parentNode.removeChild(elem); 83 | 84 | if (uploadingInProgress) { 85 | if (parseInt(filenum) != uploadOrder[currentUploadingFile]) { 86 | for (var i = 0; i < uploadOrder.length; i++) { 87 | if (uploadOrder[i] == filenum) { 88 | delete uploadOrder[i]; 89 | } 90 | } 91 | } 92 | else { 93 | uploadingInProgress = 0; 94 | 95 | RemoveFile(files[uploadOrder[currentUploadingFile]].name + '.dnl'); 96 | 97 | for (var i = 0; i < uploadOrder.length; i++) { 98 | if (uploadOrder[i] == filenum) { 99 | delete uploadOrder[i]; 100 | } 101 | } 102 | 103 | currentUploadingFile++; 104 | totalUploaded = 0; 105 | sendingOffset = 0; 106 | 107 | lastRequest = ''; 108 | fileUploadRequest.abort(); 109 | fileUploadRequest = 0; 110 | 111 | UploadFiles(); 112 | } 113 | } 114 | else { 115 | for (var i = 0; i < uploadOrder.length; i++) { 116 | if (uploadOrder[i] == filenum) { 117 | delete uploadOrder[i]; 118 | } 119 | } 120 | } 121 | } 122 | 123 | function UploadFiles() { 124 | if (uploadOrder[currentUploadingFile] === undefined) { 125 | uploadingInProgress = 0; 126 | 127 | if (currentUploadingFile < files.length - 1) { 128 | currentUploadingFile++; 129 | 130 | UploadFiles(); 131 | } 132 | 133 | return; 134 | } 135 | 136 | var fileNum = uploadOrder[currentUploadingFile]; 137 | var file = files[fileNum]; 138 | var chunkLen = 0; 139 | var filedata = ''; 140 | 141 | uploadingInProgress = 1; 142 | 143 | var fr = new FileReader(); 144 | 145 | fr.onload = function() { 146 | dataView = null; 147 | dataView = new Uint8Array(fr.result); 148 | 149 | if (file.size <= chunkSize) { 150 | sendingOffset = 0; 151 | chunkLen = file.size; 152 | 153 | for (var i = 0; i < dataView.length; i++) { 154 | if (dataView[i] < 16) { 155 | filedata += '0'; 156 | } 157 | 158 | filedata += dataView[i].toString(16).toUpperCase(); 159 | } 160 | } 161 | else { 162 | if (dataView.length - sendingOffset > chunkSize) { 163 | chunkLen = chunkSize; 164 | } 165 | else { 166 | chunkLen = dataView.length - sendingOffset; 167 | } 168 | 169 | 170 | for (var i = sendingOffset; i < sendingOffset + chunkLen; i++) { 171 | if (dataView[i] < 16) { 172 | filedata += '0'; 173 | } 174 | 175 | filedata += dataView[i].toString(16).toUpperCase(); 176 | } 177 | } 178 | 179 | fileUploadRequest = new XMLHttpRequest(); 180 | 181 | fileUploadRequest.onreadystatechange = function() { 182 | if (fileUploadRequest.readyState != 4) return; 183 | 184 | if (fileUploadRequest.status == 200) { 185 | if (chunkLen + sendingOffset < dataView.length) { 186 | totalUploaded += chunkSize; 187 | 188 | UpdateGraph(Math.round((totalUploaded / file.size) * 100), uploadOrder[currentUploadingFile]); 189 | 190 | sendingOffset += chunkSize; 191 | UploadFiles(); 192 | } 193 | else { 194 | var statusElement = document.getElementById('fileStatus' + uploadOrder[currentUploadingFile]); 195 | 196 | sendingOffset = 0; 197 | 198 | UpdateGraph(100, uploadOrder[currentUploadingFile]); 199 | 200 | uploadingInProgress = 0; 201 | 202 | UpdateFileList(); 203 | 204 | totalUploaded = 0; 205 | 206 | if (statusElement) { 207 | statusElement.classList.add("uploaded"); 208 | } 209 | 210 | if (currentUploadingFile < files.length) { 211 | currentUploadingFile++; 212 | UploadFiles(); 213 | } 214 | } 215 | } 216 | else { 217 | UploadFiles(); 218 | } 219 | 220 | fileUploadRequest = 0; 221 | } 222 | 223 | lastRequest = 'upload.lua?cmd=upload&filename=' + uploadDir + file.name + '&filesize=' + file.size + '&len=' + chunkLen + '&offset=' + sendingOffset + '&data=' + filedata; 224 | 225 | fileUploadRequest.timeout = 5000; 226 | fileUploadRequest.open('GET', lastRequest, true); 227 | fileUploadRequest.send(); 228 | }; 229 | 230 | fr.readAsArrayBuffer(file); 231 | } 232 | 233 | function UploadDir(dir) { 234 | if (uploadingInProgress == 0) { 235 | document.getElementById('dir').innerHTML = "/" + dir; 236 | uploadDir = dir; 237 | if (!(uploadDir == "")) { 238 | uploadDir += "/"; 239 | } 240 | } 241 | } 242 | 243 | function formatFileSize(bytes) { 244 | if (typeof bytes !== 'number') { 245 | return ''; 246 | } 247 | 248 | if (bytes >= 1073741824) { 249 | return (bytes / 1073741824).toFixed(2) + ' GB'; 250 | } 251 | 252 | if (bytes >= 1048576) { 253 | return (bytes / 1048576).toFixed(2) + ' MB'; 254 | } 255 | 256 | return (bytes / 1024).toFixed(2) + ' KB'; 257 | } 258 | 259 | function UpdateGraph(percent, id) { 260 | var el = document.getElementById('graph' + id); // get canvas 261 | 262 | if (!el) { 263 | return; 264 | } 265 | 266 | var options = { 267 | percent: el.getAttribute('data-percent') || 0, 268 | size: el.getAttribute('data-size') || 48, 269 | lineWidth: el.getAttribute('data-line') || 8, 270 | rotate: el.getAttribute('data-rotate') || 0 271 | } 272 | 273 | var canvas = document.createElement('canvas'); 274 | 275 | if (typeof(G_vmlCanvasManager) !== 'undefined') { 276 | G_vmlCanvasManager.initElement(canvas); 277 | } 278 | 279 | var ctx = canvas.getContext('2d'); 280 | canvas.width = canvas.height = options.size; 281 | 282 | el.appendChild(canvas); 283 | 284 | ctx.translate(options.size / 2, options.size / 2); // change center 285 | ctx.rotate((-1 / 2 + options.rotate / 180) * Math.PI); // rotate -90 deg 286 | 287 | var radius = (options.size - options.lineWidth) / 2; 288 | 289 | function drawCircle(color, lineWidth, percent) { 290 | if (percent) { 291 | percent = Math.min(Math.max(0, percent), 1); 292 | ctx.beginPath(); 293 | ctx.arc(0, 0, radius, 0, Math.PI * 2 * percent, false); 294 | ctx.strokeStyle = color; 295 | ctx.lineCap = 'round'; // butt, round or square 296 | ctx.lineWidth = lineWidth 297 | ctx.stroke(); 298 | } 299 | }; 300 | 301 | options.percent = percent; 302 | 303 | drawCircle('#2e3134', options.lineWidth + 1, 100 / 100); 304 | drawCircle('#007a96', options.lineWidth, options.percent / 100); 305 | } 306 | 307 | function Keys(obj) { 308 | var keys = []; 309 | 310 | for(var key in obj){ 311 | if(obj.hasOwnProperty(key)){ 312 | keys.push(key); 313 | } 314 | } 315 | 316 | return keys; 317 | } 318 | 319 | function UpdateFileList() { 320 | var fileListRequest = new XMLHttpRequest(); 321 | 322 | fileListRequest.onreadystatechange = function() { 323 | if (fileListRequest.readyState != 4) return; 324 | 325 | if (fileListRequest.status == 200) { 326 | var fileInfo = JSON.parse(fileListRequest.responseText); 327 | var fileList = fileInfo['files']; 328 | 329 | document.getElementById('fileInfo').innerHTML = ''; 330 | 331 | var tpl = '
  • %filenamelink%%filesize%

  • '; 332 | var tplTotal = '
  • Used:%used%

  • Free:%free%

  • Total:%total%

  • '; 333 | 334 | var append, link; 335 | 336 | Keys(fileList).sort(function(a,b){var c=a.toLowerCase(),d=b.toLowerCase();return cd?1:0}).forEach(function(item) { 337 | if (!item.match(/\.lc$/ig) && item.match(/^http\//ig)) { 338 | link = item.replace(/\.gz$/g, '').replace(/^http\//g, ''); 339 | append = tpl.replace(/%filenamelink%/g, '' + item + ''); 340 | } 341 | else { 342 | append = tpl.replace(/%filenamelink%/g, item); 343 | } 344 | 345 | append = append.replace(/%filename%/g, item); 346 | append = append.replace(/%filesize%/g, formatFileSize(parseInt(fileList[item]))); 347 | document.getElementById('fileInfo').insertAdjacentHTML('beforeend', append); 348 | }); 349 | 350 | append = tplTotal.replace(/%used%/g, formatFileSize(parseInt(fileInfo['used']))); 351 | append = append.replace(/%free%/g, formatFileSize(parseInt(fileInfo['free']))); 352 | append = append.replace(/%total%/g, formatFileSize(parseInt(fileInfo['total']))); 353 | 354 | document.getElementById('fileInfo').insertAdjacentHTML('beforeend', append); 355 | } 356 | else { 357 | 358 | } 359 | 360 | fileListRequest = null; 361 | } 362 | 363 | fileListRequest.open('GET', 'upload.lua?cmd=list', true); 364 | fileListRequest.send(); 365 | } 366 | 367 | function RemoveFile(name) { 368 | var fileRemoveRequest = new XMLHttpRequest(); 369 | 370 | fileRemoveRequest.onreadystatechange = function() { 371 | if (fileRemoveRequest.readyState != 4) return; 372 | 373 | if (fileRemoveRequest.status == 200) { 374 | UpdateFileList(); 375 | } 376 | } 377 | 378 | fileRemoveRequest.open('GET', 'upload.lua?cmd=remove&filename=' + name, true); 379 | fileRemoveRequest.send(); 380 | } 381 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | --------------------------------------------------------------------------------