commit 54d9d8a0742ab4bfc36394311d41777fffbfa615 Author: Mentality Date: Sat Apr 11 16:39:07 2026 +0300 Create update.yml diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml new file mode 100644 index 0000000..8b4d51f --- /dev/null +++ b/.github/workflows/update.yml @@ -0,0 +1,518 @@ +name: Update Clash Config + +on: + schedule: + - cron: '*/30 * * * *' + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: pip install requests PyYAML + + - name: Run Updater + run: | + cat > updater.py << 'EOF' + import requests + import yaml + import socket + import concurrent.futures + from urllib.parse import urlparse, parse_qs + import base64 + import json + import re + import sys + + SOURCE_URL = "https://github.com/AvenCores/goida-vpn-configs/raw/refs/heads/main/githubmirror/26.txt" + OUTPUT_FILE = "clash.yaml" + TIMEOUT = 3 + MAX_WORKERS = 50 + + def parse_vmess(uri): + try: + b64_part = uri[8:].strip() + padding = 4 - (len(b64_part) % 4) + if padding != 4: + b64_part += '=' * padding + decoded = base64.b64decode(b64_part).decode('utf-8') + vmess_json = json.loads(decoded) + + proxy = { + "name": vmess_json.get("ps", "VMess"), + "type": "vmess", + "server": vmess_json.get("add", ""), + "port": int(vmess_json.get("port", 0)), + "uuid": vmess_json.get("id", ""), + "alterId": int(vmess_json.get("aid", 0)), + "cipher": vmess_json.get("scy", "auto"), + "network": vmess_json.get("net", "tcp"), + "udp": True + } + + net = vmess_json.get("net", "tcp") + if net == "ws": + proxy["ws-opts"] = { + "path": vmess_json.get("path", "/"), + "headers": {"Host": vmess_json.get("host", vmess_json.get("add", ""))} + } + elif net == "grpc": + proxy["grpc-opts"] = {"grpc-service-name": vmess_json.get("path", "")} + + if vmess_json.get("tls") == "tls": + proxy["tls"] = True + proxy["skip-cert-verify"] = True + proxy["sni"] = vmess_json.get("sni", vmess_json.get("add", "")) + + return proxy + except: + return None + + def parse_trojan(uri): + try: + parsed = urlparse(uri) + name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "Trojan" + + proxy = { + "name": name, + "type": "trojan", + "server": parsed.hostname, + "port": parsed.port or 443, + "password": parsed.username or "", + "udp": True, + "skip-cert-verify": True + } + + if parsed.query: + query = parse_qs(parsed.query) + if "sni" in query: + proxy["sni"] = query["sni"][0] + if "type" in query: + net_type = query["type"][0] + if net_type == "ws": + proxy["network"] = "ws" + proxy["ws-opts"] = { + "path": query.get("path", ["/"])[0], + "headers": {"Host": query.get("host", [parsed.hostname])[0]} + } + elif net_type == "grpc": + proxy["network"] = "grpc" + proxy["grpc-opts"] = {"grpc-service-name": query.get("serviceName", [""])[0]} + + return proxy + except: + return None + + def parse_shadowsocks(uri): + try: + if "#" in uri: + uri_part, name_part = uri.split("#", 1) + name = requests.utils.unquote(name_part) + else: + uri_part = uri + name = "Shadowsocks" + + uri_part = uri_part[5:] + + if '@' in uri_part: + userinfo, server_port = uri_part.split('@', 1) + try: + padding = 4 - (len(userinfo) % 4) + if padding != 4: + userinfo += '=' * padding + decoded = base64.b64decode(userinfo).decode('utf-8') + cipher, password = decoded.split(':', 1) + except: + if ':' in userinfo: + cipher, password = userinfo.split(':', 1) + else: + cipher, password = "aes-256-gcm", userinfo + else: + return None + + server, port = server_port.rsplit(':', 1) + + proxy = { + "name": name, + "type": "ss", + "server": server, + "port": int(port), + "cipher": cipher, + "password": password, + "udp": True + } + return proxy + except: + return None + + def parse_vless(uri): + try: + parsed = urlparse(uri) + name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "VLESS" + + proxy = { + "name": name, + "type": "vless", + "server": parsed.hostname, + "port": parsed.port or 443, + "uuid": parsed.username or "", + "udp": True, + "skip-cert-verify": True, + "encryption": "none" + } + + if parsed.query: + query = parse_qs(parsed.query) + if "security" in query and query["security"][0] == "tls": + proxy["tls"] = True + if "sni" in query: + proxy["sni"] = query["sni"][0] + if "type" in query: + net_type = query["type"][0] + if net_type == "ws": + proxy["network"] = "ws" + proxy["ws-opts"] = { + "path": query.get("path", ["/"])[0], + "headers": {"Host": query.get("host", [parsed.hostname])[0]} + } + elif net_type == "grpc": + proxy["network"] = "grpc" + proxy["grpc-opts"] = {"grpc-service-name": query.get("serviceName", [""])[0]} + + return proxy + except: + return None + + def parse_socks5(uri): + try: + parsed = urlparse(uri) + name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "SOCKS5" + + proxy = { + "name": name, + "type": "socks5", + "server": parsed.hostname, + "port": parsed.port or 1080, + "udp": True + } + + if parsed.username and parsed.password: + proxy["username"] = parsed.username + proxy["password"] = parsed.password + + return proxy + except: + return None + + def parse_http(uri): + try: + parsed = urlparse(uri) + name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "HTTP" + + proxy = { + "name": name, + "type": "http", + "server": parsed.hostname, + "port": parsed.port or 8080, + } + + if parsed.username and parsed.password: + proxy["username"] = parsed.username + proxy["password"] = parsed.password + + if parsed.scheme == "https": + proxy["tls"] = True + proxy["skip-cert-verify"] = True + + return proxy + except: + return None + + def parse_hysteria(uri): + try: + parsed = urlparse(uri) + name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "Hysteria" + + proxy = { + "name": name, + "type": "hysteria", + "server": parsed.hostname, + "port": parsed.port or 443, + "up": 100, + "down": 100, + "skip-cert-verify": True + } + + if parsed.query: + query = parse_qs(parsed.query) + if "auth" in query: + proxy["auth-str"] = query["auth"][0] + if "peer" in query: + proxy["sni"] = query["peer"][0] + if "upmbps" in query: + proxy["up"] = int(query["upmbps"][0]) + if "downmbps" in query: + proxy["down"] = int(query["downmbps"][0]) + + return proxy + except: + return None + + def parse_hysteria2(uri): + try: + parsed = urlparse(uri) + name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "Hysteria2" + + password = parsed.username or "" + + proxy = { + "name": name, + "type": "hysteria2", + "server": parsed.hostname, + "port": parsed.port or 443, + "password": password, + "skip-cert-verify": True + } + + if parsed.query: + query = parse_qs(parsed.query) + if "auth" in query and not password: + proxy["password"] = query["auth"][0] + if "sni" in query: + proxy["sni"] = query["sni"][0] + + return proxy + except: + return None + + def parse_tuic(uri): + try: + parsed = urlparse(uri) + name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "Tuic" + + uuid = "" + password = "" + if parsed.username and ':' in parsed.username: + uuid, password = parsed.username.split(':', 1) + else: + uuid = parsed.username or "" + + proxy = { + "name": name, + "type": "tuic", + "server": parsed.hostname, + "port": parsed.port or 443, + "uuid": uuid, + "password": password, + "udp": True, + "skip-cert-verify": True + } + + if parsed.query: + query = parse_qs(parsed.query) + if "sni" in query: + proxy["sni"] = query["sni"][0] + + return proxy + except: + return None + + def parse_wireguard(uri): + try: + parsed = urlparse(uri) + name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "WireGuard" + + proxy = { + "name": name, + "type": "wireguard", + "server": parsed.hostname, + "port": parsed.port or 51820, + "private-key": parsed.username or "", + "udp": True + } + + if parsed.query: + query = parse_qs(parsed.query) + if "publicKey" in query: + proxy["public-key"] = query["publicKey"][0] + if "address" in query: + addresses = query["address"][0].split(',') + proxy["ip"] = addresses[0] if addresses else None + if "mtu" in query: + proxy["mtu"] = int(query["mtu"][0]) + + return proxy + except: + return None + + def parse_shadowtls(uri): + try: + parsed = urlparse(uri) + name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "ShadowTLS" + + proxy = { + "name": name, + "type": "shadowtls", + "server": parsed.hostname, + "port": parsed.port or 443, + "version": 3, + "skip-cert-verify": True + } + + if parsed.query: + query = parse_qs(parsed.query) + if "password" in query: + proxy["password"] = query["password"][0] + if "sni" in query: + proxy["sni"] = query["sni"][0] + + return proxy + except: + return None + + def parse_proxies_from_text(text): + proxies = [] + lines = text.strip().split('\n') + + parsers = { + 'vmess://': parse_vmess, + 'trojan://': parse_trojan, + 'ss://': parse_shadowsocks, + 'vless://': parse_vless, + 'socks5://': parse_socks5, + 'socks://': parse_socks5, + 'http://': parse_http, + 'https://': parse_http, + 'hysteria://': parse_hysteria, + 'hysteria2://': parse_hysteria2, + 'hy2://': parse_hysteria2, + 'tuic://': parse_tuic, + 'wireguard://': parse_wireguard, + 'wg://': parse_wireguard, + 'shadowtls://': parse_shadowtls, + } + + for line in lines: + line = line.strip() + if not line or line.startswith('#'): + continue + + for prefix, parser in parsers.items(): + if line.startswith(prefix): + proxy = parser(line) + if proxy: + proxies.append(proxy) + break + + return proxies + + def check_tcp_port(proxy): + try: + if proxy.get("type") == "wireguard": + print(f"⚠️ WireGuard пропущен (UDP): {proxy.get('server')}:{proxy.get('port')}") + return proxy + + server = proxy.get("server") + port = proxy.get("port") + if not server or not port: + return None + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(TIMEOUT) + result = sock.connect_ex((server, port)) + sock.close() + + if result == 0: + return proxy + else: + print(f"❌ Мёртв: {server}:{port} ({proxy.get('name')})") + return None + except: + return None + + def filter_alive_proxies(proxies): + alive = [] + print(f"🔄 Проверяем {len(proxies)} серверов...") + + with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: + futures = {executor.submit(check_tcp_port, p): p for p in proxies} + for future in concurrent.futures.as_completed(futures): + result = future.result() + if result: + print(f"✅ Жив: {result.get('server')}:{result.get('port')} ({result.get('name')}) - {result.get('type')}") + alive.append(result) + + print(f"✅ Найдено живых: {len(alive)} из {len(proxies)}") + return alive + + def generate_clash_config(proxies): + config = { + "proxies": proxies, + "proxy-groups": [ + { + "name": "🚀 Авто-выбор", + "type": "url-test", + "proxies": [p["name"] for p in proxies], + "url": "http://www.gstatic.com/generate_204", + "interval": 300 + }, + { + "name": "🌍 Прокси", + "type": "select", + "proxies": ["🚀 Авто-выбор"] + [p["name"] for p in proxies] + } + ], + "rules": [ + "MATCH,🌍 Прокси" + ] + } + return config + + def main(): + print("🚀 Загрузка списка прокси...") + try: + response = requests.get(SOURCE_URL, timeout=30) + response.raise_for_status() + content = response.text + except Exception as e: + print(f"❌ Ошибка загрузки TXT: {e}") + sys.exit(1) + + print("🔍 Парсинг всех форматов...") + all_proxies = parse_proxies_from_text(content) + print(f"📦 Распознано прокси: {len(all_proxies)}") + + print("🩺 Проверка доступности...") + alive_proxies = filter_alive_proxies(all_proxies) + + print("📝 Генерация YAML...") + config = generate_clash_config(alive_proxies) + + with open(OUTPUT_FILE, 'w', encoding='utf-8') as f: + yaml.dump(config, f, allow_unicode=True, default_flow_style=False, sort_keys=False) + + print(f"✅ Готово! Сохранено в {OUTPUT_FILE}") + + if __name__ == "__main__": + main() + EOF + python updater.py + + - name: Commit and Push changes + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add clash.yaml + git diff --quiet && git diff --staged --quiet || (git commit -m "Auto-update config [skip ci]" && git push)