# -*- coding: utf-8 -*-
"""只操作本程序创建的临时对象；全部数据为合成样本。"""
import os, sys, json, sqlite3, tempfile, pathlib, subprocess, time, socket, stat
from pathlib import Path
def emit(**kw):
    print(json.dumps(kw, ensure_ascii=False, sort_keys=True))

import shutil, threading, urllib.request, urllib.error, http.server, socketserver, contextlib
NGINX=os.environ.get('NGINX_BIN') or shutil.which('nginx')
assert NGINX, '需真实Nginx；建议用官方1.28.0独立编译并设置NGINX_BIN'
def freeport():
    s=socket.socket(); s.bind(('127.0.0.1',0)); p=s.getsockname()[1]; s.close(); return p
class Threaded(socketserver.ThreadingMixIn,http.server.HTTPServer):
    daemon_threads=True
def backend(handler,host='127.0.0.1',port=0):
    class H(http.server.BaseHTTPRequestHandler):
        protocol_version='HTTP/1.1'
        def log_message(self,*a): pass
        def do_GET(self):
            status,headers,body=handler(self.path)
            self.send_response(status)
            for k,v in headers.items(): self.send_header(k,v)
            self.send_header('Content-Length',str(len(body))); self.end_headers()
            try: self.wfile.write(body)
            except (BrokenPipeError,ConnectionResetError): pass
    s=Threaded((host,port),H); threading.Thread(target=s.serve_forever,daemon=True).start(); return s
def request(port,path='/',host='demo.test'):
    q=urllib.request.Request('http://127.0.0.1:'+str(port)+path,headers={'Host':host,'Connection':'close'})
    opener=urllib.request.build_opener(urllib.request.ProxyHandler({}))
    try: r=opener.open(q,timeout=5)
    except urllib.error.HTTPError as e: r=e
    with r: return {'status':r.code,'headers':dict(r.headers),'body':r.read().decode()}
@contextlib.contextmanager
def nginx(http_config):
    assert os.name=='posix', '本实验按Linux独立prefix设计'
    with tempfile.TemporaryDirectory(prefix='ywebs-next132-b2-nginx-') as d:
        p=Path(d); assert p.name.startswith('ywebs-next132-b2-nginx-')
        def render(body):
            text='worker_processes 1; daemon off; master_process on; pid '+d+'/nginx.pid; error_log '+d+'/error.log notice; events {worker_connections 128;} http {access_log off; client_body_temp_path '+d+'/body; proxy_temp_path '+d+'/proxy; '+body+'}'
            for forbidden in ['/var/www','/opt/ywebs','/usr/local/nginx/conf']: assert forbidden not in text
            (p/'nginx.conf').write_text(text,encoding='utf-8')
        render(http_config)
        def command(*a): return subprocess.run([NGINX,'-p',d+'/','-c',str(p/'nginx.conf')]+list(a),stdout=subprocess.PIPE,stderr=subprocess.STDOUT,universal_newlines=True)
        tested=command('-t'); assert tested.returncode==0,tested.stdout
        proc=subprocess.Popen([NGINX,'-p',d+'/','-c',str(p/'nginx.conf')],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
        time.sleep(.3); assert proc.poll() is None,(p/'error.log').read_text()
        try: yield p,proc,render,command
        finally:
            if proc.poll() is None: proc.terminate()
            try: proc.wait(timeout=5)
            except subprocess.TimeoutExpired: proc.kill(); proc.wait()
import struct
version=subprocess.check_output([NGINX,'-v'],stderr=subprocess.STDOUT,universal_newlines=True).strip(); assert '1.28.0' in version,version
dns=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); dns.bind(('127.0.0.1',0)); dns.settimeout(.2); state={'ip':'127.0.0.2','queries':0,'stop':False}
def serve_dns():
    while not state['stop']:
        try: data,addr=dns.recvfrom(2048)
        except socket.timeout: continue
        state['queries']+=1; idx=12
        while data[idx]!=0: idx+=data[idx]+1
        end=idx+5; question=data[12:end]; qtype=struct.unpack('!H',data[idx+1:idx+3])[0]
        answer=b''
        if qtype==1: answer=b'\xc0\x0c'+struct.pack('!HHIH',1,1,1,4)+socket.inet_aton(state['ip'])
        header=data[:2]+struct.pack('!HHHHH',0x8180,1,1 if answer else 0,0,0); dns.sendto(header+question+answer,addr)
threading.Thread(target=serve_dns,daemon=True).start(); bp=freeport(); port=freeport()
a=backend(lambda p:(200,{},b'A'),'127.0.0.2',bp); b=backend(lambda p:(200,{},b'B'),'127.0.0.3',bp)
conf='resolver 127.0.0.1:'+str(dns.getsockname()[1])+' valid=1s ipv6=off; upstream fixed {server 127.0.0.2:'+str(bp)+';} upstream changing {zone changing 64k; server demo.example.test:'+str(bp)+' resolve;} server {listen 127.0.0.1:'+str(port)+'; location /dynamic {proxy_pass http://changing;} location /fixed {proxy_pass http://fixed;}}'
try:
    with nginx(conf):
        first=request(port,'/dynamic')['body']; fixed_before=request(port,'/fixed')['body']; state['ip']='127.0.0.3'; start=time.monotonic(); seen=[]
        while time.monotonic()-start<8:
            time.sleep(.3); marker=request(port,'/dynamic')['body']; seen.append(marker)
            if marker=='B': break
        fixed_after=request(port,'/fixed')['body']
    assert first=='A' and 'B' in seen and fixed_before==fixed_after=='A'
    emit(version=version,dynamic_first=first,dynamic_after=seen[-1],fixed_after=fixed_after,dns_queries=state['queries'],observations=seen,scope='固定地址组对照动态域名组；固定组模拟已解析地址，未修改系统DNS')
finally:
    state['stop']=True; a.shutdown(); b.shutdown(); a.server_close(); b.server_close(); dns.close()
