import asyncio import json import time from urllib.parse import urlparse import aiohttp from mcstatus import JavaServer 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': 'Minecraft Server', 'host': 'jakelaptop.rainbow-herring.ts.net', 'port': 10000, 'kind': 'minecraft', 'critical': True, }, ] 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_minecraft(session, target, timeout): host, port = target['host'], target['port'] result = { 'name': target['name'], 'kind': 'minecraft', 'url': f'{host}:{port}', 'critical': target.get('critical', True), 'up': False, 'status_code': None, 'latency_ms': None, 'db': None, 'version': None, 'detail': '', 'dns': {}, 'players': None, 'motd': None, } try: server = JavaServer(host, port, timeout=timeout) status = await server.async_status() except asyncio.TimeoutError: result['detail'] = 'timeout' return result except OSError: result['detail'] = 'connection error' return result except Exception as exc: result['detail'] = type(exc).__name__ return result result['up'] = True result['latency_ms'] = round(status.latency) result['players'] = f"{status.players.online}/{status.players.max}" if status.version and status.version.name: result['version'] = status.version.name try: result['motd'] = status.motd.to_plain() except Exception: pass return result async def check_target(session, target, timeout): if target.get('kind') == 'minecraft': result = await check_minecraft(session, target, timeout) else: 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 url = target.get('url') or f'//{target["host"]}' host = urlparse(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)