Files
2026-11-11 14:22:02 +08:00

63 lines
3.4 KiB
Python

"""User-authorized IM deployment transport. Credentials and relayed keys stay in memory."""
import sys,json,getpass,shlex,base64,hashlib
from pathlib import Path
sys.path.insert(0,str(Path(__file__).resolve().parent/'deploy-python'))
import paramiko
hosts=Path(__file__).resolve().parent/'im-deploy-known-hosts'
class PinFirstHost(paramiko.MissingHostKeyPolicy):
def missing_host_key(self,client,hostname,key):
print('SSH_HOST_KEY '+hostname+' SHA256:'+base64.b64encode(hashlib.sha256(key.asbytes()).digest()).decode().rstrip('='),flush=True)
client.get_host_keys().add(hostname,key.get_name(),key);client.save_host_keys(str(hosts))
client=paramiko.SSHClient()
if hosts.exists():client.load_host_keys(str(hosts))
client.set_missing_host_key_policy(PinFirstHost())
password=getpass.getpass('IM SSH password (memory only): ')
client.connect('192.168.110.111',username='ps',password=password,look_for_keys=False,allow_agent=False,timeout=12,auth_timeout=12)
group=None
def execute(c,command,timeout=45,sudo=False):
if sudo:command='sudo -S -p '+shlex.quote('')+' -- sh -c '+shlex.quote(command)
stdin,stdout,stderr=c.exec_command(command,timeout=timeout)
if sudo:stdin.write(password+'\n');stdin.flush()
stdin.channel.shutdown_write()
out=stdout.read();err=stderr.read();return stdout.channel.recv_exit_status(),out,err
print('IM_SSH_READY',flush=True)
for line in sys.stdin:
try:
request=json.loads(line);action=request['action']
if action=='close':break
if action in ('exec','exec_group'):
c=group if action=='exec_group' else client
status,out,err=execute(c,request['command'],request.get('timeout',45),request.get('sudo',False))
result={'exit':status,'out':out.decode('utf-8','replace'),'err':err.decode('utf-8','replace')}
elif action in ('upload','download','upload_group'):
c=group if action=='upload_group' else client
with c.open_sftp() as sftp:
if action in ('upload','upload_group'):sftp.put(request['local'],request['remote'])
else:sftp.get(request['remote'],request['local'])
result={'ok':True}
elif action=='connect_group':
group=paramiko.SSHClient();group.load_host_keys(str(Path(__file__).resolve().parent/'deploy-known-hosts'))
group.set_missing_host_key_policy(paramiko.RejectPolicy())
gp=getpass.getpass('Group SSH password (not stored): ')
group.connect('39.97.232.35',username='root',password=gp,look_for_keys=False,allow_agent=False,timeout=12,auth_timeout=12)
del gp;result={'group_connected':True}
elif action=='relay_key_to_group':
source=request['source'];target=request['target']
assert source.startswith('/opt/') or source.startswith('/www/')
assert target.startswith('/www/wwwroot/x.zhenyangtang.com.cn/shared/') and group is not None
status,key,err=execute(client,'cat -- '+shlex.quote(source),sudo=True)
assert status==0 and len(key)==64 and all(c in b'0123456789abcdefABCDEF' for c in key),'invalid key source'
with group.open_sftp() as sftp:
try:sftp.stat(target);raise RuntimeError('target key already exists')
except FileNotFoundError:pass
with sftp.file(target,'wb') as f:f.write(key)
sftp.chmod(target,0o600)
del key;result={'key_relayed':True}
else:raise ValueError('unsupported action')
print(json.dumps(result,ensure_ascii=False),flush=True)
except Exception as e:
print(json.dumps({'error':type(e).__name__,'message':str(e)},ensure_ascii=False),flush=True)
if group:group.close()
client.close();del password