├── .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 |
\n")
20 | for name, size in pairs(file.list()) do
21 | local isHttpFile = string.match(name, "(http/)") ~= nil
22 | if isHttpFile then
23 | local url = string.match(name, ".*/(.*)")
24 | connection:send('
\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
')
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 |
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 |
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
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 = '
';
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 |
--------------------------------------------------------------------------------