#!/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') 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 old_version = util.trim(fs.readfile(settings.version_file) or '') -- 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 = 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 -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 os.execute(string.format("wget -T 120 -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 -- 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 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)