"""Thread-local SSH sessions, mandatory explicit jump, strict known-host keys.""" import json import hashlib import base64 import re import threading from pathlib import Path def private_text(path): p = Path(path) if not p.is_absolute() or p.is_symlink() or not p.is_file() or p.stat().st_mode & 0o077: raise ValueError('Private regular absolute credential/config reference required') return p.read_text() def private_json(path): return json.loads(private_text(path)) def credentials(spec): if not spec.get('credential_ref'):return {} if spec.get('credential_format','json')=='json':return private_json(spec['credential_ref']) if spec['credential_format']=='codocs-password': matches=re.findall(r'(?m)^[ \t]*(?:[-*][ \t]+)?密码[::][ \t]*([^\r\n]+)$',private_text(spec['credential_ref'])) if len(matches)!=1:raise ValueError('Unambiguous existing CoDocs password field required') return {'password':matches[0].strip()} raise ValueError('Unsupported credential reference format') class Pool: def __init__(self, config, jump_id, client_factory=None): self.config, self.jump_id = config, jump_id if jump_id not in config['hosts'] or config['hosts'][jump_id].get('role') != 'white_jump': raise ValueError('Explicit registered white jump required') if client_factory is None: known=Path(config['known_hosts']) if not known.is_absolute() or known.is_symlink() or not known.is_file() or known.stat().st_mode & 0o022:raise ValueError('Explicit trusted known-hosts file required') if any(not re.fullmatch('SHA256:[A-Za-z0-9+/]{43}=?',s.get('host_key_sha256','')) for s in config['hosts'].values()):raise ValueError('Approved explicit host fingerprints required') self.factory = client_factory self.local = threading.local() self.clients = [] self.lock = threading.Lock() def get(self, rid): sessions = getattr(self.local, 'sessions', None) if sessions is None: sessions = self.local.sessions = {} if rid in sessions: c = sessions[rid] if c.get_transport() and c.get_transport().is_active(): return c # Do not transparently retry operations after a lost connection. raise RuntimeError('SSH session lost; probe remote outcome on next run') spec = self.config['hosts'][rid] jump = self.get(self.jump_id) if rid != self.jump_id else None if self.factory: client = self.factory(rid, spec, jump) else: import paramiko client = paramiko.SSHClient() known=Path(self.config['known_hosts']) if not known.is_absolute() or known.is_symlink() or not known.is_file() or known.stat().st_mode & 0o022:raise ValueError('Trusted non-writable known-hosts reference required') entries=paramiko.HostKeys(str(known)) hostname=spec['host'] if spec.get('port',22)==22 else '['+spec['host']+']:'+str(spec['port']) approved=[] for key in (entries.lookup(hostname) or {}).values(): actual='SHA256:'+base64.b64encode(hashlib.sha256(key.asbytes()).digest()).decode().rstrip('=') if actual==spec['host_key_sha256'].rstrip('='):approved.append(key) if not approved:raise ValueError('Approved known-host key absent; no credentials transmitted') for key in approved:client.get_host_keys().add(hostname,key.get_name(),key) client.set_missing_host_key_policy(paramiko.RejectPolicy()) auth = credentials(spec) if set(auth) - {'username', 'password', 'key_filename', 'passphrase'}: raise ValueError('Unexpected SSH credential fields') if auth.get('username',spec['username'])!=spec['username']:raise ValueError('Credential username differs from registered host') if auth.get('key_filename'): key=Path(auth['key_filename']) if not key.is_absolute() or not key.is_file() or key.stat().st_mode & 0o077:raise ValueError('Private absolute SSH key required') sock = jump.get_transport().open_channel('direct-tcpip', (spec['host'], spec.get('port', 22)), ('127.0.0.1', 0)) if jump else None try: client.connect(spec['host'], port=spec.get('port', 22), username=spec['username'], password=auth.get('password'), key_filename=auth.get('key_filename'), passphrase=auth.get('passphrase'), sock=sock, allow_agent=False, look_for_keys=False, timeout=20, banner_timeout=20, auth_timeout=20) except Exception: client.close() raise RuntimeError('SSH connect failed; inspect registered route/host key/credential reference') from None finally: auth.clear() sessions[rid] = client with self.lock: self.clients.append(client) return client def close(self): for client in reversed(self.clients): client.close() self.clients.clear()