luci-mod-admin-full: restructure and fix backup, restore and sysuprade (#517)

Do not use standard post security checking for actions that require file upload
since reading the token value will trigger parsing of the http message body
before the file upload handler has been set, which causes LuCI to buffer the
entire request body in memory.

In order to simplify the code and logic flow, split action_flashops() into
separate handlers for reset, backup, restore and sysupgrade.

Let the backup restore and sysupgrade handlers use the new test_post_security()
method in luci.dispatcher to perform token checking *after* setting the upload
handler.

Signed-off-by: Jo-Philipp Wich <jow@openwrt.org>
This commit is contained in:
Jo-Philipp Wich 2015-10-22 08:35:34 +02:00
parent d32c685039
commit 94ab57f48c
3 changed files with 202 additions and 160 deletions

View file

@ -31,9 +31,15 @@ function index()
entry({"admin", "system", "leds"}, cbi("admin_system/leds"), _("<abbr title=\"Light Emitting Diode\">LED</abbr> Configuration"), 60) entry({"admin", "system", "leds"}, cbi("admin_system/leds"), _("<abbr title=\"Light Emitting Diode\">LED</abbr> Configuration"), 60)
end end
entry({"admin", "system", "flashops"}, post_on({ exec = "1" }, "action_flashops"), _("Backup / Flash Firmware"), 70) entry({"admin", "system", "flashops"}, call("action_flashops"), _("Backup / Flash Firmware"), 70)
entry({"admin", "system", "flashops", "reset"}, post("action_reset"))
entry({"admin", "system", "flashops", "backup"}, post("action_backup"))
entry({"admin", "system", "flashops", "backupfiles"}, form("admin_system/backupfiles")) entry({"admin", "system", "flashops", "backupfiles"}, form("admin_system/backupfiles"))
-- call() instead of post() due to upload handling!
entry({"admin", "system", "flashops", "restore"}, call("action_restore"))
entry({"admin", "system", "flashops", "sysupgrade"}, call("action_sysupgrade"))
entry({"admin", "system", "reboot"}, template("admin_system/reboot"), _("Reboot"), 90) entry({"admin", "system", "reboot"}, template("admin_system/reboot"), _("Reboot"), 90)
entry({"admin", "system", "reboot", "call"}, post("action_reboot")) entry({"admin", "system", "reboot", "call"}, post("action_reboot"))
end end
@ -171,31 +177,25 @@ function action_packages()
end end
end end
function action_flashops() local function image_supported(image)
local http = require "luci.http" return (os.execute("sysupgrade -T %q >/dev/null" % image) == 0)
local sys = require "luci.sys"
local fs = require "nixio.fs"
local submit = (http.formvalue("exec") == "1")
local upgrade_avail = fs.access("/lib/upgrade/platform.sh")
local reset_avail = os.execute([[grep '"rootfs_data"' /proc/mtd >/dev/null 2>&1]]) == 0
local restore_cmd = "tar -xzC/ >/dev/null 2>&1"
local backup_cmd = "sysupgrade --create-backup - 2>/dev/null"
local image_tmp = "/tmp/firmware.img"
local function image_supported()
return (os.execute("sysupgrade -T %q >/dev/null" % image_tmp) == 0)
end end
local function image_checksum() local function image_checksum(image)
return (luci.sys.exec("md5sum %q" % image_tmp):match("^([^%s]+)")) return (luci.sys.exec("md5sum %q" % image):match("^([^%s]+)"))
end
local function supports_sysupgrade()
return nixio.fs.access("/lib/upgrade/platform.sh")
end
local function supports_reset()
return (os.execute([[grep -sq '"rootfs_data"' /proc/mtd]]) == 0)
end end
local function storage_size() local function storage_size()
local size = 0 local size = 0
if fs.access("/proc/mtd") then if nixio.fs.access("/proc/mtd") then
for l in io.lines("/proc/mtd") do for l in io.lines("/proc/mtd") do
local d, s, e, n = l:match('^([^%s]+)%s+([^%s]+)%s+([^%s]+)%s+"([^%s]+)"') local d, s, e, n = l:match('^([^%s]+)%s+([^%s]+)%s+([^%s]+)%s+"([^%s]+)"')
if n == "linux" or n == "firmware" then if n == "linux" or n == "firmware" then
@ -203,7 +203,7 @@ function action_flashops()
break break
end end
end end
elseif fs.access("/proc/partitions") then elseif nixio.fs.access("/proc/partitions") then
for l in io.lines("/proc/partitions") do for l in io.lines("/proc/partitions") do
local x, y, b, n = l:match('^%s*(%d+)%s+(%d+)%s+([^%s]+)%s+([^%s]+)') local x, y, b, n = l:match('^%s*(%d+)%s+(%d+)%s+([^%s]+)%s+([^%s]+)')
if b and n and not n:match('[0-9]') then if b and n and not n:match('[0-9]') then
@ -215,61 +215,59 @@ function action_flashops()
return size return size
end end
function action_flashops()
-- --
-- Handle modifying actions -- Overview
-- --
if submit then luci.template.render("admin_system/flashops", {
reset_avail = supports_reset(),
upgrade_avail = supports_sysupgrade()
})
end
function action_sysupgrade()
local fs = require "nixio.fs"
local http = require "luci.http"
local image_tmp = "/tmp/firmware.img"
local fp local fp
http.setfilehandler( http.setfilehandler(
function(meta, chunk, eof) function(meta, chunk, eof)
if not fp then if not fp and meta and meta.name == "image" then
if meta and meta.name == "image" then
fp = io.open(image_tmp, "w") fp = io.open(image_tmp, "w")
else
fp = io.popen(restore_cmd, "w")
end end
end if fp and chunk then
if chunk then
fp:write(chunk) fp:write(chunk)
end end
if eof then if fp and eof then
fp:close() fp:close()
end end
end end
) )
if http.formvalue("backup") then if not luci.dispatcher.test_post_security() then
-- fs.unlink(image_tmp)
-- Assemble file list, generate backup return
-- end
local reader = ltn12_popen(backup_cmd)
http.header('Content-Disposition', 'attachment; filename="backup-%s-%s.tar.gz"' % { --
luci.sys.hostname(), os.date("%Y-%m-%d")}) -- Cancel firmware flash
http.prepare_content("application/x-targz") --
luci.ltn12.pump.all(reader, http.write) if http.formvalue("cancel") then
return fs.unlink(image_tmp)
http.redirect(luci.dispatcher.build_url('admin/system/flashops'))
elseif http.formvalue("restore") then
--
-- Unpack received .tar.gz
--
local upload = http.formvalue("archive")
if upload and #upload > 0 then
luci.template.render("admin_system/applyreboot")
luci.sys.reboot()
return return
end end
elseif http.formvalue("image") or http.formvalue("step") then
-- --
-- Initiate firmware flash -- Initiate firmware flash
-- --
local step = tonumber(http.formvalue("step") or 1) local step = tonumber(http.formvalue("step") or 1)
if step == 1 then if step == 1 then
if image_supported() then if image_supported(image_tmp) then
luci.template.render("admin_system/upgrade", { luci.template.render("admin_system/upgrade", {
checksum = image_checksum(), checksum = image_checksum(image_tmp),
storage = storage_size(), storage = storage_size(),
size = (fs.stat(image_tmp, "size") or 0), size = (fs.stat(image_tmp, "size") or 0),
keep = (not not http.formvalue("keep")) keep = (not not http.formvalue("keep"))
@ -277,12 +275,11 @@ function action_flashops()
else else
fs.unlink(image_tmp) fs.unlink(image_tmp)
luci.template.render("admin_system/flashops", { luci.template.render("admin_system/flashops", {
reset_avail = reset_avail, reset_avail = supports_reset(),
upgrade_avail = upgrade_avail, upgrade_avail = supports_sysupgrade(),
image_invalid = true image_invalid = true
}) })
end end
return
-- --
-- Start sysupgrade flash -- Start sysupgrade flash
-- --
@ -294,29 +291,71 @@ function action_flashops()
addr = (#keep > 0) and "192.168.1.1" or nil addr = (#keep > 0) and "192.168.1.1" or nil
}) })
fork_exec("killall dropbear uhttpd; sleep 1; /sbin/sysupgrade %s %q" %{ keep, image_tmp }) fork_exec("killall dropbear uhttpd; sleep 1; /sbin/sysupgrade %s %q" %{ keep, image_tmp })
end
end
function action_backup()
local reader = ltn12_popen("sysupgrade --create-backup - 2>/dev/null")
luci.http.header(
'Content-Disposition', 'attachment; filename="backup-%s-%s.tar.gz"' %{
luci.sys.hostname(),
os.date("%Y-%m-%d")
})
luci.http.prepare_content("application/x-targz")
luci.ltn12.pump.all(reader, luci.http.write)
end
function action_restore()
local fs = require "nixio.fs"
local http = require "luci.http"
local archive_tmp = "/tmp/restore.tar.gz"
local fp
http.setfilehandler(
function(meta, chunk, eof)
if not fp and meta and meta.name == "archive" then
fp = io.open(archive_tmp, "w")
end
if fp and chunk then
fp:write(chunk)
end
if fp and eof then
fp:close()
end
end
)
if not luci.dispatcher.test_post_security() then
fs.unlink(archive_tmp)
return return
end end
elseif reset_avail and http.formvalue("reset") then
-- local upload = http.formvalue("archive")
-- Reset system if upload and #upload > 0 then
-- luci.template.render("admin_system/applyreboot")
os.execute("tar -C / -xzf %q >/dev/null 2>&1" % archive_tmp)
luci.sys.reboot()
return
end
http.redirect(luci.dispatcher.build_url('admin/system/flashops'))
end
function action_reset()
if supports_reset() then
luci.template.render("admin_system/applyreboot", { luci.template.render("admin_system/applyreboot", {
title = luci.i18n.translate("Erasing..."), title = luci.i18n.translate("Erasing..."),
msg = luci.i18n.translate("The system is erasing the configuration partition now and will reboot itself when finished."), msg = luci.i18n.translate("The system is erasing the configuration partition now and will reboot itself when finished."),
addr = "192.168.1.1" addr = "192.168.1.1"
}) })
fork_exec("killall dropbear uhttpd; sleep 1; mtd -r erase rootfs_data") fork_exec("killall dropbear uhttpd; sleep 1; mtd -r erase rootfs_data")
return return
end end
end
-- http.redirect(luci.dispatcher.build_url('admin/system/flashops'))
-- Overview
--
luci.template.render("admin_system/flashops", {
reset_avail = reset_avail,
upgrade_avail = upgrade_avail
})
end end
function action_passwd() function action_passwd()

View file

@ -17,38 +17,43 @@
<fieldset class="cbi-section"> <fieldset class="cbi-section">
<legend><%:Backup / Restore%></legend> <legend><%:Backup / Restore%></legend>
<form method="post" action="<%=url('admin/system/flashops')%>" enctype="multipart/form-data">
<input type="hidden" name="exec" value="1" />
<input type="hidden" name="token" value="<%=token%>" />
<div class="cbi-section-descr"><%:Click "Generate archive" to download a tar archive of the current configuration files. To reset the firmware to its initial state, click "Perform reset" (only possible with squashfs images).%></div> <div class="cbi-section-descr"><%:Click "Generate archive" to download a tar archive of the current configuration files. To reset the firmware to its initial state, click "Perform reset" (only possible with squashfs images).%></div>
<div class="cbi-section-node"> <div class="cbi-section-node">
<form class="inline" method="post" action="<%=url('admin/system/flashops/backup')%>">
<input type="hidden" name="token" value="<%=token%>" />
<div class="cbi-value<% if not reset_avail then %> cbi-value-last<% end %>"> <div class="cbi-value<% if not reset_avail then %> cbi-value-last<% end %>">
<label class="cbi-value-title" for="image"><%:Download backup%>:</label> <label class="cbi-value-title" for="image"><%:Download backup%>:</label>
<div class="cbi-value-field"> <div class="cbi-value-field">
<input class="cbi-button cbi-button-apply" type="submit" name="backup" value="<%:Generate archive%>" /> <input class="cbi-button cbi-button-apply" type="submit" name="backup" value="<%:Generate archive%>" />
</div> </div>
</div> </div>
</form>
<% if reset_avail then %> <% if reset_avail then %>
<form class="inline" method="post" action="<%=url('admin/system/flashops/reset')%>">
<input type="hidden" name="token" value="<%=token%>" />
<div class="cbi-value cbi-value-last"> <div class="cbi-value cbi-value-last">
<label class="cbi-value-title"><%:Reset to defaults%>:</label> <label class="cbi-value-title"><%:Reset to defaults%>:</label>
<div class="cbi-value-field"> <div class="cbi-value-field">
<input onclick="return confirm('<%:Really reset all changes?%>')" class="cbi-button cbi-button-reset" type="submit" name="reset" value="<%:Perform reset%>" /> <input onclick="return confirm('<%:Really reset all changes?%>')" class="cbi-button cbi-button-reset" type="submit" name="reset" value="<%:Perform reset%>" />
</div> </div>
</div> </div>
</form>
<% end %> <% end %>
</div> </div>
<br /> <br />
<div class="cbi-section-descr"><%:To restore configuration files, you can upload a previously generated backup archive here.%></div> <div class="cbi-section-descr"><%:To restore configuration files, you can upload a previously generated backup archive here.%></div>
<div class="cbi-section-node"> <div class="cbi-section-node">
<form class="inline" method="post" action="<%=url('admin/system/flashops/restore')%>" enctype="multipart/form-data">
<div class="cbi-value cbi-value-last"> <div class="cbi-value cbi-value-last">
<label class="cbi-value-title" for="archive"><%:Restore backup%>:</label> <label class="cbi-value-title" for="archive"><%:Restore backup%>:</label>
<div class="cbi-value-field"> <div class="cbi-value-field">
<input type="hidden" name="token" value="<%=token%>" />
<input type="file" name="archive" id="archive" /> <input type="file" name="archive" id="archive" />
<input type="submit" class="cbi-button cbi-input-apply" name="restore" value="<%:Upload archive...%>" /> <input type="submit" class="cbi-button cbi-input-apply" name="restore" value="<%:Upload archive...%>" />
</div> </div>
</div> </div>
</div>
</form> </form>
</div>
</fieldset> </fieldset>
<br /> <br />
@ -56,8 +61,7 @@
<fieldset class="cbi-section"> <fieldset class="cbi-section">
<legend><%:Flash new firmware image%></legend> <legend><%:Flash new firmware image%></legend>
<% if upgrade_avail then %> <% if upgrade_avail then %>
<form method="post" action="<%=url('admin/system/flashops')%>" enctype="multipart/form-data"> <form method="post" action="<%=url('admin/system/flashops/sysupgrade')%>" enctype="multipart/form-data">
<input type="hidden" name="exec" value="1" />
<input type="hidden" name="token" value="<%=token%>" /> <input type="hidden" name="token" value="<%=token%>" />
<div class="cbi-section-descr"><%:Upload a sysupgrade-compatible image here to replace the running firmware. Check "Keep settings" to retain the current configuration (requires an OpenWrt compatible firmware image).%></div> <div class="cbi-section-descr"><%:Upload a sysupgrade-compatible image here to replace the running firmware. Check "Keep settings" to retain the current configuration (requires an OpenWrt compatible firmware image).%></div>
<div class="cbi-section-node"> <div class="cbi-section-node">

View file

@ -45,12 +45,11 @@
</fieldset> </fieldset>
<div class="cbi-page-actions right"> <div class="cbi-page-actions right">
<form style="display:inline" action="<%=REQUEST_URI%>" method="post"> <form class="inline" action="<%=REQUEST_URI%>" method="post">
<input class="cbi-button cbi-button-reset" type="submit" value="<%:Cancel%>" /> <input type="hidden" name="token" value="<%=token%>" />
</form>
<form style="display:inline" action="<%=REQUEST_URI%>" method="post">
<input type="hidden" name="step" value="2" /> <input type="hidden" name="step" value="2" />
<input type="hidden" name="keep" value="<%=keep and "1" or ""%>" /> <input type="hidden" name="keep" value="<%=keep and "1" or ""%>" />
<input class="cbi-button cbi-button-reset" name="cancel" type="submit" value="<%:Cancel%>" />
<input class="cbi-button cbi-button-apply" type="submit" value="<%:Proceed%>" /> <input class="cbi-button cbi-button-apply" type="submit" value="<%:Proceed%>" />
</form> </form>
</div> </div>