diff --git a/autoupdater-mod/Makefile b/autoupdater-mod/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..4bd78a11cd11ac8a6b61fa341b43d3f3ea4bfaa7 --- /dev/null +++ b/autoupdater-mod/Makefile @@ -0,0 +1,38 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=ffnw-autoupdater-mod +PKG_VERSION:=1 +PKG_RELEASE:=1 + +PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME) + +include $(INCLUDE_DIR)/package.mk + +define Package/ffnw-autoupdater-mod + SECTION:=Daemon + CATEGORY:=Freifunk Nordwest + TITLE:=custom autoupdater special for batman upgrade +endef + +define Package/ffnw-autoupdater-mod/description + custom autoupdater special for batman upgrade +endef + +define Build/Prepare + mkdir -p $(PKG_BUILD_DIR) +endef + +define Build/Configure +endef + +define Build/Compile +endef + +define Package/ffnw-autoupdater-mod/install + $(INSTALL_DIR) $(1)/lib/gluon/cron/ + $(INSTALL_DATA) files/lib/gluon/cron/autoupdater-mod $(1)/lib/gluon/cron/autoupdater-mod + $(INSTALL_DIR) $(1)/lib/ffnw/autoupdater-mod/ + $(INSTALL_BIN) files/lib/ffnw/autoupdater-mod/* $(1)/lib/ffnw/autoupdater-mod/ +endef + +$(eval $(call BuildPackage,ffnw-autoupdater-mod)) diff --git a/autoupdater-mod/files/lib/ffnw/autoupdater-mod/autoupdater b/autoupdater-mod/files/lib/ffnw/autoupdater-mod/autoupdater new file mode 100755 index 0000000000000000000000000000000000000000..ce9f2b10247c63c4cd7cb60611193a9a7cdacb08 --- /dev/null +++ b/autoupdater-mod/files/lib/ffnw/autoupdater-mod/autoupdater @@ -0,0 +1,279 @@ +#!/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 = uci:get_all('autoupdater', 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 + + +for _, a in ipairs(arg) do + if a == '-f' then + force = true + elseif a == '--fallback' then + fallback = true + end +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 -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 + -- 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) diff --git a/autoupdater-mod/files/lib/ffnw/autoupdater-mod/autoupdater-mod.sh b/autoupdater-mod/files/lib/ffnw/autoupdater-mod/autoupdater-mod.sh new file mode 100755 index 0000000000000000000000000000000000000000..f8b7b4fae4467c199471bdb47913d9e9ec60fd9d --- /dev/null +++ b/autoupdater-mod/files/lib/ffnw/autoupdater-mod/autoupdater-mod.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +cleanup () { + rm -rf /lib/gluon/cron/autoupdater-mod + rm -rf /lib/ffnw/autoupdater-mod + } + +find /usr/sbin/ -name "autoupdater" +if [ $? -eq 0 ]; then + mv /lib/ffnw/autoupdater-mod/autoupdater /usr/sbin/autoupdater + cleanup +else + cleanup +fi + diff --git a/autoupdater-mod/files/lib/gluon/cron/autoupdater-mod b/autoupdater-mod/files/lib/gluon/cron/autoupdater-mod new file mode 100644 index 0000000000000000000000000000000000000000..2a8c1e40104ca5c23dcdd9958439314523c57586 --- /dev/null +++ b/autoupdater-mod/files/lib/gluon/cron/autoupdater-mod @@ -0,0 +1 @@ +*/5 * * * * sh /lib/ffnw/autoupdater-mod/autoupdater-mod.sh