Initialize Clash Web source and deployment documentation
This commit is contained in:
commit
8e0fa00a73
66 files changed
+8532
No files matched your search
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only VM diagnostics. Never prints subscription URLs or node credentials."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--host', required=True, help='Hostname of a saved subscription')
|
||||
parser.add_argument('--data-dir', default='/var/lib/clashweb')
|
||||
parser.add_argument('--proxy', help='Override the saved VM Mihomo HTTP proxy address')
|
||||
args = parser.parse_args()
|
||||
if os.geteuid() != 0:
|
||||
raise SystemExit('Run with sudo to read the protected VM database; all network tests run as user clashweb.')
|
||||
database = (Path(args.data_dir) / 'clashweb.sqlite').resolve()
|
||||
with sqlite3.connect(database.as_uri() + '?mode=ro', uri=True) as db:
|
||||
row = db.execute("SELECT value FROM kv WHERE key='state'").fetchone()
|
||||
if not row:
|
||||
raise SystemExit('No saved state found.')
|
||||
state = json.loads(row[0])
|
||||
settings = state.get('settings', {})
|
||||
proxy = args.proxy or f"http://{settings.get('lanIP') or '127.0.0.1'}:{settings.get('mixedPort', 7890)}"
|
||||
subscriptions = [s for s in state.get('subscriptions', []) if urlsplit(s.get('url', '')).hostname == args.host]
|
||||
if not subscriptions:
|
||||
raise SystemExit('No saved subscription matches this hostname.')
|
||||
code = r'''
|
||||
import hashlib,json,os,pathlib,socket,ssl,time,urllib.error,urllib.request
|
||||
from urllib.parse import urlsplit
|
||||
payload=json.loads(__import__('sys').stdin.read())
|
||||
url=payload['url'];host=urlsplit(url).hostname
|
||||
class SafeRedirect(urllib.request.HTTPRedirectHandler):
|
||||
max_redirections=5
|
||||
def redirect_request(self,request,fp,code,msg,headers,newurl):
|
||||
target=urlsplit(newurl)
|
||||
if target.scheme not in ('http','https') or target.username is not None:
|
||||
raise urllib.error.URLError('Unsupported redirect')
|
||||
return super().redirect_request(request,fp,code,msg,headers,newurl)
|
||||
verify=ssl.get_default_verify_paths()
|
||||
result={'host':host,'runAsUID':os.getuid(),'caBundleExists':bool(verify.cafile and pathlib.Path(verify.cafile).exists()),'utcTime':time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),'tests':[]}
|
||||
try:
|
||||
addresses=socket.getaddrinfo(host,443,type=socket.SOCK_STREAM)
|
||||
result['systemDNSAddresses']=sorted({a[4][0] for a in addresses})
|
||||
except Exception as error:result['systemDNSError']=type(error).__name__
|
||||
for mode,proxy in [('direct',{}),('vm-proxy',{'http':payload['proxy'],'https':payload['proxy']})]:
|
||||
started=time.monotonic();entry={'mode':mode}
|
||||
try:
|
||||
opener=urllib.request.build_opener(urllib.request.ProxyHandler(proxy),SafeRedirect())
|
||||
request=urllib.request.Request(url,headers={'User-Agent':'clash.meta/ClashWeb'})
|
||||
try:response=opener.open(request,timeout=20)
|
||||
except urllib.error.HTTPError as error:response=error
|
||||
with response:
|
||||
body=response.read(8*1024*1024+1)
|
||||
entry.update({'httpStatus':response.status,'contentType':response.headers.get('Content-Type'),'bytes':len(body),'sha256':hashlib.sha256(body).hexdigest(),'hasClashProxies':b'proxies:' in body,'cfChallenge':response.headers.get('cf-mitigated')=='challenge'})
|
||||
except Exception as error:
|
||||
reason=getattr(error,'reason',error)
|
||||
entry['errorType']=type(reason).__name__
|
||||
if isinstance(reason,ssl.SSLCertVerificationError):
|
||||
entry['certificateErrorCode']=reason.verify_code;entry['detail']=reason.verify_message
|
||||
elif isinstance(reason,socket.gaierror):entry['detail']='System DNS lookup failed'
|
||||
elif isinstance(reason,(socket.timeout,TimeoutError)):entry['detail']='Connection or read timed out'
|
||||
else:
|
||||
entry['detail']='Network connection failed'
|
||||
if getattr(reason,'errno',None) is not None:entry['errno']=reason.errno
|
||||
entry['seconds']=round(time.monotonic()-started,2);result['tests'].append(entry)
|
||||
print(json.dumps(result,ensure_ascii=False,indent=2))
|
||||
'''
|
||||
for subscription in subscriptions:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['/usr/bin/sudo', '-n', '-u', 'clashweb', 'python3', '-c', code],
|
||||
input=json.dumps({'url': subscription['url'], 'proxy': proxy}),
|
||||
text=True, capture_output=True, timeout=60,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
raise SystemExit('Diagnostics timed out; inspect VM system DNS and connectivity.')
|
||||
if result.returncode:
|
||||
print('Diagnostics subprocess failed; no stored URL or subscription body has been printed.', file=sys.stderr)
|
||||
raise SystemExit(result.returncode)
|
||||
print(result.stdout.strip())
|
||||
Reference in new issue
Block a user