import asyncio import json import time from urllib.parse import urlparse import aiohttp TARGETS = [ { 'name': 'Gitea', 'url': 'https://gitea.rainbow-herring.ts.net/api/healthz', 'kind': 'gitea', 'critical': True, }, { 'name': 'Packs Site (Production)', 'url': 'https://jakerasp.rainbow-herring.ts.net/healthz/', 'kind': 'packs', 'critical': True, }, { 'name': 'Packs Site (Development)', 'url': 'https://jakepc.rainbow-herring.ts.net/healthz/', 'kind': 'packs', 'critical': False, }, ] GITEA_VERSION_URL = 'https://gitea.rainbow-herring.ts.net/api/v1/version' DOH_RESOLVERS = { 'Cloudflare': 'https://cloudflare-dns.com/dns-query', 'Google': 'https://dns.google/resolve', } async def _get(session, url, timeout): started = time.monotonic() async with session.get( url, timeout=aiohttp.ClientTimeout(total=timeout), allow_redirects=False, ) as resp: latency_ms = round((time.monotonic() - started) * 1000) body = await resp.text() return resp.status, latency_ms, body async def _resolve_public(session, resolver_url, host, timeout): try: async with session.get( resolver_url, params={'name': host, 'type': 'A'}, headers={'Accept': 'application/dns-json'}, timeout=aiohttp.ClientTimeout(total=timeout), ) as resp: if resp.status != 200: return {'ok': False, 'ips': [], 'error': f'HTTP {resp.status}'} body = await resp.text() data = json.loads(body) except asyncio.TimeoutError: return {'ok': False, 'ips': [], 'error': 'timeout'} except aiohttp.ClientError: return {'ok': False, 'ips': [], 'error': 'error'} except Exception: return {'ok': False, 'ips': [], 'error': 'error'} status = data.get('Status') if status == 3: return {'ok': False, 'ips': [], 'error': 'NXDOMAIN'} if status == 2: return {'ok': False, 'ips': [], 'error': 'SERVFAIL'} if status != 0: return {'ok': False, 'ips': [], 'error': f'status {status}'} answers = [a for a in data.get('Answer', []) if a.get('type') == 1] if not answers: return {'ok': False, 'ips': [], 'error': 'no A record'} return {'ok': True, 'ips': [a['data'] for a in answers], 'error': ''} async def check_target(session, target, timeout): result = { 'name': target['name'], 'kind': target.get('kind', ''), 'url': target['url'], 'critical': target.get('critical', True), 'up': False, 'status_code': None, 'latency_ms': None, 'db': None, 'version': None, 'detail': '', 'dns': {}, } try: status, latency_ms, body = await _get(session, target['url'], timeout) result['status_code'] = status result['latency_ms'] = latency_ms if status == 200: data = {} try: data = json.loads(body) except (json.JSONDecodeError, ValueError): pass result['up'] = data.get('status') == 'pass' if data.get('db') is not None: result['db'] = data['db'] else: result['detail'] = f'HTTP {status}' except asyncio.TimeoutError: result['detail'] = 'timeout' except aiohttp.ClientError: result['detail'] = 'connection error' except Exception as exc: result['detail'] = type(exc).__name__ if result['up'] and result['kind'] == 'gitea': try: status, _, body = await _get(session, GITEA_VERSION_URL, timeout) if status == 200: result['version'] = json.loads(body).get('version') except Exception: pass host = urlparse(target['url']).hostname for label, resolver_url in DOH_RESOLVERS.items(): result['dns'][label] = await _resolve_public(session, resolver_url, host, timeout) return result async def run_checks(session, timeout): results = await asyncio.gather( *(check_target(session, target, timeout) for target in TARGETS) ) return list(results)