mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-06 20:43:46 +08:00
154 lines
6.5 KiB
Python
Executable File
154 lines
6.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Stage the pinned ARM64 browser under /data; never install packages into AGNOS."""
|
|
|
|
import fcntl
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import platform
|
|
import pwd
|
|
import shutil
|
|
import subprocess
|
|
import tarfile
|
|
import tempfile
|
|
import urllib.request
|
|
|
|
ROOT = Path('/data/arabesque-firefox')
|
|
BROWSER_UID = 61234
|
|
|
|
|
|
def report(message):
|
|
print(message, flush=True)
|
|
|
|
|
|
def verify(path, package):
|
|
algorithm = 'sha512' if 'sha512' in package else 'sha256'
|
|
with path.open('rb') as stream:
|
|
digest = hashlib.file_digest(stream, algorithm).hexdigest()
|
|
if path.stat().st_size != package['size'] or digest != package[algorithm]:
|
|
raise ValueError(f'Package checksum mismatch: {package["name"]}')
|
|
|
|
|
|
def download(package, cache):
|
|
path = cache / package['name']
|
|
if not path.exists():
|
|
partial = path.with_suffix('.partial')
|
|
try:
|
|
with urllib.request.urlopen(package['url'], timeout=30) as response, partial.open('wb') as target:
|
|
if not response.url.startswith('https://'):
|
|
raise ValueError('Refusing an insecure browser package redirect')
|
|
remaining = package['size']
|
|
while block := response.read(min(1024 * 1024, remaining + 1)):
|
|
remaining -= len(block)
|
|
if remaining < 0:
|
|
raise ValueError('Browser package exceeds pinned size')
|
|
target.write(block)
|
|
label = 'Firefox' if package['name'].startswith('firefox-') else 'browser libraries'
|
|
report(f'Downloading {label}… {100 * (package["size"] - remaining) // package["size"]}%')
|
|
verify(partial, package)
|
|
partial.rename(path)
|
|
finally:
|
|
partial.unlink(missing_ok=True)
|
|
verify(path, package)
|
|
return path
|
|
|
|
|
|
def unpack(path, destination):
|
|
if path.suffix == '.deb':
|
|
subprocess.run(['/usr/bin/dpkg-deb', '--extract', str(path), str(destination)], check=True, timeout=30)
|
|
else:
|
|
with tarfile.open(path) as archive:
|
|
archive.extractall(destination, filter='data')
|
|
|
|
|
|
def prepare():
|
|
manifest = json.loads(Path(__file__).with_name('runtime.json').read_text())
|
|
if os.geteuid() != 0 or platform.machine() != 'aarch64' or Path('/VERSION').read_text().strip() != manifest['agnos']:
|
|
raise RuntimeError('Prepare this runtime as root on ARM64 AGNOS 19.8.1')
|
|
for path in (ROOT, ROOT / 'runtime', ROOT / 'state', ROOT / 'packages'):
|
|
if path.is_symlink():
|
|
raise RuntimeError(f'Refusing symlink: {path}')
|
|
if ROOT.exists() and (ROOT.stat().st_uid != 0 or ROOT.stat().st_mode & 0o022):
|
|
raise RuntimeError('Unsafe browser directory ownership')
|
|
ROOT.mkdir(mode=0o755, exist_ok=True)
|
|
with (ROOT / '.prepare.lock').open('a') as lock:
|
|
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
prepare_locked(manifest)
|
|
|
|
|
|
def prepare_locked(manifest):
|
|
cache = ROOT / 'packages'
|
|
if (ROOT / 'runtime').exists():
|
|
raise RuntimeError('Runtime already exists; stop and review an update separately. The profile is never replaced.')
|
|
try:
|
|
pwd.getpwuid(BROWSER_UID)
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
raise RuntimeError(f'UID {BROWSER_UID} is already assigned')
|
|
for entry in Path('/proc').iterdir():
|
|
if entry.name.isdigit():
|
|
try:
|
|
if entry.stat().st_uid == BROWSER_UID:
|
|
raise RuntimeError(f'UID {BROWSER_UID} already has running processes')
|
|
except FileNotFoundError:
|
|
pass
|
|
if shutil.disk_usage('/data').free < 6 * 1024**3:
|
|
raise RuntimeError('At least 6 GiB free on /data is required to prepare Firefox')
|
|
active = subprocess.check_output(['systemctl', '--no-pager', '--plain', '--no-legend', 'list-units', 'arabesque-firefox-*'], text=True)
|
|
own_unit = next((Path(line[3:]).name for line in Path('/proc/self/cgroup').read_text().splitlines() if line.startswith('0::')), '')
|
|
if any(line.split()[0] != own_unit for line in active.splitlines() if line.split()):
|
|
raise RuntimeError('Close all Firefox sessions before preparing the runtime')
|
|
# A hard stop can interrupt extraction; only this root-owned staging area is disposable.
|
|
for path in ROOT.glob('.prepare-*'):
|
|
if path.is_symlink():
|
|
path.unlink()
|
|
elif path.is_dir():
|
|
shutil.rmtree(path)
|
|
state = ROOT / 'state'
|
|
if state.exists() and (state.stat().st_uid != BROWSER_UID or state.stat().st_mode & 0o077):
|
|
raise RuntimeError('Unexpected browser state owner or permissions')
|
|
if not state.exists():
|
|
state.mkdir(mode=0o700)
|
|
os.chown(state, BROWSER_UID, BROWSER_UID)
|
|
cache.mkdir(mode=0o700, exist_ok=True)
|
|
if cache.stat().st_uid != 0 or cache.stat().st_mode & 0o077:
|
|
raise RuntimeError('The browser package cache must be owned by root with mode 0700')
|
|
with tempfile.TemporaryDirectory(prefix='.prepare-', dir=ROOT) as temporary:
|
|
staging = Path(temporary) / 'runtime'
|
|
staging.mkdir()
|
|
for index, package in enumerate(manifest['packages'], 1):
|
|
report(f'Preparing Firefox ({index}/{len(manifest["packages"])})…')
|
|
archive = download(package, cache)
|
|
if archive.is_symlink() or archive.stat().st_uid != 0 or archive.stat().st_mode & 0o022:
|
|
raise RuntimeError('Unsafe browser package cache file')
|
|
report('Unpacking Firefox…' if package['name'].startswith('firefox-') else 'Preparing browser libraries…')
|
|
unpack(archive, staging)
|
|
schemas = staging / 'usr/share/glib-2.0/schemas'
|
|
for source in Path('/usr/share/glib-2.0/schemas').glob('*.xml'):
|
|
if not (schemas / source.name).exists():
|
|
shutil.copyfile(source, schemas / source.name)
|
|
subprocess.run(['/usr/bin/glib-compile-schemas', '--strict', str(schemas)], check=True, timeout=10)
|
|
distribution = staging / 'firefox/distribution'
|
|
distribution.mkdir(exist_ok=True)
|
|
(distribution / 'policies.json').write_text(json.dumps({'policies': {
|
|
'DisableAppUpdate': True, 'DontCheckDefaultBrowser': True, 'DisableTelemetry': True, 'DisableFirefoxStudies': True,
|
|
}}))
|
|
for name in ('empty', 'devices', 'devices/shm'):
|
|
(staging / 'isolation' / name).mkdir(parents=True, exist_ok=True)
|
|
for name in ('null', 'zero', 'random', 'urandom'):
|
|
(staging / 'isolation/devices' / name).touch()
|
|
(staging / 'ready.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
|
for path in (staging, *staging.rglob('*')):
|
|
if not path.is_symlink():
|
|
os.chown(path, 0, 0)
|
|
path.chmod(0o755 if path.is_dir() or path.stat().st_mode & 0o111 else 0o644)
|
|
(ROOT / '.isolation-probe').write_text('This file must be hidden from the Firefox worker.\n')
|
|
staging.rename(ROOT / 'runtime')
|
|
report('Firefox is prepared. Starting the browser…')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
prepare()
|