autoupdater: convert to Lua
In addition, a new config field 'version_file' is added to remove the last Gluon-specific part from the autoupdater package.
This commit is contained in:
parent
93a61df6da
commit
dc73c75810
|
@ -1,7 +1,7 @@
|
||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=autoupdater
|
PKG_NAME:=autoupdater
|
||||||
PKG_VERSION:=1
|
PKG_VERSION:=2
|
||||||
|
|
||||||
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
|
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ include $(GLUONDIR)/include/package.mk
|
||||||
define Package/autoupdater
|
define Package/autoupdater
|
||||||
SECTION:=admin
|
SECTION:=admin
|
||||||
CATEGORY:=Administration
|
CATEGORY:=Administration
|
||||||
DEPENDS:=+lua-platform-info +opkg +ecdsautils +!BUSYBOX_CONFIG_SHA512SUM:coreutils-sha512sum
|
DEPENDS:=+lua-platform-info +luci-lib-core +ecdsautils +!BUSYBOX_CONFIG_SHA512SUM:coreutils-sha512sum
|
||||||
TITLE:=Automatically update firmware
|
TITLE:=Automatically update firmware
|
||||||
endef
|
endef
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#config autoupdater settings
|
#config autoupdater settings
|
||||||
# option enabled 1
|
# option enabled 1
|
||||||
# option branch "stable"
|
# option branch "stable"
|
||||||
|
# option version_file "/lib/firmware_version"
|
||||||
|
|
||||||
#config branch stable
|
#config branch stable
|
||||||
# The branch name given in the manifest
|
# The branch name given in the manifest
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
local io = io
|
||||||
|
local math = math
|
||||||
|
local nixio = require 'nixio'
|
||||||
|
|
||||||
|
|
||||||
|
module 'autoupdater.util'
|
||||||
|
|
||||||
|
|
||||||
|
-- Executes a command in the background, returning its PID and a pipe connected to the command's standard input
|
||||||
|
function popen(command)
|
||||||
|
local inr, inw = nixio.pipe()
|
||||||
|
local pid = nixio.fork()
|
||||||
|
|
||||||
|
if pid > 0 then
|
||||||
|
inr:close()
|
||||||
|
|
||||||
|
return pid, inw
|
||||||
|
elseif pid == 0 then
|
||||||
|
nixio.dup(inr, nixio.stdin)
|
||||||
|
|
||||||
|
inr:close()
|
||||||
|
inw:close()
|
||||||
|
|
||||||
|
nixio.exec('/bin/sh', '-c', command)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Seeds Lua's random generator from /dev/urandom
|
||||||
|
function randomseed()
|
||||||
|
local f = io.open('/dev/urandom', 'r')
|
||||||
|
local b1, b2, b3, b4 = f:read(4):byte(1, 4)
|
||||||
|
f:close()
|
||||||
|
|
||||||
|
-- The and is necessary as Lua on OpenWrt doesn't like integers over 2^31-1
|
||||||
|
math.randomseed(nixio.bit.band(b1*0x1000000 + b2*0x10000 + b3*0x100 + b4, 0x7fffffff))
|
||||||
|
end
|
|
@ -0,0 +1,79 @@
|
||||||
|
module 'autoupdater.version'
|
||||||
|
|
||||||
|
|
||||||
|
-- version comparison is based on dpkg code
|
||||||
|
local function isdigit(s, i)
|
||||||
|
local c = s:sub(i, i)
|
||||||
|
return c and c:match('^%d$')
|
||||||
|
end
|
||||||
|
|
||||||
|
local function char_value(s, i)
|
||||||
|
return s:byte(i, i) or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local function char_order(s, i)
|
||||||
|
local c = s:sub(i, i)
|
||||||
|
|
||||||
|
if c == '' or c:match('^%d$') then
|
||||||
|
return 0
|
||||||
|
elseif c:match('^%a$') then
|
||||||
|
return c:byte()
|
||||||
|
elseif c == '~' then
|
||||||
|
return -1
|
||||||
|
else
|
||||||
|
return c:byte() + 256
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- returns true when a is a higher version number than b
|
||||||
|
function newer_than(a, b)
|
||||||
|
local apos = 1
|
||||||
|
local bpos = 1
|
||||||
|
|
||||||
|
while apos <= a:len() or bpos <= b:len() do
|
||||||
|
local first_diff = 0
|
||||||
|
|
||||||
|
while (apos <= a:len() and not isdigit(a, apos)) or (bpos <= b:len() and not isdigit(b, bpos)) do
|
||||||
|
local ac = char_order(a, apos)
|
||||||
|
local bc = char_order(b, bpos)
|
||||||
|
|
||||||
|
if ac ~= bc then
|
||||||
|
return ac > bc
|
||||||
|
end
|
||||||
|
|
||||||
|
apos = apos + 1
|
||||||
|
bpos = bpos + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
while a:sub(apos, apos) == '0' do
|
||||||
|
apos = apos + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
while b:sub(bpos, bpos) == '0' do
|
||||||
|
bpos = bpos + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
while isdigit(a, apos) and isdigit(b, bpos) do
|
||||||
|
if first_diff == 0 then
|
||||||
|
first_diff = char_value(a, apos) - char_value(b, bpos)
|
||||||
|
end
|
||||||
|
|
||||||
|
apos = apos + 1
|
||||||
|
bpos = bpos + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if isdigit(a, apos) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if isdigit(b, bpos) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if first_diff ~= 0 then
|
||||||
|
return first_diff > 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
|
@ -1,186 +1,205 @@
|
||||||
#!/bin/sh
|
#!/usr/bin/lua
|
||||||
|
|
||||||
|
|
||||||
BRANCH=$(uci get autoupdater.settings.branch)
|
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')
|
||||||
|
|
||||||
PROBABILITY=$(uci get autoupdater.${BRANCH}.probability)
|
local autoupdater_util = require('autoupdater.util')
|
||||||
|
local autoupdater_version = require('autoupdater.version')
|
||||||
|
|
||||||
if test "a$1" != "a-f"; then
|
|
||||||
if test $(uci get autoupdater.settings.enabled) != 1; then
|
|
||||||
echo "autoupdater is disabled"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
# get one random byte from /dev/urandom, convert it to decimal and check
|
|
||||||
# against update_probability*255
|
|
||||||
hexdump -n1 -e '/1 "%d"' /dev/urandom | awk "{exit \$1 > $PROBABILITY * 255}"
|
|
||||||
if test $? -ne 0; then
|
|
||||||
echo "No autoupdate this time. Use -f to override"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
BRANCH_NAME=$(uci get autoupdater.${BRANCH}.name)
|
autoupdater_util.randomseed()
|
||||||
MIRRORS=$(for mirror in $(uci get autoupdater.${BRANCH}.mirror); do \
|
|
||||||
hexdump -n1 -e '/1 "%d '"$mirror"'\n"' /dev/urandom; \
|
|
||||||
done | sort -n | cut -d' ' -f2)
|
|
||||||
PUBKEYS=$(uci get autoupdater.${BRANCH}.pubkey)
|
|
||||||
GOOD_SIGNATURES=$(uci get autoupdater.${BRANCH}.good_signatures)
|
|
||||||
|
|
||||||
VERSION_FILE=/lib/gluon/release
|
|
||||||
|
|
||||||
# returns 0 when $1 is a higher version number than $2
|
local settings = uci:get_all('autoupdater', 'settings')
|
||||||
newer_than() {
|
local branch = uci:get_all('autoupdater', settings.branch)
|
||||||
# negate the return value as opkg returns 1 when the proposition is true
|
|
||||||
! opkg compare-versions "$1" '>>' "$2"
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch_manifest() {
|
|
||||||
local mirror=$1
|
|
||||||
local manifest=$2
|
|
||||||
|
|
||||||
wget -O$manifest "${mirror}/${BRANCH}.manifest" || wget -O$manifest "${mirror}/manifest"
|
local old_version = util.trim(fs.readfile(settings.version_file) or '')
|
||||||
|
|
||||||
if test $? -ne 0; then
|
|
||||||
echo "Couldn't fetch manifest from $mirror" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
if arg[1] ~= '-f' then
|
||||||
}
|
if settings.enabled ~= '1' then
|
||||||
|
io.stderr:write('autoupdater is disabled.\n')
|
||||||
|
os.exit(0)
|
||||||
|
end
|
||||||
|
|
||||||
verify_manifest() {
|
if math.random() >= tonumber(branch.probability) then
|
||||||
local manifest=$1
|
io.stderr:write('No autoupdate this time. Use -f to override.\n')
|
||||||
local manifest_upper=$2
|
os.exit(0)
|
||||||
local manifest_lower=$(mktemp)
|
end
|
||||||
awk "BEGIN { sep=0 }
|
end
|
||||||
/^---\$/ { sep=1; next }
|
|
||||||
{ if(sep==0) print > \"$manifest_upper\";
|
|
||||||
else print > \"$manifest_lower\"}" \
|
|
||||||
$manifest
|
|
||||||
|
|
||||||
local signatures=""
|
|
||||||
while read sig; do
|
|
||||||
echo "$sig" | grep -q "^[0-9a-f]\{128\}$"
|
|
||||||
if test $? -ne 0; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
signatures="$signatures -s $sig"
|
|
||||||
done < $manifest_lower
|
|
||||||
|
|
||||||
local pubkeys=""
|
-- Verifies a file given as a list of lines with a list of signatures using ecdsaverify
|
||||||
for key in $PUBKEYS; do
|
local function verify_lines(lines, sigs)
|
||||||
pubkeys="$pubkeys -p $key"
|
local command = string.format('ecdsaverify -n %i', branch.good_signatures)
|
||||||
done
|
|
||||||
|
|
||||||
rm -f $manifest_lower
|
-- 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
|
||||||
|
|
||||||
ecdsaverify -n $GOOD_SIGNATURES $pubkeys $signatures $manifest_upper
|
for _, key in ipairs(branch.pubkey) do
|
||||||
|
if key:match('^' .. string.rep('%x', 64) .. '$') then
|
||||||
|
command = command .. ' -p ' .. key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if test $? -ne 0; then
|
|
||||||
echo "Not enough valid signatures!" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
-- Call ecdsautils
|
||||||
}
|
local pid, f = autoupdater_util.popen(command)
|
||||||
|
|
||||||
analyse_manifest() {
|
for _, line in ipairs(lines) do
|
||||||
local manifest_upper=$1
|
f:write(line)
|
||||||
|
f:write('\n')
|
||||||
|
end
|
||||||
|
|
||||||
grep -q "^BRANCH=${BRANCH_NAME}$" $manifest_upper
|
f:close()
|
||||||
|
|
||||||
if test $? -ne 0; then
|
|
||||||
echo "Wrong branch. We are on ${BRANCH_NAME}" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local my_firmware
|
local wpid, status, code = nixio.waitpid(pid)
|
||||||
my_firmware=$(grep "^${my_model} " $manifest_upper)
|
return wpid and status == 'exited' and code == 0
|
||||||
|
end
|
||||||
|
|
||||||
if test $? -ne 0; then
|
|
||||||
echo "No matching firmware found (model ${my_model})" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
fw_version=$(echo "${my_firmware}"|cut -d' ' -f2)
|
-- Downloads, parses and verifies the update manifest from a mirror
|
||||||
fw_checksum=$(echo "${my_firmware}"|cut -d' ' -f3)
|
-- Returns a table with the fields version, checksum and filename if everything is ok, nil otherwise
|
||||||
fw_file=$(echo "${my_firmware}"|cut -d' ' -f4)
|
local function read_manifest(mirror)
|
||||||
|
local sep = false
|
||||||
|
|
||||||
return 0
|
local lines = {}
|
||||||
}
|
local sigs = {}
|
||||||
|
|
||||||
fetch_firmware() {
|
local branch_ok = false
|
||||||
local MIRROR=$1
|
|
||||||
local fw_image=$2
|
|
||||||
|
|
||||||
wget -O$fw_image "${MIRROR}/${fw_file}"
|
local ret = {}
|
||||||
|
|
||||||
if test $? -ne 0; then
|
-- Read all lines from the manifest
|
||||||
echo "Error downloading image from $MIRROR" >&2
|
-- The upper part is saves to lines, the lower part to sigs
|
||||||
return 1
|
for line in io.popen(string.format("wget -O- '%s/%s.manifest'", mirror, branch.name), 'r'):lines() do
|
||||||
fi
|
if not sep then
|
||||||
|
if line == '---' then
|
||||||
|
sep = true
|
||||||
|
else
|
||||||
|
table.insert(lines, line)
|
||||||
|
|
||||||
return 0
|
if line == ('BRANCH=' .. branch.name) then
|
||||||
}
|
branch_ok = true
|
||||||
|
end
|
||||||
|
|
||||||
autoupdate() {
|
local model, version, checksum, filename = line:match('^([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+)$')
|
||||||
local MIRROR=$1
|
|
||||||
|
|
||||||
local manifest=$(mktemp)
|
if model == platform_info.get_image_name() then
|
||||||
fetch_manifest $MIRROR $manifest || { rm -f $manifest; return 1; }
|
ret.version = version
|
||||||
|
ret.checksum = checksum
|
||||||
|
ret.filename = filename
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(sigs, line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local manifest_upper=$(mktemp)
|
-- Do some very basic checks before checking the signatures
|
||||||
verify_manifest $manifest $manifest_upper || { rm -f $manifest $manifest_upper; return 1; }
|
-- (as the signature verification is computationally expensive)
|
||||||
rm -f $manifest
|
if not sep then
|
||||||
|
io.stderr:write('There seems to have gone something wrong downloading the manifest from ' .. mirror .. '\n')
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
analyse_manifest $manifest_upper || { rm -f $manifest_upper; return 1; }
|
if not branch_ok then
|
||||||
rm -f $manifest_upper
|
io.stderr:write('Wrong branch. We are on ', branch.name, '.\n')
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
if newer_than "$fw_version" "$my_version"; then
|
if not ret.version then
|
||||||
echo "New version available"
|
io.stderr:write('No matching firmware found (model ' .. platform_info.get_image_name() .. ')\n')
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
# drop caches to make room for firmware image
|
if not verify_lines(lines, sigs) then
|
||||||
sync
|
io.stderr:write('Not enough valid signatures!\n')
|
||||||
sysctl -w vm.drop_caches=3
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
local fw_image=$(mktemp)
|
return ret
|
||||||
fetch_firmware $MIRROR $fw_image || { rm -f $fw_image; return 1; }
|
end
|
||||||
|
|
||||||
image_sha512=$(sha512sum "$fw_image" | awk '{print $1}')
|
|
||||||
image_md5=$(md5sum "$fw_image" | awk '{print $1}')
|
|
||||||
if [ "$image_sha512" != "$fw_checksum" -a "$image_md5" != "$fw_checksum" ]; then
|
|
||||||
echo "Invalid image checksum" >&2
|
|
||||||
rm -f $fw_image
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
echo "Upgrading firmware."
|
|
||||||
|
|
||||||
sysupgrade "${fw_image}"
|
-- Downloads the firmware image from a mirror to a given output file
|
||||||
else
|
local function fetch_firmware(mirror, filename, output)
|
||||||
echo "No new firmware available"
|
if os.execute(string.format("wget -O '%s' '%s/%s'", output, mirror, filename)) ~= 0 then
|
||||||
fi
|
io.stderr:write('Error downloading the image from ' .. mirror .. '\n')
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
return 0
|
return true
|
||||||
}
|
end
|
||||||
|
|
||||||
trap 'echo Signal ignored.' INT TERM PIPE
|
|
||||||
|
|
||||||
my_model="$(lua -e 'print(require("platform_info").get_image_name())')"
|
-- 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 [ ! -f "$VERSION_FILE" ]; then
|
if not autoupdater_version.newer_than(manifest.version, old_version) then
|
||||||
echo "Couldn't determine firmware version!" >&2
|
io.stderr:write('No new firmware available.\n')
|
||||||
exit 1
|
return true
|
||||||
fi
|
end
|
||||||
|
|
||||||
my_version="$(cat "$VERSION_FILE")"
|
io.stderr:write('New version available.\n')
|
||||||
|
|
||||||
for mirror in $MIRRORS; do
|
os.execute('sync; sysctl -w vm.drop_caches=3')
|
||||||
|
collectgarbage()
|
||||||
|
|
||||||
autoupdate $mirror && exit 0
|
local image = os.tmpname()
|
||||||
|
if not fetch_firmware(mirror, manifest.filename, image) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
unset fw_version
|
local checksum = util.exec(string.format("sha512sum '%s'", image)):match('^%x+')
|
||||||
unset fw_checksum
|
if checksum ~= manifest.checksum then
|
||||||
unset fw_file
|
io.stderr:write('Invalid image checksum!\n')
|
||||||
|
os.remove(image)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
done
|
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)
|
||||||
|
|
Loading…
Reference in New Issue