This commit is contained in:
Mateusz Gruszczyński committed 2026-10-25 17:45:57 +01:00
1 parent bd077754a9
commit ed7e3377c1
40 files changed
+1764 -365

No files matched your search

+124
View File
@@ -0,0 +1,124 @@
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]+)"\)|(?<!fn )admin_vhosts_javascript\(\)', main)
# Regex gets extra empty for vhosts function
for line in parts:
if line:
if line=='web/dashboard-defaults.json': continue
if line=='web/dashboard-widgets.json':
app.append('const ADMIN_DASHBOARD_WIDGET_REGISTRY = Object.freeze('+ (admin/line).read_text()+');')
else: app.append((admin/line).read_text())
else: app.append('(()=>{\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_METRICS_SPLIT -->'),('RP_CHARTS_MAIN','partials/overview-charts.html','<!-- RP_CHARTS_SPLIT -->')]:
raw=(web/rel).read_text(); one,two=raw.split(split)
html=html.replace(f'<!-- {marker} -->',one).replace(f'<!-- {marker.replace("MAIN","WEB")} -->',two)
html=re.sub(r'<script[^>]*src="/assets/app_[^\"]+"[^>]*></script>', '', html)
html=re.sub(r'<link[^>]*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()