mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-13 20:08:05 +08:00
🎁 Sync 2026-10-28 19:35:15
This commit is contained in:
1 parent
85209f310d
commit
390801c844
90 files changed
+5821
-1967
No files matched your search
@@ -22,7 +22,7 @@ local function sink_socket(sock, io_err)
|
||||
if not chunk then
|
||||
return 1
|
||||
else
|
||||
return sock:send(chunk)
|
||||
return sock:sendall(chunk)
|
||||
end
|
||||
end
|
||||
else
|
||||
@@ -64,14 +64,17 @@ local function chunksource(sock, buffer)
|
||||
return function()
|
||||
local output
|
||||
local _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
|
||||
while not count and #buffer <= 1024 do
|
||||
while not count and #buffer < 1024 do
|
||||
local newblock, code = sock:recv(1024 - #buffer)
|
||||
if not newblock then
|
||||
if not newblock or newblock == "" then
|
||||
return nil, code
|
||||
end
|
||||
buffer = buffer .. newblock
|
||||
_, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
|
||||
end
|
||||
if not count then
|
||||
return nil, -1, "invalid encoding"
|
||||
end
|
||||
count = tonumber(count, 16)
|
||||
if not count then
|
||||
return nil, -1, "invalid encoding"
|
||||
@@ -83,6 +86,7 @@ local function chunksource(sock, buffer)
|
||||
return output
|
||||
else
|
||||
output = buffer:sub(endp+1, endp+count)
|
||||
local buffered_tail = #buffer - endp - #output
|
||||
buffer = ""
|
||||
if count - #output > 0 then
|
||||
local remain, code = sock:recvall(count-#output)
|
||||
@@ -92,7 +96,7 @@ local function chunksource(sock, buffer)
|
||||
output = output .. remain
|
||||
count, code = sock:recvall(2)
|
||||
else
|
||||
count, code = sock:recvall(count+2-#buffer+endp)
|
||||
count, code = sock:recvall(2-buffered_tail)
|
||||
end
|
||||
if not count then
|
||||
return nil, code
|
||||
|
||||
@@ -95,14 +95,30 @@ function auto_setup()
|
||||
-- form style
|
||||
local packages = luci.http.formvalue("packages")
|
||||
local pkgs = ""
|
||||
local function valid_package(value)
|
||||
return type(value) == "string" and #value <= 128 and
|
||||
value:match("^[%w][%w%.%+_%-]*$") ~= nil
|
||||
end
|
||||
local function invalid_package()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({success=1, scope="params", error="Invalid package name"})
|
||||
end
|
||||
if type(packages) == "table" then
|
||||
if #packages > 0 then
|
||||
for k, v in pairs(packages) do
|
||||
if not valid_package(v) then
|
||||
invalid_package()
|
||||
return
|
||||
end
|
||||
pkgs = pkgs .. " " .. luci.util.shellquote(v)
|
||||
end
|
||||
end
|
||||
else
|
||||
if packages ~= nil and packages ~= "" then
|
||||
if not valid_package(packages) then
|
||||
invalid_package()
|
||||
return
|
||||
end
|
||||
pkgs = luci.util.shellquote(packages)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Validate all arguments before touching logs, installing packages or sourcing
|
||||
# metadata scripts. This script is also callable without the LuCI controller.
|
||||
[ "$#" -gt 0 ] || { echo "No packages specified" >&2; exit 1; }
|
||||
for pkg in "$@"; do
|
||||
case "$pkg" in
|
||||
''|[!a-zA-Z0-9]*|*[!a-zA-Z0-9_.+-]*) echo "Invalid package name" >&2; exit 1 ;;
|
||||
esac
|
||||
[ "${#pkg}" -le 128 ] || { echo "Invalid package name" >&2; exit 1; }
|
||||
done
|
||||
|
||||
> /var/log/auto_setup.success
|
||||
> /var/log/auto_setup.failed
|
||||
> /var/log/auto_setup.input
|
||||
|
||||
save_input() {
|
||||
local pkg
|
||||
for pkg in $@; do
|
||||
for pkg in "$@"; do
|
||||
echo "$pkg" >> /var/log/auto_setup.input
|
||||
done
|
||||
}
|
||||
@@ -37,14 +47,14 @@ load_quickstart_cfg() {
|
||||
auto_setup_app() {
|
||||
local pkg=$1
|
||||
is-opkg install "app-meta-$pkg" || return 1
|
||||
sh -c ". '/usr/libexec/istorea/$pkg.sh'"
|
||||
sh -c '. "$1"' sh "/usr/libexec/istorea/$pkg.sh"
|
||||
}
|
||||
|
||||
auto_setup_apps() {
|
||||
local pkg
|
||||
for pkg in $@; do
|
||||
for pkg in "$@"; do
|
||||
echo "Setting up $pkg..."
|
||||
if auto_setup_app $pkg; then
|
||||
if auto_setup_app "$pkg"; then
|
||||
echo "Set up $pkg success"
|
||||
echo "$pkg" >> /var/log/auto_setup.success
|
||||
else
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Exercise the real script with task-owned paths and mocked package installs."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
class AutoSetupTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.logs = self.root / "log"
|
||||
self.logs.mkdir()
|
||||
(self.root / "bin").mkdir()
|
||||
(self.root / "metadata").mkdir()
|
||||
self.installed = self.root / "installed"
|
||||
self.configured = self.root / "configured"
|
||||
helper = self.root / "functions.sh"
|
||||
helper.write_text('config_load() { return 0; }\nconfig_get() { eval "$1=\\\"$FIXTURE_MAIN\\\""; }\n')
|
||||
installer = self.root / "bin/is-opkg"
|
||||
installer.write_text('#!/bin/sh\nprintf "%s\\n" "$@" >> "$FIXTURE_INSTALLED"\n')
|
||||
installer.chmod(0o700)
|
||||
source = (Path(__file__).resolve().parents[1] / "root/usr/libexec/quickstart/auto_setup.sh").read_text()
|
||||
source = source.replace("/var/log", str(self.logs)).replace("/lib/functions.sh", str(helper)).replace("/usr/libexec/istorea", str(self.root / "metadata"))
|
||||
self.script = self.root / "auto_setup.sh"
|
||||
self.script.write_text(source)
|
||||
self.env = dict(os.environ, PATH=str(self.root / "bin") + ":" + os.environ["PATH"], FIXTURE_MAIN=str(self.root / "data"), FIXTURE_INSTALLED=str(self.installed), FIXTURE_CONFIGURED=str(self.configured))
|
||||
|
||||
def run_script(self, *args):
|
||||
return subprocess.run(["sh", str(self.script), *args], env=self.env, capture_output=True, text=True, timeout=5)
|
||||
|
||||
def test_rejects_all_invalid_arguments_before_side_effects(self):
|
||||
for name in ("../other", "--help", "two packages", "pkg'", "", "a" * 129):
|
||||
result = self.run_script("aria2", name)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertFalse(self.installed.exists())
|
||||
self.assertEqual(list(self.logs.iterdir()), [])
|
||||
|
||||
def test_normal_packages_install_configure_and_report(self):
|
||||
packages = ("aria2", "foo+bar_1.2-3")
|
||||
for name in packages:
|
||||
(self.root / "metadata" / (name + ".sh")).write_text('printf "%s\\n" "' + name + '" >> "$FIXTURE_CONFIGURED"\n')
|
||||
result = self.run_script(*packages)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(self.installed.read_text().splitlines(), ["install", "app-meta-aria2", "install", "app-meta-foo+bar_1.2-3"])
|
||||
self.assertEqual(self.configured.read_text().splitlines(), list(packages))
|
||||
self.assertEqual((self.logs / "auto_setup.success").read_text().splitlines(), list(packages))
|
||||
self.assertEqual((self.logs / "auto_setup.input").read_text().splitlines(), list(packages))
|
||||
self.assertEqual((self.logs / "auto_setup.failed").read_text(), "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,88 @@
|
||||
-- Run against a staged source copy: lua tests/security_regression.lua ROOT
|
||||
-- All LuCI, filesystem and command dependencies are mocked.
|
||||
local root = assert(arg[1], "source root required")
|
||||
local forms, response, executions = {}, nil, {}
|
||||
local http = {
|
||||
formvalue = function(key) return forms[key] end,
|
||||
prepare_content = function() end,
|
||||
write_json = function(value) response = value end,
|
||||
}
|
||||
local fs = { readfile = function() return "" end, unlink = function() end }
|
||||
local nixio = { bit = { rshift = function(value, bits) return math.floor(value / 2^bits) end } }
|
||||
local util = { shellquote = function(value) return "'" .. value:gsub("'", "'\\''") .. "'" end }
|
||||
package.preload["luci.http"] = function() return http end
|
||||
package.preload["luci.util"] = function() return util end
|
||||
package.preload["luci.ltn12"] = function() return {} end
|
||||
package.preload["nixio"] = function() return nixio end
|
||||
package.preload["nixio.fs"] = function() return fs end
|
||||
_G.luci = { http = http, util = util }
|
||||
_G.nixio = nixio
|
||||
local original_execute = os.execute
|
||||
os.execute = function(command) executions[#executions+1] = command; return 0 end
|
||||
assert(loadfile(root .. "/luasrc/controller/quickstart.lua"))()
|
||||
local quickstart = package.loaded["luci.controller.quickstart"]
|
||||
for _, packages in ipairs({"../other", "--help", "two packages", "pkg'", {"aria2", "../other"}, {"aria2", {}}}) do
|
||||
forms, response, executions = {packages=packages}, nil, {}
|
||||
quickstart.auto_setup()
|
||||
assert(response and response.success ~= 0 and response.scope == "params", "invalid package accepted")
|
||||
assert(#executions == 0, "invalid package reached shell")
|
||||
end
|
||||
for _, packages in ipairs({"aria2", {"qbittorrent", "foo+bar_1.2-3"}}) do
|
||||
forms, response, executions = {packages=packages}, nil, {}
|
||||
quickstart.auto_setup()
|
||||
assert(response and response.success == 0 and #executions == 1, "normal package failed")
|
||||
end
|
||||
os.execute = original_execute
|
||||
|
||||
assert(loadfile(root .. "/luasrc/controller/istore_backend.lua"))()
|
||||
local proxy = package.loaded["luci.controller.istore_backend"].istore_backend
|
||||
local function upvalue(name)
|
||||
for i=1,100 do
|
||||
local key, value = debug.getupvalue(proxy, i)
|
||||
if not key then break end
|
||||
if key == name then return value end
|
||||
end
|
||||
error("missing proxy helper " .. name)
|
||||
end
|
||||
local sink = upvalue("sink_socket")
|
||||
local sent = ""
|
||||
assert(sink({sendall=function(_, chunk) sent=sent..chunk; return #chunk end})("whole body"))
|
||||
assert(sent == "whole body", "proxy did not send complete input")
|
||||
|
||||
local chunksource = upvalue("chunksource")
|
||||
local wire = "5\r\nhello\r\n5\r\nworld\r\n0\r\n\r\n"
|
||||
for split=0,#wire do
|
||||
local pos, calls = split+1, 0
|
||||
local sock = {}
|
||||
function sock:recv(n)
|
||||
assert(n > 0, "zero-length receive")
|
||||
calls = calls + 1
|
||||
assert(calls < 100, "unbounded receive loop")
|
||||
local data=wire:sub(pos, pos+math.min(n, 3)-1)
|
||||
pos=pos+#data
|
||||
return data
|
||||
end
|
||||
function sock:recvall(n)
|
||||
local result=""
|
||||
while #result<n do
|
||||
local data=self:recv(n-#result)
|
||||
if not data or data=="" then return nil, -1 end
|
||||
result=result..data
|
||||
end
|
||||
return result
|
||||
end
|
||||
local source=chunksource(sock, wire:sub(1,split))
|
||||
local output=""
|
||||
for i=1,100 do
|
||||
local data, err=source()
|
||||
assert(not err, "chunk parse failed at split " .. split)
|
||||
if not data then break end
|
||||
output=output..data
|
||||
end
|
||||
assert(output=="helloworld", "chunk boundary corrupted body at " .. split)
|
||||
end
|
||||
local source=chunksource({recv=function() return "" end}, "incomplete")
|
||||
assert(source()==nil, "EOF not handled")
|
||||
source=chunksource({recv=function() error("must not read zero bytes") end}, string.rep("z",1024))
|
||||
assert(source()==nil, "oversize chunk header accepted")
|
||||
print("PASS: package validation, full proxy writes, all chunk boundaries, EOF and header limit")
|
||||
Reference in new issue
Block a user