├── SS_Node_Extractor.spec └── ss_extractor_gui.py /SS_Node_Extractor.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | a = Analysis( 5 | ['ss_extractor_gui.py'], 6 | pathex=[], 7 | binaries=[], 8 | datas=[], 9 | hiddenimports=[], 10 | hookspath=[], 11 | hooksconfig={}, 12 | runtime_hooks=[], 13 | excludes=[], 14 | noarchive=False, 15 | optimize=0, 16 | ) 17 | pyz = PYZ(a.pure) 18 | 19 | exe = EXE( 20 | pyz, 21 | a.scripts, 22 | a.binaries, 23 | a.datas, 24 | [], 25 | name='SS_Node_Extractor', 26 | debug=False, 27 | bootloader_ignore_signals=False, 28 | strip=False, 29 | upx=True, 30 | upx_exclude=[], 31 | runtime_tmpdir=None, 32 | console=False, 33 | disable_windowed_traceback=False, 34 | argv_emulation=False, 35 | target_arch=None, 36 | codesign_identity=None, 37 | entitlements_file=None, 38 | ) 39 | -------------------------------------------------------------------------------- /ss_extractor_gui.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from tkinter import messagebox 4 | import requests 5 | import base64 6 | import json 7 | import pyaes 8 | import binascii 9 | from datetime import datetime 10 | import threading 11 | import pyperclip 12 | import os 13 | import yaml 14 | from urllib.parse import quote 15 | 16 | class SSExtractorGUI: 17 | def __init__(self, root): 18 | self.root = root 19 | self.root.title("SS Node Extractor") 20 | self.root.geometry("600x400") 21 | 22 | # 设置窗口图标 23 | self.setup_ui() 24 | 25 | def setup_ui(self): 26 | # 标题区域 27 | title_frame = ttk.Frame(self.root) 28 | title_frame.pack(pady=10) 29 | 30 | ttk.Label(title_frame, text="SS Node Extractor", font=('Arial', 16, 'bold')).pack() 31 | ttk.Label(title_frame, text=f"Version: 1.0").pack() 32 | ttk.Label(title_frame, text="作者: 佚名").pack() 33 | 34 | # 按钮区域 35 | button_frame = ttk.Frame(self.root) 36 | button_frame.pack(pady=10) 37 | 38 | self.extract_btn = ttk.Button(button_frame, text="提取节点", command=self.start_extraction) 39 | self.extract_btn.pack(side=tk.LEFT, padx=5) 40 | 41 | self.copy_btn = ttk.Button(button_frame, text="复制节点", command=self.copy_nodes, state=tk.DISABLED) 42 | self.copy_btn.pack(side=tk.LEFT, padx=5) 43 | 44 | self.save_btn = ttk.Button(button_frame, text="保存到文件", command=self.save_to_file, state=tk.DISABLED) 45 | self.save_btn.pack(side=tk.LEFT, padx=5) 46 | 47 | # 添加新的转换按钮 48 | convert_frame = ttk.Frame(self.root) 49 | convert_frame.pack(pady=5) 50 | 51 | self.clash_btn = ttk.Button(convert_frame, text="保存为Clash配置", 52 | command=self.save_clash_config, state=tk.DISABLED) 53 | self.clash_btn.pack(side=tk.LEFT, padx=5) 54 | 55 | self.base64_btn = ttk.Button(convert_frame, text="保存为Base64订阅", 56 | command=self.save_base64_subscription, state=tk.DISABLED) 57 | self.base64_btn.pack(side=tk.LEFT, padx=5) 58 | 59 | # 进度条 60 | self.progress = ttk.Progressbar(self.root, mode='indeterminate') 61 | self.progress.pack(fill=tk.X, padx=20, pady=10) 62 | 63 | # 结果显示区域 64 | self.result_text = tk.Text(self.root, height=15, width=60) 65 | self.result_text.pack(padx=20, pady=10) 66 | 67 | self.nodes = [] 68 | 69 | def decrypt_data(self, encrypted_data, key, iv): 70 | decryptor = pyaes.AESModeOfOperationCBC(key, iv=iv) 71 | decrypted = b''.join(decryptor.decrypt(encrypted_data[i:i+16]) for i in range(0, len(encrypted_data), 16)) 72 | return decrypted[:-decrypted[-1]] 73 | 74 | def extract_nodes(self): 75 | try: 76 | url = 'http://api.skrapp.net/api/serverlist' 77 | headers = { 78 | 'accept': '/', 79 | 'accept-language': 'zh-Hans-CN;q=1, en-CN;q=0.9', 80 | 'appversion': '1.3.1', 81 | 'user-agent': 'SkrKK/1.3.1 (iPhone; iOS 13.5; Scale/2.00)', 82 | 'content-type': 'application/x-www-form-urlencoded', 83 | 'Cookie': 'PHPSESSID=fnffo1ivhvt0ouo6ebqn86a0d4' 84 | } 85 | data = {'data': '4265a9c353cd8624fd2bc7b5d75d2f18b1b5e66ccd37e2dfa628bcb8f73db2f14ba98bc6a1d8d0d1c7ff1ef0823b11264d0addaba2bd6a30bdefe06f4ba994ed'} 86 | key = b'65151f8d966bf596' 87 | iv = b'88ca0f0ea1ecf975' 88 | 89 | response = requests.post(url, headers=headers, data=data) 90 | 91 | if response.status_code == 200: 92 | encrypted_text = response.text.strip() 93 | encrypted_data = binascii.unhexlify(encrypted_text) 94 | decrypted_data = self.decrypt_data(encrypted_data, key, iv) 95 | nodes_data = json.loads(decrypted_data) 96 | 97 | self.nodes = [] 98 | for node in nodes_data['data']: 99 | ss_url = f"aes-256-cfb:{node['password']}@{node['ip']}:{node['port']}" 100 | ss_base64 = base64.b64encode(ss_url.encode('utf-8')).decode('utf-8') 101 | ss_link = f"ss://{ss_base64}#{node['title']}" 102 | self.nodes.append(ss_link) 103 | 104 | self.root.after(0, self.update_ui_after_extraction) 105 | else: 106 | self.root.after(0, lambda: self.show_error("请求失败")) 107 | except Exception as e: 108 | self.root.after(0, lambda: self.show_error(f"错误: {str(e)}")) 109 | 110 | def start_extraction(self): 111 | self.extract_btn.config(state=tk.DISABLED) 112 | self.copy_btn.config(state=tk.DISABLED) 113 | self.save_btn.config(state=tk.DISABLED) 114 | self.result_text.delete(1.0, tk.END) 115 | self.progress.start() 116 | 117 | thread = threading.Thread(target=self.extract_nodes) 118 | thread.daemon = True 119 | thread.start() 120 | 121 | def update_ui_after_extraction(self): 122 | self.progress.stop() 123 | self.extract_btn.config(state=tk.NORMAL) 124 | self.copy_btn.config(state=tk.NORMAL) 125 | self.save_btn.config(state=tk.NORMAL) 126 | self.clash_btn.config(state=tk.NORMAL) # 启用Clash配置按钮 127 | self.base64_btn.config(state=tk.NORMAL) # 启用Base64订阅按钮 128 | 129 | self.result_text.delete(1.0, tk.END) 130 | for node in self.nodes: 131 | self.result_text.insert(tk.END, node + '\n') 132 | 133 | def copy_nodes(self): 134 | nodes_text = '\n'.join(self.nodes) 135 | pyperclip.copy(nodes_text) 136 | self.show_message("节点已复制到剪贴板") 137 | 138 | def save_to_file(self): 139 | if not self.nodes: 140 | return 141 | 142 | filename = f"ss_nodes_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" 143 | with open(filename, 'w', encoding='utf-8') as f: 144 | f.write('\n'.join(self.nodes)) 145 | self.show_message(f"节点已保存到文件: {filename}") 146 | 147 | def show_message(self, message): 148 | tk.messagebox.showinfo("提示", message) 149 | 150 | def show_error(self, message): 151 | self.progress.stop() 152 | self.extract_btn.config(state=tk.NORMAL) 153 | tk.messagebox.showerror("错误", message) 154 | 155 | def save_clash_config(self): 156 | if not self.nodes: 157 | self.show_error("没有可用的节点") 158 | return 159 | 160 | try: 161 | # 将所有节点合并成一个字符串并进行 URL 编码 162 | nodes_text = '|'.join(self.nodes) 163 | encoded_nodes = quote(nodes_text) 164 | 165 | # 构建转换网页 URL 166 | web_url = ( 167 | "https://suburl.v1.mk/" 168 | f"#/sub?target=clash" # 使用 #/sub 而不是 /sub 169 | f"&url={encoded_nodes}" 170 | "&insert=false" 171 | "&config=https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/config/ACL4SSR_Online_Full_NoAuto.ini" 172 | "&emoji=true" 173 | "&list=false" 174 | "&udp=false" 175 | "&tfo=false" 176 | "&expand=true" 177 | "&scv=false" 178 | "&fdn=false" 179 | "&new_name=true" 180 | ) 181 | 182 | # 使用默认浏览器打开转换网页 183 | import webbrowser 184 | webbrowser.open(web_url) 185 | self.show_message("已打开转换网页,请点击下载按钮保存配置文件") 186 | 187 | except Exception as e: 188 | self.show_error(f"打开网页失败: {str(e)}") 189 | 190 | def convert_to_base64(self): 191 | if not self.nodes: 192 | self.show_error("没有可用的节点") 193 | return 194 | 195 | # 确保每个节点都是单独的一行,并移除空行 196 | nodes_text = '\n'.join(node.strip() for node in self.nodes if node.strip()) 197 | # 确保最后有一个换行符,这对某些客户���很重要 198 | if not nodes_text.endswith('\n'): 199 | nodes_text += '\n' 200 | return base64.b64encode(nodes_text.encode('utf-8')).decode('utf-8') 201 | 202 | def save_base64_subscription(self): 203 | try: 204 | # 将节点转换为Base64格式 205 | nodes_text = '\n'.join(node.strip() for node in self.nodes if node.strip()) 206 | if not nodes_text.endswith('\n'): 207 | nodes_text += '\n' 208 | 209 | # 进行Base64编码 210 | base64_content = base64.b64encode(nodes_text.encode('utf-8')).decode('utf-8') 211 | 212 | # 保存文件 213 | filename = f"subscription_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" 214 | with open(filename, 'w', encoding='utf-8', newline='') as f: 215 | f.write(base64_content) 216 | 217 | # 验证文件 218 | try: 219 | with open(filename, 'r', encoding='utf-8') as f: 220 | test_content = f.read().strip() 221 | decoded = base64.b64decode(test_content).decode('utf-8') 222 | if not any(line.startswith('ss://') for line in decoded.splitlines()): 223 | raise ValueError("订阅内容格式错误") 224 | except Exception as e: 225 | os.remove(filename) 226 | raise ValueError(f"订阅文件验证失败: {str(e)}") 227 | 228 | self.show_message(f"Base64订阅已保存到文件: {filename}\n" 229 | f"共 {len(self.nodes)} 个节点\n" 230 | "可直接导入到V2rayN使用") 231 | 232 | except Exception as e: 233 | self.show_error(f"生成Base64订阅失败: {str(e)}") 234 | 235 | if __name__ == "__main__": 236 | root = tk.Tk() 237 | app = SSExtractorGUI(root) 238 | root.mainloop() --------------------------------------------------------------------------------