"""Read-only public GET checks; no cookies, redirects, writes or private APIs.""" import argparse from datetime import datetime, timezone import hashlib import json from pathlib import Path import platform import urllib.error import urllib.request SERVICES=[('gamepunch','/api/v1/gamepunch/home','/gamepunch/api/v1/gamepunch/home','sections'),('coinpunch','/public/coinpunch/data/home-overview.json','/coinpunch/data/home-overview.json','global'),('punch-livescore','/public/punch-livescore/api/live-scores','/punch-livescore/api/live-scores','response'),('punch-re','/public/punch-re/api/regions?type=sale','/punch-re/api/regions?type=sale','regions')] class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self,*args,**kwargs):return None def run(): checks=[];opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect()) jobs=[] for app,old,new,key in SERVICES: jobs.extend([(app,'old_data','https://gamepunch.org'+old,key),(app,'new_data','https://punchblog.com'+new,key),(app,'old_html','https://gamepunch.org/public/'+app,None)]) for app,kind,url,key in jobs: try: request=urllib.request.Request(url,headers={'User-Agent':'PunchblogPublicContractCheck/1.0','Accept':'application/json' if key else 'text/html'}) try: response=opener.open(request,timeout=25) except urllib.error.HTTPError as error: response=error with response: raw=response.read(4*1024*1024+1) if len(raw)>4*1024*1024:raise ValueError('Response size limit exceeded') value=json.loads(raw) if key and response.status==200 else None matches=isinstance(value,dict) and isinstance(value.get(key),list if key in ['sections','response','regions'] else dict) if key else response.status==410 checks.append({'app':app,'kind':kind,'url':url,'status':response.status,'type':response.headers.get('Content-Type'),'robots':response.headers.get('X-Robots-Tag'),'contract_matches':matches,'body_sha256':hashlib.sha256(raw).hexdigest(),'bytes':len(raw)}) except Exception as error:checks.append({'app':app,'kind':kind,'url':url,'error':type(error).__name__}) rows=[[r['app'],r['kind'],r.get('status','요청 실패'),str(r.get('contract_matches',False)),r.get('robots','')] for r in checks] return {'observed_at':datetime.now(timezone.utc).isoformat(),'environment':{'os':platform.system(),'python':platform.python_version(),'network':'12 public HTTPS GET requests, TLS verified'},'method':'Check each installed-client data URL, moved data URL and retired HTML root separately. Do not follow redirects. Publish response hashes and limited headers, never bodies, cookies or credentials.','limits':'Current response snapshot, not an uptime or device test. A matching outer field does not validate every nested value or all app screens. Historical before-states cannot be regenerated by this script.','checks':checks,'tables':{'routes':{'headers':['서비스','요청 종류','HTTP','기대 형식 일치','검색 제외 헤더'],'rows':rows}}} if __name__=='__main__': parser=argparse.ArgumentParser();parser.add_argument('--output',required=True,type=Path);args=parser.parse_args() with args.output.open('x',encoding='utf-8') as stream:json.dump(run(),stream,ensure_ascii=False,indent=2);stream.write('\n')