320 lines
8.5 KiB
Lua
Executable File
320 lines
8.5 KiB
Lua
Executable File
#!/usr/bin/lua
|
|
|
|
|
|
local nixio = require('nixio')
|
|
local fs = require('nixio.fs')
|
|
local platform_info = require('platform_info')
|
|
local uci = require('uci').cursor()
|
|
|
|
local autoupdater_util = require('autoupdater.util')
|
|
local autoupdater_version = require('autoupdater.version')
|
|
|
|
|
|
if not platform_info.get_image_name() then
|
|
io.stderr:write("The autoupdater doesn't support this hardware model.\n")
|
|
os.exit(1)
|
|
end
|
|
|
|
|
|
autoupdater_util.randomseed()
|
|
|
|
|
|
local settings = uci:get_all('autoupdater', 'settings')
|
|
local branch_name = settings.branch
|
|
|
|
local version_file = io.open(settings.version_file)
|
|
local old_version = version_file and version_file:read('*l') or ''
|
|
version_file:close()
|
|
|
|
|
|
-- If force is true the updater will perform an upgrade regardless of
|
|
-- the priority and even when it is disabled in uci
|
|
local force = false
|
|
|
|
-- If fallback is true the updater will perform an update only if the
|
|
-- timespan given by the priority and another 24h have passed
|
|
local fallback = false
|
|
|
|
|
|
local function parse_args()
|
|
local i = 1
|
|
while arg[i] do
|
|
if arg[i] == '-f' then
|
|
force = true
|
|
elseif arg[i] == '--fallback' then
|
|
fallback = true
|
|
elseif arg[i] == '-b' then
|
|
i = i+1
|
|
|
|
if not arg[i] then
|
|
io.stderr:write("Error parsing command line: expected branch name\n")
|
|
os.exit(1)
|
|
end
|
|
|
|
branch_name = arg[i]
|
|
else
|
|
io.stderr:write("Error parsing command line: unexpected argument '" .. arg[i] .. "'\n")
|
|
os.exit(1)
|
|
end
|
|
|
|
i = i+1
|
|
end
|
|
end
|
|
|
|
|
|
parse_args()
|
|
|
|
|
|
local branch = uci:get_all('autoupdater', branch_name)
|
|
if not branch then
|
|
io.stderr:write("Can't find configuration for branch '" .. branch_name .. "'\n")
|
|
os.exit(1)
|
|
end
|
|
|
|
|
|
if settings.enabled ~= '1' and not force then
|
|
io.stderr:write('autoupdater is disabled.\n')
|
|
os.exit(0)
|
|
end
|
|
|
|
|
|
-- Verifies a file given as a list of lines with a list of signatures using ecdsaverify
|
|
local function verify_lines(lines, sigs)
|
|
local command = {'ecdsaverify', '-n', tostring(branch.good_signatures)}
|
|
|
|
-- Build command line from sigs and branch.pubkey
|
|
for _, sig in ipairs(sigs) do
|
|
if sig:match('^' .. string.rep('%x', 128) .. '$') then
|
|
table.insert(command, '-s')
|
|
table.insert(command, sig)
|
|
end
|
|
end
|
|
|
|
for _, key in ipairs(branch.pubkey) do
|
|
if key:match('^' .. string.rep('%x', 64) .. '$') then
|
|
table.insert(command, '-p')
|
|
table.insert(command, key)
|
|
end
|
|
end
|
|
|
|
|
|
-- Call ecdsautils
|
|
local pid, f = autoupdater_util.popen(unpack(command))
|
|
|
|
for _, line in ipairs(lines) do
|
|
f:write(line)
|
|
f:write('\n')
|
|
end
|
|
|
|
f:close()
|
|
|
|
|
|
local wpid, status, code = nixio.waitpid(pid)
|
|
return wpid and status == 'exited' and code == 0
|
|
end
|
|
|
|
|
|
-- Downloads, parses and verifies the update manifest from a mirror
|
|
-- Returns a table with the fields version, checksum and filename if everything is ok, nil otherwise
|
|
local function read_manifest(mirror)
|
|
local sep = false
|
|
|
|
local lines = {}
|
|
local sigs = {}
|
|
|
|
local branch_ok = false
|
|
|
|
local ret = {}
|
|
|
|
-- Read all lines from the manifest
|
|
-- The upper part is saves to lines, the lower part to sigs
|
|
for line in io.popen(string.format("exec wget -T 120 -O- '%s/%s.manifest'", mirror, branch.name), 'r'):lines() do
|
|
if not sep then
|
|
if line == '---' then
|
|
sep = true
|
|
else
|
|
table.insert(lines, line)
|
|
|
|
if line == ('BRANCH=' .. branch.name) then
|
|
branch_ok = true
|
|
end
|
|
|
|
local date = line:match('^DATE=(.+)$')
|
|
local priority = line:match('^PRIORITY=([%d%.]+)$')
|
|
local model, version, checksum, filename = line:match('^([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+)$')
|
|
|
|
if date then
|
|
ret.date = autoupdater_util.parse_date(date)
|
|
elseif priority then
|
|
ret.priority = tonumber(priority)
|
|
elseif model == platform_info.get_image_name() then
|
|
ret.version = version
|
|
ret.checksum = checksum
|
|
ret.filename = filename
|
|
end
|
|
end
|
|
else
|
|
table.insert(sigs, line)
|
|
end
|
|
end
|
|
|
|
-- Do some very basic checks before checking the signatures
|
|
-- (as the signature verification is computationally expensive)
|
|
if not sep then
|
|
io.stderr:write('There seems to have gone something wrong downloading the manifest from ' .. mirror .. '\n')
|
|
return nil
|
|
end
|
|
|
|
if not ret.date or not ret.priority then
|
|
io.stderr:write('The manifest downloaded from ' .. mirror .. ' is invalid (DATE or PRIORITY missing)\n')
|
|
return nil
|
|
end
|
|
|
|
if not branch_ok then
|
|
io.stderr:write('Wrong branch. We are on ', branch.name, '.\n')
|
|
return nil
|
|
end
|
|
|
|
if not ret.version then
|
|
io.stderr:write('No matching firmware found (model ' .. platform_info.get_image_name() .. ')\n')
|
|
return nil
|
|
end
|
|
|
|
if not verify_lines(lines, sigs) then
|
|
io.stderr:write('Not enough valid signatures!\n')
|
|
return nil
|
|
end
|
|
|
|
return ret
|
|
end
|
|
|
|
|
|
-- Downloads the firmware image from a mirror to a given output file
|
|
local function fetch_firmware(mirror, filename, output)
|
|
if autoupdater_util.exec('wget', '-T', '120', '-O', output, mirror .. '/' .. filename) ~= 0 then
|
|
io.stderr:write('Error downloading the image from ' .. mirror .. '\n')
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
-- Returns the computed update probability
|
|
local function get_probability(date, priority)
|
|
local seconds = priority * 86400
|
|
local diff = os.difftime(os.time(), date)
|
|
|
|
if diff < 0 then
|
|
-- When the difference is negative, there are two possibilities: The manifest contains a wrong date, or our own clock is wrong.
|
|
-- As there isn't anything useful to do for an incorrect manifest, we'll assume the latter case and update anyways as we
|
|
-- can't do anything better
|
|
io.stderr:write('Warning: clock seems to be incorrect.\n')
|
|
|
|
if tonumber(fs.readfile('/proc/uptime'):match('^([^ ]+) ')) < 600 then
|
|
-- If the uptime is very low, it's possible we just didn't get the time over NTP yet, so we'll just wait until the next time the updater runs
|
|
return 0
|
|
else
|
|
-- Will give 1 when priority == 0, and lower probabilities the higher the priority value is
|
|
-- (similar to the old static probability system)
|
|
return 0.75^priority
|
|
end
|
|
|
|
elseif fallback then
|
|
if diff >= seconds + 86400 then
|
|
return 1
|
|
else
|
|
return 0
|
|
end
|
|
|
|
elseif diff >= seconds then
|
|
return 1
|
|
|
|
else
|
|
local x = diff/seconds
|
|
-- This is the most simple polynomial with value 0 at 0, 1 at 1, and whose first derivative is 0 at both 0 and 1
|
|
-- (we all love continuously differentiable functions, right?)
|
|
return (-2)*x^3 + 3*x^2
|
|
end
|
|
end
|
|
|
|
|
|
-- Tries to perform an update from a given mirror
|
|
local function autoupdate(mirror)
|
|
local download_d_dir = '/usr/lib/autoupdater/download.d'
|
|
local abort_d_dir = '/usr/lib/autoupdater/abort.d'
|
|
local upgrade_d_dir = '/usr/lib/autoupdater/upgrade.d'
|
|
|
|
|
|
local manifest = read_manifest(mirror)
|
|
if not manifest then
|
|
return false
|
|
end
|
|
|
|
if not autoupdater_version.newer_than(manifest.version, old_version) then
|
|
io.stderr:write('No new firmware available.\n')
|
|
return true
|
|
end
|
|
|
|
io.stderr:write('New version available.\n')
|
|
|
|
|
|
if not force and math.random() >= get_probability(manifest.date, manifest.priority) then
|
|
io.stderr:write('No autoupdate this time. Use -f to override.\n')
|
|
return true
|
|
end
|
|
|
|
autoupdater_util.run_dir(download_d_dir)
|
|
collectgarbage()
|
|
|
|
local image = os.tmpname()
|
|
if not fetch_firmware(mirror, manifest.filename, image) then
|
|
autoupdater_util.run_dir(abort_d_dir)
|
|
return false
|
|
end
|
|
|
|
local popen = io.popen(string.format("exec sha512sum '%s'", image))
|
|
local checksum = popen:read('*l'):match('^%x+')
|
|
popen:close()
|
|
if checksum ~= manifest.checksum then
|
|
io.stderr:write('Invalid image checksum!\n')
|
|
os.remove(image)
|
|
autoupdater_util.run_dir(abort_d_dir)
|
|
return false
|
|
end
|
|
|
|
autoupdater_util.run_dir(upgrade_d_dir)
|
|
|
|
io.stderr:write('Upgrading firmware...\n')
|
|
local null = nixio.open('/dev/null', 'w+')
|
|
if null then
|
|
nixio.dup(null, nixio.stdin)
|
|
nixio.dup(null, nixio.stderr)
|
|
if null:fileno() > 2 then
|
|
null:close()
|
|
end
|
|
end
|
|
|
|
nixio.exec('/sbin/sysupgrade', image)
|
|
|
|
-- This should never be reached as nixio.exec replaces the autoupdater process unless /sbin/sysupgrade can't be executed
|
|
-- We output the error message through stdout as stderr isn't available anymore
|
|
io.write('Failed to call sysupgrade?\n')
|
|
os.remove(image)
|
|
autoupdater_util.run_dir(abort_d_dir)
|
|
os.exit(1)
|
|
end
|
|
|
|
|
|
local mirrors = branch.mirror
|
|
|
|
while #mirrors > 0 do
|
|
local mirror = table.remove(mirrors, math.random(#mirrors))
|
|
if autoupdate(mirror) then
|
|
os.exit(0)
|
|
end
|
|
end
|
|
|
|
io.stderr:write('No usable mirror found.\n')
|
|
os.exit(1)
|