mirror of
https://github.com/LamGC/quickly-conf-sshd.git
synced 2026-10-12 23:33:56 +00:00
test: isolate BusyBox standalone applets before shell scenarios
This commit is contained in:
@@ -3,7 +3,7 @@ export const escapeHTML = value => String(value).replace(/[&<>"']/g, c => ({'&':
|
|||||||
export function commandsFor(config) {
|
export function commandsFor(config) {
|
||||||
const fetch = `curl -fsSL '${config.baseUrl}/script.sh' | sh -s --`;
|
const fetch = `curl -fsSL '${config.baseUrl}/script.sh' | sh -s --`;
|
||||||
return [
|
return [
|
||||||
{id:'install',title:'安装并定时同步',description:'更新当前用户的公钥并设置定时同步;保留已有自定义计划,新安装默认每 15 分钟一次。',command:`${fetch} -c`},
|
{id:'install',title:'安装并定时同步',description:`更新当前用户的公钥并设置定时同步;保留已有自定义计划,新安装计划为 ${config.defaultCron || '*/15 * * * *'}。`,command:`${fetch} -c`},
|
||||||
{id:'once',title:'仅更新一次公钥',description:'更新为当前发布的公钥,不设置定时任务。',command:`${fetch} -o`},
|
{id:'once',title:'仅更新一次公钥',description:'更新为当前发布的公钥,不设置定时任务。',command:`${fetch} -o`},
|
||||||
{id:'help',title:'查看帮助',description:'先了解全部参数与行为,不修改配置。',command:`${fetch} -h`},
|
{id:'help',title:'查看帮助',description:'先了解全部参数与行为,不修改配置。',command:`${fetch} -h`},
|
||||||
{id:'preview',title:'预览公钥更新',description:'校验当前发布并预览将要修改的文件,不应用更新。',command:`${fetch} -o --dry-run`},
|
{id:'preview',title:'预览公钥更新',description:'校验当前发布并预览将要修改的文件,不应用更新。',command:`${fetch} -o --dry-run`},
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ usage() {
|
|||||||
Usage: sh conf-sshd.sh [options]
|
Usage: sh conf-sshd.sh [options]
|
||||||
(no options), -o, --only-update-keys Validate and sync managed public keys only
|
(no options), -o, --only-update-keys Validate and sync managed public keys only
|
||||||
-c, --cron ["five fields"|false] Sync and schedule; bare -c keeps an existing
|
-c, --cron ["five fields"|false] Sync and schedule; bare -c keeps an existing
|
||||||
schedule, otherwise every 15 minutes.
|
schedule, otherwise the configured default.
|
||||||
-c false only removes this script's schedule.
|
-c false only removes this script's schedule.
|
||||||
-u, --update-self Explicitly update the installed script only
|
-u, --update-self Explicitly update the installed script only
|
||||||
--uninstall Remove own schedule/script; preserve keys/config
|
--uninstall Remove own schedule/script; preserve keys/config
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ test('HTML escapes comments; commands use explicit script endpoint and separate
|
|||||||
const keys=await parseKeys(fixture.trim()+' <script>alert(1)</script>');
|
const keys=await parseKeys(fixture.trim()+' <script>alert(1)</script>');
|
||||||
const html=renderKeys(keys);assert.ok(html.includes('<script>'));assert.ok(!html.includes('<script>'));
|
const html=renderKeys(keys);assert.ok(html.includes('<script>'));assert.ok(!html.includes('<script>'));
|
||||||
const commands=commandsFor(config);assert.ok(commands.find(x=>x.id==='install').command.endsWith(' -c'));
|
const commands=commandsFor(config);assert.ok(commands.find(x=>x.id==='install').command.endsWith(' -c'));
|
||||||
assert.ok(commands.find(x=>x.id==='root-on').advanced);assert.ok(commands.find(x=>x.id==='uninstall').advanced);
|
assert.ok(commands.find(x=>x.id==='root-on').advanced);assert.ok(commandsFor({...config,defaultCron:'0 */6 * * *'})[0].description.includes('0 */6 * * *'));assert.ok(commands.find(x=>x.id==='uninstall').advanced);
|
||||||
assert.ok(commands.every(x=>!x.command.includes('github.io')&&!x.command.includes('.keys')));
|
assert.ok(commands.every(x=>!x.command.includes('github.io')&&!x.command.includes('.keys')));
|
||||||
});
|
});
|
||||||
test('build creates consistent assets/release; production empty is blocked',async()=>{
|
test('build creates consistent assets/release; production empty is blocked',async()=>{
|
||||||
|
|||||||
+60
-4
@@ -7,6 +7,7 @@ import base64
|
|||||||
import struct
|
import struct
|
||||||
import signal
|
import signal
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import shlex
|
import shlex
|
||||||
@@ -56,6 +57,9 @@ class ShellTests(unittest.TestCase):
|
|||||||
TEST_ROOT=str(self.root), TEST_RESPONSE=str(self.payload),
|
TEST_ROOT=str(self.root), TEST_RESPONSE=str(self.payload),
|
||||||
TEST_UPDATE=str(self.update), TEST_CRONTAB=str(self.cron),
|
TEST_UPDATE=str(self.update), TEST_CRONTAB=str(self.cron),
|
||||||
TEST_CALLS=str(self.calls))
|
TEST_CALLS=str(self.calls))
|
||||||
|
real_id = shutil.which('id')
|
||||||
|
self.assertIsNotNone(real_id)
|
||||||
|
self.mock('id', f'#!/bin/sh\nexec {shlex.quote(real_id)} "$@"\n')
|
||||||
self.mock('curl', '''#!/bin/sh
|
self.mock('curl', '''#!/bin/sh
|
||||||
printf 'curl %s\\n' "$*" >> "$TEST_CALLS"
|
printf 'curl %s\\n' "$*" >> "$TEST_CALLS"
|
||||||
[ "${TEST_CURL_ERROR:-0}" = 0 ] || exit "$TEST_CURL_ERROR"
|
[ "${TEST_CURL_ERROR:-0}" = 0 ] || exit "$TEST_CURL_ERROR"
|
||||||
@@ -88,12 +92,47 @@ fi
|
|||||||
|
|
||||||
def mock(self, name, text):
|
def mock(self, name, text):
|
||||||
path = self.bin / name
|
path = self.bin / name
|
||||||
|
# Probes use invalid real-tool arguments, so a failed interception can
|
||||||
|
# never mutate the actual account or crontab. Every mock answers before
|
||||||
|
# its ordinary test-specific implementation runs.
|
||||||
|
first, rest = text.split('\n', 1)
|
||||||
|
text = (first + '\n'
|
||||||
|
'if [ "${1-}" = --conf-sshd-isolation-probe ]; then\n'
|
||||||
|
f' printf \'%s\\n\' {shlex.quote("conf-sshd-mock:" + name)}\n'
|
||||||
|
' exit 0\n'
|
||||||
|
'fi\n' + rest)
|
||||||
path.write_text(text)
|
path.write_text(text)
|
||||||
path.chmod(0o700)
|
path.chmod(0o700)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
def isolation_prelude(self):
|
||||||
|
# BusyBox FEATURE_SH_STANDALONE may run applets before consulting PATH.
|
||||||
|
# Shell functions take precedence over applets, and an absolute mock
|
||||||
|
# path cannot resolve to an applet. Build fresh for every invocation so
|
||||||
|
# test-specific id/stat/cp/cat/dd/mv mocks are intercepted as well.
|
||||||
|
lines = []
|
||||||
|
for path in sorted(self.bin.iterdir()):
|
||||||
|
if re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', path.name):
|
||||||
|
lines.append(f'{path.name}() {{ {shlex.quote(str(path))} "$@"; }}')
|
||||||
|
# Hyphenated command names such as apt-get are not POSIX function
|
||||||
|
# identifiers and have no BusyBox applets; their PATH mocks remain.
|
||||||
|
for name in ('id', 'crontab'):
|
||||||
|
lines.extend([
|
||||||
|
f'if [ "$({name} --conf-sshd-isolation-probe)" != conf-sshd-mock:{name} ]; then',
|
||||||
|
f' printf \'%s\\n\' \'TEST ISOLATION FAILED: {name} mock was bypassed\' >&2',
|
||||||
|
' exit 97',
|
||||||
|
'fi',
|
||||||
|
])
|
||||||
|
return '\n'.join(lines) + '\n'
|
||||||
|
|
||||||
|
def script_command(self, *args, script=None):
|
||||||
|
# Preserve the production script's arguments and $0 while loading it
|
||||||
|
# in a shell whose command resolution has passed the isolation probes.
|
||||||
|
return [*SHELL, '-c', self.isolation_prelude() + '. "$0"',
|
||||||
|
str(script or self.script), *args]
|
||||||
|
|
||||||
def run_script(self, *args, ok=True, env=None, script=None):
|
def run_script(self, *args, ok=True, env=None, script=None):
|
||||||
completed = subprocess.run([*SHELL, str(script or self.script), *args],
|
completed = subprocess.run(self.script_command(*args, script=script),
|
||||||
env=self.env | (env or {}), capture_output=True, text=True,
|
env=self.env | (env or {}), capture_output=True, text=True,
|
||||||
timeout=15)
|
timeout=15)
|
||||||
if ok:
|
if ok:
|
||||||
@@ -126,6 +165,23 @@ fi
|
|||||||
self.assertEqual(SOURCE.count('@@DEFAULT_CRON@@'), 1)
|
self.assertEqual(SOURCE.count('@@DEFAULT_CRON@@'), 1)
|
||||||
subprocess.run([*SHELL, '-n', str(self.script)], check=True)
|
subprocess.run([*SHELL, '-n', str(self.script)], check=True)
|
||||||
|
|
||||||
|
def test_isolation_probe_aborts_before_installer_when_mock_is_bypassed(self):
|
||||||
|
for name in ('id', 'crontab'):
|
||||||
|
with self.subTest(name=name):
|
||||||
|
mock_path = self.bin / name
|
||||||
|
original = mock_path.read_text()
|
||||||
|
try:
|
||||||
|
# Simulate a broken interception without calling real tools.
|
||||||
|
mock_path.write_text('#!/bin/sh\nprintf "wrong-tool\\n"\n')
|
||||||
|
result = self.run_script('-c', ok=False)
|
||||||
|
self.assertEqual(result.returncode, 97)
|
||||||
|
self.assertIn(f'TEST ISOLATION FAILED: {name}', result.stderr)
|
||||||
|
self.assertFalse((self.home / '.conf-sshd').exists())
|
||||||
|
self.assertFalse(self.auth.exists())
|
||||||
|
self.assertEqual(self.calls.read_text(), '')
|
||||||
|
finally:
|
||||||
|
mock_path.write_text(original)
|
||||||
|
|
||||||
def test_default_sync_has_no_system_or_cron_side_effects(self):
|
def test_default_sync_has_no_system_or_cron_side_effects(self):
|
||||||
old_cron = self.cron.read_bytes()
|
old_cron = self.cron.read_bytes()
|
||||||
self.run_script()
|
self.run_script()
|
||||||
@@ -383,7 +439,7 @@ fi
|
|||||||
|
|
||||||
def test_concurrent_run_refuses_and_signal_cleanup(self):
|
def test_concurrent_run_refuses_and_signal_cleanup(self):
|
||||||
env = self.env | {'TEST_CURL_WAIT': '1'}
|
env = self.env | {'TEST_CURL_WAIT': '1'}
|
||||||
first = subprocess.Popen([*SHELL, str(self.script), '-o'], env=env,
|
first = subprocess.Popen(self.script_command('-o'), env=env,
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||||
try:
|
try:
|
||||||
deadline = time.monotonic() + 5
|
deadline = time.monotonic() + 5
|
||||||
@@ -526,7 +582,7 @@ fi
|
|||||||
def test_piped_install_ignores_same_named_unrelated_cwd_file(self):
|
def test_piped_install_ignores_same_named_unrelated_cwd_file(self):
|
||||||
decoy = self.root / 'sh'
|
decoy = self.root / 'sh'
|
||||||
decoy.write_text(SCRIPT + '\n# UNTRUSTED CWD DECOY\n')
|
decoy.write_text(SCRIPT + '\n# UNTRUSTED CWD DECOY\n')
|
||||||
completed = subprocess.run([*SHELL, '-s', '--', '-c'], input=SCRIPT,
|
completed = subprocess.run([*SHELL, '-s', '--', '-c'], input=self.isolation_prelude() + SCRIPT,
|
||||||
cwd=self.root, env=self.env, capture_output=True, text=True, timeout=15)
|
cwd=self.root, env=self.env, capture_output=True, text=True, timeout=15)
|
||||||
self.assertEqual(completed.returncode, 0, completed.stdout + completed.stderr)
|
self.assertEqual(completed.returncode, 0, completed.stdout + completed.stderr)
|
||||||
self.assertNotIn('UNTRUSTED CWD DECOY', self.installed.read_text())
|
self.assertNotIn('UNTRUSTED CWD DECOY', self.installed.read_text())
|
||||||
@@ -617,7 +673,7 @@ fi
|
|||||||
self.run_script('-u', ok=False)
|
self.run_script('-u', ok=False)
|
||||||
|
|
||||||
def test_signal_removes_own_lock_and_temporary_files(self):
|
def test_signal_removes_own_lock_and_temporary_files(self):
|
||||||
first = subprocess.Popen([*SHELL, str(self.script), '-o'],
|
first = subprocess.Popen(self.script_command('-o'),
|
||||||
env=self.env | {'TEST_CURL_WAIT': '1'},
|
env=self.env | {'TEST_CURL_WAIT': '1'},
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
||||||
start_new_session=True)
|
start_new_session=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user