from pathlib import Path import json import os import re from playwright.sync_api import sync_playwright ROOT=Path(__file__).resolve().parents[1] admin=ROOT/'src/admin' router=(admin/'router.rs').read_text() main=router.split('fn admin_app_javascript()',1)[1].split('fn admin_vhosts_javascript()',1)[0] web=admin/'web' vhosts=['core.js','protocol.js','health.js','rates.js','editor.js','render.js'] chunk='\n'.join((web/'vhosts'/f).read_text() for f in vhosts) app=['"use strict";', 'const ADMIN_DASHBOARD_DEFAULTS = Object.freeze('+ (web/'dashboard-defaults.json').read_text()+');'] parts=re.findall(r'include_str!\("(web/[^"\n]+)"\)|(?{\n'+chunk+'\n})();') js='\n'.join(app) startup='if (document.readyState === "complete") startAdminPanel();\nelse document.addEventListener("DOMContentLoaded", startAdminPanel, { once: true });' assert startup in js, 'runtime script bootstrap not found' js=js.replace(startup,'// Browser mock harness: startup disabled') # App JS is reconstructed in the same module order as router.rs. html=(web/'index.html').read_text() for marker,rel,split in [('RP_METRICS_MAIN','partials/overview-metrics.html',''),('RP_CHARTS_MAIN','partials/overview-charts.html','')]: raw=(web/rel).read_text(); one,two=raw.split(split) html=html.replace(f'',one).replace(f'',two) html=re.sub(r']*src="/assets/app_[^\"]+"[^>]*>', '', html) html=re.sub(r']*href="/assets/style_[^\"]+"[^>]*>', '', html) html=html.replace('__RUSTPROXY_VERSION__','0.34.13') mock_cfg={ 'global_security': {'user_agent_blocking':{'enabled':True,'patterns':['bad-bot','curl/*']},'country_access':{'mode':'block','countries':['RU']},'acl':None,'method_policy':{'mode':'block','methods':['TRACE']},'content_type_policy':{'enabled':False,'blocked':[]},'rate_limit':{'enabled':False,'requests':100,'window_seconds':60,'burst':20},'security_headers':{'hsts':{'enabled':False,'max_age_seconds':31536000,'include_subdomains':False,'preload':False},'nosniff':True,'frame_options':'deny','referrer_policy':None,'content_security_policy':None,'permissions_policy':None}}, 'geoip_updates':{'enabled':False,'format':'csv_pair','ipv4_url':'','ipv6_url':'','mmdb_url':'','max_bytes':1000000,'interval_seconds':86400}, 'logging': {'capture_user_agent': True, 'capture_country': False,'file':{'enabled':False},'loki':{'enabled':False},'logstash':{'enabled':False},'format':{}}, 'error_log':{'enabled':False,'path':'/var/log/rustproxy/error.log','format':'JSON'}, 'cache':{},'transform':{},'compression':{},'server_header':{},'diagnostic_headers':{}, 'vhosts':[],'redirects':[],'streams':[],'not_found_hosts':[],'host_policies':[],'acl_profiles':[],'listeners':{},'tls':{}, } with sync_playwright() as p: browser=p.chromium.launch(headless=True,executable_path='/usr/bin/chromium',args=['--no-sandbox','--disable-dev-shm-usage']) page=browser.new_page() page.set_default_timeout(4000) errors=[] page.on('pageerror',lambda exc: errors.append(str(exc))) page.set_content(html,wait_until='load') page.evaluate('''() => { window.__calls=[]; window.__mockFetch=async function(path,options={}) { const request={path:typeof path==='string'?path:path.url,method:options.method||'GET',body:options.body?JSON.parse(options.body):null}; __calls.push(request); const reply=JSON.parse(JSON.stringify(envelope||{revision:0,config:{}})); if (request.method==='PUT') { if (request.path==='/api/config/global-security') reply.config.global_security=request.body.value; if (request.path==='/api/config/logging') {reply.config.logging=request.body.value.logging;reply.config.error_log=request.body.value.error_log;} if (request.path==='/api/config/geoip') reply.config.geoip_updates=request.body.value; reply.revision+=1; } if(request.method==='POST' && request.path==='/api/config/geoip/update')return new Response(JSON.stringify({outcome:'current'}),{status:200,headers:{'content-type':'application/json'}}); return new Response(JSON.stringify(reply),{status:200,headers:{'content-type':'application/json'}}); }; window.fetch=window.__mockFetch; }''') try: page.add_script_tag(content=js) except Exception as e: print('INIT ERRORS',errors) raise print('script page errors:', errors[:8]); assert not errors, errors page.evaluate('''(config) => {authenticated=true; csrf='test'; envelope={revision:10,config}; renderConfig=()=>{}; refresh=async()=>{}; renderSettings();}''',mock_cfg) assert not errors,errors print('initial global UA:',page.locator('#global-ua-block-patterns').input_value()) # Global rules save: UA + header untouched page.locator('#global-ua-block-patterns').evaluate('(el)=>{el.value="bad-bot\\nvery-bad";el.dispatchEvent(new Event("input",{bubbles:true}));}') page.locator('#global-security-form').evaluate('(form)=>form.dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}))') page.wait_for_function('window.__calls.length >= 1') result=page.evaluate('window.__calls.at(-1)') print('global rules save',result['path'],result['body']['value']['user_agent_blocking'],result['body']['value']['security_headers']['frame_options']) assert result['path']=='/api/config/global-security' assert result['body']['value']['user_agent_blocking']['patterns']==['bad-bot','very-bad'] assert result['body']['value']['security_headers']['frame_options']=='deny' page.wait_for_timeout(200) # Global headers save: UA untouched, changed nosniff page.locator('#global-nosniff').evaluate('(el)=>{el.checked=false;el.dispatchEvent(new Event("change",{bubbles:true}));}') page.locator('#global-security-headers-form').evaluate('(form)=>form.dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}))') page.wait_for_function('window.__calls.length >= 2') result=page.evaluate('window.__calls.at(-1)') print('global headers save',result['path'],result['body']['value']['user_agent_blocking'],result['body']['value']['security_headers']['nosniff']) assert result['body']['value']['user_agent_blocking']['patterns']==['bad-bot','very-bad'] assert result['body']['value']['security_headers']['nosniff']==False page.wait_for_timeout(200) # GeoIP triggered as action not section save page.locator('#geoip-update-now').evaluate('(el)=>el.click()') page.wait_for_function('window.__calls.some(x=>x.method==="POST" && x.path==="/api/config/geoip/update")') result=page.evaluate('window.__calls.at(-1)') print('geoip update',result['method'],result['path']) assert result['path']=='/api/config/geoip/update' # Error-log subsection must save logging and error_log together. page.evaluate('renderSettings()') page.locator('#error-log-enabled').evaluate('(el)=>{el.checked=true;el.dispatchEvent(new Event("change",{bubbles:true}));}') page.locator('#error-log-form').evaluate('(form)=>form.dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}))') page.wait_for_function('window.__calls.some(x=>x.path==="/api/config/logging" && x.method==="PUT")') result=page.evaluate('window.__calls.at(-1)') print('logging group save',result['path'],list(result['body']['value'])) assert result['path']=='/api/config/logging' assert set(result['body']['value'])=={'logging','error_log'} assert result['body']['value']['error_log']['enabled']==True assert result['body']['value']['logging']['capture_user_agent']==True page.wait_for_timeout(120) # GeoIP settings are saved through its section, while update remains POST. page.evaluate('renderSettings()') page.locator('#geoip-interval-hours').evaluate('(el)=>{el.value="48";el.dispatchEvent(new Event("input",{bubbles:true}));}') page.locator('#geoip-form').evaluate('(form)=>form.dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}))') page.wait_for_function('window.__calls.some(x=>x.path==="/api/config/geoip" && x.method==="PUT")') result=page.evaluate('window.__calls.at(-1)') print('geoip section save',result['path'],result['body']['value']['interval_seconds']) assert result['path']=='/api/config/geoip' assert result['body']['value']['interval_seconds']==48*3600 assert result['body']['expected_revision']==13 print('browser final errors',errors[:10]); assert not errors,errors browser.close()