"""Captured JSON transport for protected callers; never run against live data to stdout.""" import json import ssl import sys import urllib.error import urllib.parse import urllib.request from mfa_state import NoRedirect, endpoint, unique_object MAX_BODY = 1024 * 1024 def service_endpoint(value, profile='privacyidea'): """Named destinations; profiles never inherit each other's credential scope.""" if profile == 'privacyidea': return endpoint(value) hosts = {'lldap-auth': 'lldap.coulomb.social', 'keycape-discovery': 'kc.coulomb.social'} if profile not in hosts: raise ValueError() if value in ('https://' + hosts[profile], 'https://' + hosts[profile] + ':443'): return value base = endpoint(value) if urllib.parse.urlsplit(base).hostname not in ('127.0.0.1', '::1'): raise ValueError() return base def request(url, payload=None, token=None, *, method=None, profile='privacyidea'): try: parsed = urllib.parse.urlsplit(url) base = service_endpoint(urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, '', '', '')), profile) if parsed.fragment or not parsed.path.startswith('/') or any(ord(c) < 32 for c in url): return 0, None verb = method or ('POST' if payload is not None else 'GET') if profile != 'privacyidea': if token is not None or parsed.query: return 0, None if profile == 'lldap-auth': if (verb != 'POST' or parsed.path != '/auth/simple/login' or not isinstance(payload, dict) or set(payload) != {'username', 'password'} or payload['username'] != 'admin' or not isinstance(payload['password'], str) or not payload['password']): return 0, None elif verb != 'GET' or parsed.path != '/.well-known/openid-configuration' or payload is not None: return 0, None # Origin validated before creating a request containing protected values. headers = {'Accept': 'application/json'} data = None if payload is not None: data = json.dumps(payload).encode('utf-8') if len(data) > MAX_BODY: return 0, None headers['Content-Type'] = 'application/json' if token: if not isinstance(token, str) or len(token) > 16384 or any(ord(c) < 33 or ord(c) > 126 for c in token): return 0, None headers['Authorization'] = token req = urllib.request.Request(base + parsed.path + ('?' + parsed.query if parsed.query else ''), data=data, headers=headers, method=method or ('POST' if payload is not None else 'GET')) context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) context.load_verify_locations(cafile=ssl.get_default_verify_paths().openssl_cafile) opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect(), urllib.request.HTTPSHandler(context=context)) with opener.open(req, timeout=20) as response: status, raw = response.status, response.read(MAX_BODY + 1) if len(raw) > MAX_BODY: return status, None return status, json.loads(raw, object_pairs_hook=unique_object) if raw else None except urllib.error.HTTPError as exc: exc.close() return exc.code, None except Exception: # Callers emit fixed phases, never exception strings or response bodies. return 0, None def main(): try: if len(sys.argv) != 3 or sys.argv[1] not in ('GET', 'POST'): raise ValueError() token = sys.stdin.readline(16386).removesuffix('\n') body = sys.stdin.read(MAX_BODY + 1) if len(body) > MAX_BODY: raise ValueError() status, result = request(sys.argv[2], json.loads(body, object_pairs_hook=unique_object) if body else None, token or None, method=sys.argv[1]) if not 200 <= status < 300 or not isinstance(result, dict): raise ValueError() # Private shell adapter: caller must capture this; it can contain a JWT/config. print(json.dumps(result)) return 0 except Exception: print('CURL_FAILED') return 1 if __name__ == '__main__': raise SystemExit(main())