gluon-packages/admin/autoupdater/files/usr/sbin/autoupdater

237 lines
6.0 KiB
Lua
Executable File

#!/usr/bin/lua
local fs = require('luci.fs')
local nixio = require('nixio')
local platform_info = require('platform_info')
local uci = require('luci.model.uci').cursor()
local util = require('luci.util')
local autoupdater_util = require('autoupdater.util')
local autoupdater_version = require('autoupdater.version')
autoupdater_util.randomseed()
local settings = uci:get_all('autoupdater', 'settings')
local branch = uci:get_all('autoupdater', settings.branch)
local old_version = util.trim(fs.readfile(settings.version_file) or '')
local force = (arg[1] == '-f')
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 = string.format('ecdsaverify -n %i', 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
command = command .. ' -s ' .. sig
end
end
for _, key in ipairs(branch.pubkey) do
if key:match('^' .. string.rep('%x', 64) .. '$') then
command = command .. ' -p ' .. key
end
end
-- Call ecdsautils
local pid, f = autoupdater_util.popen(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("wget -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 os.execute(string.format("wget -O '%s' '%s/%s'", 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
return 0
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 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
os.execute('sync; sysctl -w vm.drop_caches=3')
collectgarbage()
local image = os.tmpname()
if not fetch_firmware(mirror, manifest.filename, image) then
return false
end
local checksum = util.exec(string.format("sha512sum '%s'", image)):match('^%x+')
if checksum ~= manifest.checksum then
io.stderr:write('Invalid image checksum!\n')
os.remove(image)
return false
end
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)
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)