-
Notifications
You must be signed in to change notification settings - Fork 3.1k
/
Copy pathluaOTAserver.lua
259 lines (217 loc) · 8.78 KB
/
luaOTAserver.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
--------------------------------------------------------------------------------
-- LuaOTA provisioning system for ESPs using NodeMCU Lua
-- LICENCE: http://opensource.org/licenses/MIT
-- TerryE 15 Jul 2017
--
-- See luaOTA.md for description
--------------------------------------------------------------------------------
--[[ luaOTAserver.lua - an example provisioning server
This module implements an example server-side implementation of LuaOTA provisioning
system for ESPs used the SPI Flash FS (SPIFFS) on development and production modules.
This implementation is a simple TCP listener which can have one active provisioning
client executing the luaOTA module at a time. It will synchronise the client's FS
with the content of the given directory on the command line.
]]
-- luacheck: std max
local socket = require "socket"
local lfs = require "lfs"
local md5 = require "md5"
local json = require "cjson"
require "std.strict" -- see http://www.lua.org/extras/5.1/strict.lua
-- Local functions (implementation see below) ------------------------------------------
local get_inventory -- function(root_directory, CPU_ID)
local send_command -- function(esp, resp, buffer)
local receive_and_parse -- function(esp)
local provision -- function(esp, config, files, inventory, fingerprint)
local read_file -- function(fname)
local save_file -- function(fname, data)
local hmac -- function(data)
-- Function-wide locals (can be upvalues)
local unpack = table.unpack or unpack
local concat = table.concat
local load = loadstring or load
local format = string.format
-- use string % operators as a synomyn for string.format
getmetatable("").__mod =
function(a, b)
return not b and a or
(type(b) == "table" and format(a, unpack(b)) or format(a, b))
end
local ESPport = 8266
local ESPtimeout = 15
local src_dir = arg[1] or "."
-- Main process ------------------------ do encapsulation to prevent name-clash upvalues
local function main ()
local server = assert(socket.bind("*", ESPport))
local ip, port = server:getsockname()
print("Lua OTA service listening on %s:%u\n After connecting, the ESP timeout is %u s"
% {ip, port, ESPtimeout})
-- Main loop forever waiting for ESP clients then processing each request ------------
while true do
local esp = server:accept() -- wait for ESP connection
esp:settimeout(ESPtimeout) -- set session timeout
-- receive the opening request
local config = receive_and_parse(esp)
if config and config.a == "HI" then
print ("Processing provision check from ESP-"..config.id)
local inventory, fingerprint = get_inventory(src_dir, config.id)
-- Process the ESP request
if config.chk and config.chk == fingerprint then
send_command(esp, {r = "OK!"}) -- no update so send_command with OK
esp:receive("*l") -- dummy receive to allow client to close
else
local status, msg = pcall(provision, esp, config, inventory, fingerprint)
if not status then print (msg) end
end
end
pcall(esp.close, esp)
print ("Provisioning complete")
end
end
-- Local Function Implementations ------------------------------------------------------
local function get_hmac_md5(key)
if key:len() > 64 then
key = md5.sum(key)
elseif key:len() < 64 then
key = key .. ('\0'):rep(64-key:len())
end
local ki = md5.exor(('\54'):rep(64),key)
local ko = md5.exor(('\92'):rep(64),key)
return function (data) return md5.sumhexa(ko..md5.sum(ki..data)) end
end
-- Enumerate the sources directory and load the relevent inventory
------------------------------------------------------------------
get_inventory = function(dir, cpuid)
if (not dir or lfs.attributes(dir).mode ~= "directory") then
error("Cannot open directory, aborting %s" % arg[0], 0)
end
-- Load the CPU's (or the default) inventory
local invtype, inventory = "custom", read_file("%s/ESP-%s.json" % {dir, cpuid})
if not inventory then
invtype, inventory = "default", read_file(dir .. "/default.json")
end
-- tolerate and remove whitespace formatting, then decode
inventory = (inventory or ""):gsub("[ \t]*\n[ \t]*","")
inventory = inventory:gsub("[ \t]*:[ \t]*",":")
local ok; ok,inventory = pcall(json.decode, inventory)
if ok and inventory.files then
print( "Loading %s inventory for ESP-%s" % {invtype, cpuid})
else
error( "Invalid inventory for %s :%s" % {cpuid,inventory}, 0)
end
-- Calculate the current fingerprint of the inventory
local fp,f = {},inventory.files
for i= 1,#f do
local name, fullname = f[i], "%s/%s" % {dir, f[i]}
local fa = lfs.attributes(fullname)
assert(fa, "File %s is required but not in sources directory" % name)
fp[#fp+1] = name .. ":" .. fa.modification
f[i] = {name = name, mtime = fa.modification,
size = fa.size, content = read_file(fullname) }
assert (f[i].size == #(f[i].content or ''), "File %s unreadable" % name )
end
assert(#f == #fp, "Aborting provisioning die to missing fies",0)
assert(type(inventory.secret) == "string",
"Aborting, config must contain a shared secret")
hmac = get_hmac_md5(inventory.secret)
return inventory, md5.sumhexa(concat(fp,":"))
end
-- Encode a response buff, add a signature and any optional buffer
------------------------------------------------------------------
send_command = function(esp, resp, buffer)
if type(buffer) == "string" then
resp.data = #buffer
else
buffer = ''
end
local rec = json.encode(resp)
rec = rec .. hmac(rec):sub(-6) .."\n"
-- print("requesting ", rec:sub(1,-2), #(buffer or ''))
esp:send(rec .. buffer)
end
-- Decode a response buff, check the signature and any optional buffer
----------------------------------------------------------------------
receive_and_parse = function(esp)
local line = esp:receive("*l")
if (not line) then
error( "Empty response from ESP, possible cause: file signature failure", 0)
--return nil
end
local packed_cmd, sig = line:sub(1,#line-6),line:sub(-6)
-- print("reply:", packed_cmd, sig)
local status, cmd = pcall(json.decode, packed_cmd)
if not status then error("JSON decode error") end
if not hmac or hmac(packed_cmd):sub(-6) == sig then
if cmd and cmd.data == "number" then
local data = esp:receive(cmd.data)
return cmd, data
end
return cmd
end
end
provision = function(esp, config, inventory, fingerprint)
if type(config.files) ~= "table" then config.files = {} end
local cf = config.files
for _, f in ipairs(inventory.files) do
local name, size, mtime, content = f.name, f.size, f.mtime, f.content
if not cf[name] or cf[name] ~= mtime then
-- Send the file
local action, cmd, buf
if f.name:sub(-4) == ".lua" then
assert(load(content, f.name)) -- check that the contents can compile
if content:find("--SAFETRIM\n",1,true) then
-- if the source is tagged with SAFETRIM then its safe to remove "--"
-- comments, leading and trailing whitespace. Not as good as LuaSrcDiet,
-- but this simple source compression algo preserves line numbering in
-- the generated lc files, which helps debugging.
content = content:gsub("\n[ \t]+","\n")
content = content:gsub("[ \t]+\n","\n")
content = content:gsub("%-%-[^\n]*","")
size = #content
end
action = "cm"
else
action = "dl"
end
print ("Sending file ".. name)
for i = 1, size, 1024 do
if i+1023 < size then
cmd = {a = "pu", data = 1024}
buf = content:sub(i, i+1023)
else
cmd = {a = action, data = size - i + 1, name = name}
buf = content:sub(i)
end
send_command(esp, cmd, buf)
local resp = receive_and_parse(esp)
assert(resp and resp.s == "OK", "Command to ESP failed")
if resp.lcsize then
print("Compiled file size %s bytes" % resp.lcsize)
end
end
end
cf[name] = mtime
end
config.chk = fingerprint
config.id = nil
config.a = "restart"
send_command(esp, config)
end
-- Load contents of the given file (or null if absent/unreadable)
-----------------------------------------------------------------
read_file = function(fname)
local file = io.open(fname, "rb")
if not file then return end
local data = file and file:read"*a"
file:close()
return data
end
-- Save contents to the given file
----------------------------------
save_file = function(fname, data) -- luacheck: ignore
local file = io.open(fname, "wb")
file:write(data)
file:close()
end
--------------------------------------------------------------------------------------
main() -- now that all functions have been bound to locals, we can start the show :-)