#!/usr/bin/env python3 """ProblemRanger HTTP probe. Python 3.10+, standard library only.""" import os, time, json, uuid, socket, ssl, ipaddress, http.client from urllib.parse import urlsplit from urllib.request import Request, urlopen BASE = os.environ.get('PROBLEMRANGER_URL', '').rstrip('/') TOKEN = os.environ.get('PROBLEMRANGER_PROBE_TOKEN', '') if not BASE.startswith('https://') or not TOKEN: raise SystemExit('Set PROBLEMRANGER_URL to your HTTPS site origin and PROBLEMRANGER_PROBE_TOKEN.') def api(path, data=None): raw = None if data is None else json.dumps(data).encode() req = Request(BASE + '/api/v1/agent/' + path, data=raw, headers={'Authorization': 'Bearer ' + TOKEN, 'Content-Type': 'application/json'}) with urlopen(req, timeout=30) as res: return json.load(res) class PinnedHTTPS(http.client.HTTPSConnection): def __init__(self, hostname, address, port): super().__init__(hostname, port, timeout=10, context=ssl.create_default_context()) self.address = address def connect(self): self.sock = self._context.wrap_socket(socket.create_connection((self.address, self.port), self.timeout), server_hostname=self.host) class PinnedHTTP(http.client.HTTPConnection): def __init__(self, hostname, address, port): super().__init__(hostname, port, timeout=10) self.address = address def connect(self): self.sock = socket.create_connection((self.address, self.port), self.timeout) def check(job): started = time.monotonic() result = {'id': str(uuid.uuid4()), 'monitor_id': job['id'], 'ok': False, 'status_code': None, 'error': None} conn = None try: u = urlsplit(job['url']) if u.scheme not in ('http', 'https') or u.username or u.password: raise ValueError('Only public HTTP(S) endpoints without credentials are supported.') port = u.port or (443 if u.scheme == 'https' else 80) if port not in (80, 443): raise ValueError('Only ports 80 and 443 are supported.') addresses = sorted({x[4][0] for x in socket.getaddrinfo(u.hostname, port, type=socket.SOCK_STREAM)}) if not addresses or any(not ipaddress.ip_address(a).is_global for a in addresses): raise ValueError('Target must resolve exclusively to public IP addresses.') cls = PinnedHTTPS if u.scheme == 'https' else PinnedHTTP conn = cls(u.hostname, addresses[0], port) path = (u.path or '/') + ('?' + u.query if u.query else '') conn.request('GET', path, headers={'Host': u.netloc, 'User-Agent': 'ProblemRanger-Probe/0.1.0', 'Connection': 'close'}) response = conn.getresponse() result['status_code'] = response.status result['ok'] = response.status == job['expected_status'] if not result['ok']: result['error'] = f"Expected HTTP {job['expected_status']}; received {response.status}." except Exception as exc: result['error'] = str(exc)[:500] finally: if conn: conn.close() result['latency'] = min(120000, round((time.monotonic()-started)*1000)) return result print('ProblemRanger probe started. Stop with Ctrl+C. Tokens and target URLs are not logged.') try: while True: try: jobs = api('jobs').get('jobs', []) for job in jobs: result = check(job) for attempt in range(3): try: api('results', result) break except Exception: if attempt == 2: raise time.sleep(2 ** attempt) if jobs: print(f'Submitted {len(jobs)} checks.') except Exception as exc: print(f'Agent communication failed ({type(exc).__name__}); retrying in 10 seconds.') time.sleep(10) except KeyboardInterrupt: print('\nProbe stopped.')