Advance standalone admission, finite artillery and native validation
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare reviewable private dedicated-server inputs; never launch or download."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
|
||||
from source_manifest import addon_manifest
|
||||
|
||||
APP_ID = '1874900'
|
||||
HEADERS = {'core': 'Missions/ERR_Everon_Base.conf', 'chat': 'Missions/ERR_Everon.conf'}
|
||||
SYSTEMS = '{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf'
|
||||
WORLD = '{B6C516F86302C679}Worlds/ERR_Everon.ent'
|
||||
|
||||
|
||||
def digest(path):
|
||||
value = hashlib.sha256()
|
||||
with path.open('rb') as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
|
||||
value.update(chunk)
|
||||
return value.hexdigest()
|
||||
|
||||
|
||||
def selected_scenario(source, project):
|
||||
addon = source / 'addons' / project
|
||||
text = (addon / 'addon.gproj').read_text()
|
||||
guid = re.search(r'\bGUID "([A-F0-9]{16})"', text)
|
||||
if not guid:
|
||||
raise ValueError('Selected addon has no exact GUID')
|
||||
relative = HEADERS[project]
|
||||
header = addon / relative
|
||||
meta = header.with_suffix('.conf.meta').read_text()
|
||||
resource = re.search(r'\bName "(\{[A-F0-9]{16}\}[^"\n]+)"', meta)
|
||||
if not resource or resource[1][18:] != relative:
|
||||
raise ValueError('Selected header resource identity/path mismatch')
|
||||
data = header.read_text()
|
||||
for key, expected in [('World', WORLD), ('SystemsConfig', SYSTEMS)]:
|
||||
match = re.search(r'\b' + key + r' "([^"\n]+)"', data)
|
||||
if not match or match[1] != expected:
|
||||
raise ValueError('Selected header has unexpected ' + key)
|
||||
return {'project': project, 'addon_guid': guid[1], 'header': resource[1],
|
||||
'world': WORLD, 'systems_config': SYSTEMS,
|
||||
'addon_directory': str(source / 'addons')}
|
||||
|
||||
|
||||
def inspect_runtime(runtime):
|
||||
binary = runtime / 'server' / 'ArmaReforgerServer'
|
||||
manifest = runtime / 'server' / 'steamapps' / ('appmanifest_' + APP_ID + '.acf')
|
||||
result = {'runtime_directory': str(runtime), 'binary': str(binary),
|
||||
'app_id': APP_ID, 'platform': 'linux', 'branch': 'public',
|
||||
'binary_present': binary.is_file(), 'binary_elf_x86_64': False,
|
||||
'binary_executable': False, 'steam_build_id': None,
|
||||
'install_complete': False, 'engine_version': None,
|
||||
'native_runtime_executed': False}
|
||||
if manifest.is_file():
|
||||
values = dict(re.findall(r'"(appid|buildid|StateFlags)"\s+"([^"]*)"', manifest.read_text()))
|
||||
if values.get('appid') != APP_ID:
|
||||
raise ValueError('Runtime manifest is not the stable dedicated-server app')
|
||||
result.update(steam_build_id=values.get('buildid'),
|
||||
install_complete=values.get('StateFlags') == '4',
|
||||
manifest_sha256=digest(manifest))
|
||||
if binary.is_file():
|
||||
if binary.is_symlink():
|
||||
raise ValueError('Private runtime binary must not be a symlink to another installation')
|
||||
with binary.open('rb') as stream:
|
||||
header = stream.read(20)
|
||||
result['binary_elf_x86_64'] = len(header) == 20 and header[:6] == b'\x7fELF\x02\x01' and int.from_bytes(header[18:20], 'little') == 62
|
||||
result['binary_executable'] = os.access(binary, os.X_OK)
|
||||
result['binary_sha256'] = digest(binary)
|
||||
return result
|
||||
|
||||
|
||||
def build_plan(source, runtime, project='core', port=2001):
|
||||
source, runtime = source.resolve(), runtime.resolve()
|
||||
if not 1024 <= port <= 65535:
|
||||
raise ValueError('Use an unprivileged port in 1024..65535')
|
||||
if source == runtime or source in runtime.parents or runtime in source.parents:
|
||||
raise ValueError('Runtime must be separate from the authored source tree')
|
||||
manifest = addon_manifest(source)
|
||||
scenario = selected_scenario(source, project)
|
||||
installation = inspect_runtime(runtime)
|
||||
config = {
|
||||
'bindAddress': '127.0.0.1', 'bindPort': port,
|
||||
'publicAddress': '127.0.0.1', 'publicPort': port,
|
||||
'game': {'name': 'Rereintroduced private validation', 'scenarioId': scenario['header'],
|
||||
'maxPlayers': 2, 'visible': False,
|
||||
'gameProperties': {'fastValidation': True, 'battlEye': True}, 'mods': []},
|
||||
'operating': {'disableCrashReporter': True},
|
||||
}
|
||||
blockers = []
|
||||
if not (installation['install_complete'] and installation['binary_elf_x86_64'] and installation['binary_executable']):
|
||||
blockers.append('Stable native Linux dedicated-server installation is missing or incomplete.')
|
||||
blockers.extend([
|
||||
'Server binary dependencies and exact engine-version compatibility have not been validated by a native run.',
|
||||
'Unpublished local-addon loading with authenticated server configuration/header selection needs native verification; do not combine -config and -addons.',
|
||||
'Loopback server configuration must be reviewed before any listener is started.',
|
||||
'Two genuine accepted native client identities/controllers are not available as evidence; copied profiles and synthetic actors do not qualify.',
|
||||
'Existing ordinary runner refuses concurrent game instances; it is not a multi-client supervisor.',
|
||||
])
|
||||
result = {'schema': 1, 'scope': 'no-launch dedicated preparation only', 'launch_allowed': False,
|
||||
'listener_started': False, 'genuine_clients_verified': 0, 'multiplayer_passed': False,
|
||||
'source_directory': str(source), 'scenario': scenario, 'runtime': installation,
|
||||
'network': {'bind': '127.0.0.1', 'port': port, 'public_listing': False,
|
||||
'rcon_configured': False, 'a2s_configured': False},
|
||||
'source_manifest_sha256': hashlib.sha256((json.dumps(manifest, indent=2, sort_keys=True) + '\n').encode()).hexdigest(),
|
||||
'disk_free_bytes': shutil.disk_usage(runtime if runtime.exists() else runtime.parent).free,
|
||||
'blockers': blockers,
|
||||
'evidence_required': [
|
||||
'Server and both clients: exact version, role, source/addon manifest and complete sealed raw logs.',
|
||||
'Server: native dedicated mode, selected header and actual MissionSystems config, two distinct accepted connections/current controlled prisoners.',
|
||||
'Client1 and Client2: separate native ownership/local controller and actual replicated state; record no account credentials.',
|
||||
'Both clients: JIP, same-account reconnect/reservation expiry, death/unconsciousness, spectator and extraction lifecycle.',
|
||||
'No synthetic-client substitution and no multiplayer/FPS claim from headless startup alone.',
|
||||
]}
|
||||
return result, config, manifest
|
||||
|
||||
|
||||
def write_plan(destination, result, config, manifest):
|
||||
destination = destination.absolute()
|
||||
runtime = Path(result['runtime']['runtime_directory'])
|
||||
if destination.is_symlink() or destination.exists():
|
||||
raise ValueError('Plan directory must be new; existing evidence is never overwritten')
|
||||
if not destination.parent.is_dir() or destination.parent.resolve() != runtime:
|
||||
raise ValueError('Plan directory must be a new direct child of the private runtime')
|
||||
marker = runtime / 'runtime-origin.json'
|
||||
if not marker.is_file() or marker.is_symlink():
|
||||
raise ValueError('Private runtime origin marker is required before writing plans')
|
||||
origin = json.loads(marker.read_text())
|
||||
if origin.get('app_id') != int(APP_ID) or origin.get('purpose') != 'private anonymous Reforger dedicated server preparation; no server launched':
|
||||
raise ValueError('Runtime origin marker is not the authorized private preparation')
|
||||
destination.mkdir(mode=0o700)
|
||||
for filename, value in [('preflight.json', result), ('server-config.review.json', config), ('source-manifest.json', manifest)]:
|
||||
(destination / filename).write_text(json.dumps(value, indent=2, sort_keys=True) + '\n')
|
||||
# A human-readable plan is deliberately not an executable launch script.
|
||||
(destination / 'REVIEW.txt').write_text('NO SERVER STARTED. DO NOT LAUNCH BEFORE CONFIGURATION REVIEW.\n'
|
||||
'server-config.review.json binds loopback and preserves native fast validation/BattlEye.\n'
|
||||
'Local unpublished-addon/config compatibility, engine dependencies/version and two genuine clients remain unverified.\n'
|
||||
'Do not add -addons to a -config command or treat this plan as runtime/MP evidence.\n')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--source', type=Path, required=True)
|
||||
parser.add_argument('--runtime', type=Path, default=Path('/tmp/erri-dedicated-runtime'))
|
||||
parser.add_argument('--project', choices=HEADERS, default='core')
|
||||
parser.add_argument('--port', type=int, default=2001)
|
||||
parser.add_argument('--write-plan', type=Path, help='Write review files into a NEW direct child of the private runtime; never launch')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
result, config, manifest = build_plan(args.source, args.runtime, args.project, args.port)
|
||||
if args.write_plan:
|
||||
write_plan(args.write_plan, result, config, manifest)
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
return 0 # success means inspection completed, never launch/MP qualification
|
||||
except (OSError, ValueError) as error:
|
||||
parser.error(str(error))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user