77 lines
3.6 KiB
Python
Executable File
77 lines
3.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Fixed local SSH -> VPS-001 -> VPS-003 transport. No local HTTP or direct fallback."""
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
OPERATIONS = ('catalog', 'source', 'prepare', 'review', 'publish', 'status')
|
|
REMOTE_COMMAND = 'cd /www/wwwroot/clashfreenode-new.com && exec runuser -u nginx -- php artisan editorial:release'
|
|
|
|
|
|
def ssh_command():
|
|
options = ['-F', '/dev/null', '-T', '-o', 'StrictHostKeyChecking=yes',
|
|
'-o', 'PreferredAuthentications=password', '-o', 'PubkeyAuthentication=no',
|
|
'-o', 'NumberOfPasswordPrompts=1', '-o', 'ConnectTimeout=15',
|
|
'-o', 'ServerAliveInterval=15', '-o', 'ServerAliveCountMax=3',
|
|
'-o', 'ControlMaster=no', '-o', 'ControlPath=none',
|
|
'-o', 'ClearAllForwardings=yes', '-o', 'PermitLocalCommand=no']
|
|
jump = shlex.join(['ssh', *options, '-W', '45.153.247.44:22', 'root@104.168.30.40'])
|
|
return ['ssh', *options, '-o', 'ProxyCommand=' + jump, 'root@45.153.247.44', REMOTE_COMMAND]
|
|
|
|
|
|
def atomic_json(path, data):
|
|
path = Path(path)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
fd, temp = tempfile.mkstemp(prefix=path.name + '.', dir=path.parent)
|
|
try:
|
|
with os.fdopen(fd, 'w') as f:
|
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
f.flush()
|
|
os.fsync(f.fileno())
|
|
os.replace(temp, path)
|
|
finally:
|
|
if os.path.exists(temp):
|
|
os.unlink(temp)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('operation', choices=OPERATIONS)
|
|
parser.add_argument('--input', type=Path, help='JSON request, without operation; stdin if omitted')
|
|
parser.add_argument('--output', type=Path, required=True, help='Private JSON result; review includes HTML, never load its online assets locally')
|
|
args = parser.parse_args()
|
|
raw = args.input.read_bytes() if args.input else sys.stdin.buffer.read(48 * 1024 * 1024 + 1)
|
|
if len(raw) > 48 * 1024 * 1024:
|
|
parser.error('Input exceeds 48 MiB')
|
|
payload = json.loads(raw or b'{}')
|
|
if not isinstance(payload, dict) or ('operation' in payload and payload['operation'] != args.operation):
|
|
parser.error('Invalid operation payload')
|
|
payload['operation'] = args.operation
|
|
helper = Path(__file__).with_name('ssh_askpass.py').resolve()
|
|
env = os.environ.copy()
|
|
env.update(SSH_ASKPASS=str(helper), SSH_ASKPASS_REQUIRE='force', DISPLAY=':0')
|
|
try:
|
|
result = subprocess.run(ssh_command(), input=json.dumps(payload, ensure_ascii=False).encode(),
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, timeout=660)
|
|
try:
|
|
reply = json.loads(result.stdout)
|
|
except (ValueError, UnicodeError):
|
|
reply = {'ok': False, 'error': 'SSH/server response unavailable. Query status with the original release_id; no direct fallback.',
|
|
'transport_exit': result.returncode}
|
|
atomic_json(args.output, reply)
|
|
print(json.dumps({'ok': reply.get('ok', False), 'phase': reply.get('data', {}).get('phase'),
|
|
'result_file': str(args.output.resolve()), 'transport_exit': result.returncode}))
|
|
return result.returncode if result.returncode in (0, 1, 2) else 1
|
|
except subprocess.TimeoutExpired:
|
|
atomic_json(args.output, {'ok': False, 'error': 'Transport timed out; publication may have completed. Query original release_id.'})
|
|
return 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|