nbugs commited on
Commit
cc34fea
·
verified ·
1 Parent(s): 4f5d5ba

Delete converter.py

Browse files
Files changed (1) hide show
  1. converter.py +0 -492
converter.py DELETED
@@ -1,492 +0,0 @@
1
- import base64
2
- import json
3
- import re
4
- import yaml
5
- import requests
6
- from ruamel.yaml import YAML
7
- from urllib.parse import urlparse, parse_qs
8
-
9
- # 用于生成美观YAML的实例
10
- ruamel_yaml = YAML()
11
- ruamel_yaml.indent(mapping=2, sequence=4, offset=2)
12
- ruamel_yaml.width = 80
13
- ruamel_yaml.allow_unicode = True
14
-
15
- class SubscriptionConverter:
16
- def __init__(self):
17
- self.supported_types = ['clash', 'clashr', 'surge', 'quan', 'quanx', 'loon', 'ss', 'ssr', 'v2ray']
18
-
19
- def convert(self, subscription_url, target_type, config_url=None):
20
- """
21
- 转换订阅链接到目标格式
22
- """
23
- if target_type not in self.supported_types:
24
- return {"status": "error", "message": f"不支持的目标类型: {target_type}"}
25
-
26
- try:
27
- # 获取原始订阅内容
28
- nodes = self._fetch_subscription(subscription_url)
29
- if not nodes:
30
- return {"status": "error", "message": "无法获取订阅内容或订阅内容为空"}
31
-
32
- # 获取配置文件 (如果提供)
33
- config = {}
34
- if config_url:
35
- config = self._fetch_config(config_url)
36
-
37
- # 根据目标类型进行转换
38
- if target_type == 'clash':
39
- result = self._convert_to_clash(nodes, config)
40
- elif target_type == 'v2ray':
41
- result = self._convert_to_v2ray(nodes)
42
- else:
43
- # 其他格式转换逻辑
44
- return {"status": "error", "message": f"目标类型 {target_type} 暂未实现具体转换逻辑"}
45
-
46
- return {"status": "success", "result": result}
47
-
48
- except Exception as e:
49
- return {"status": "error", "message": f"转换过程出错: {str(e)}"}
50
-
51
- def _fetch_subscription(self, url):
52
- """获取订阅内容并解析节点"""
53
- try:
54
- response = requests.get(url, timeout=15)
55
- if response.status_code != 200:
56
- return None
57
-
58
- content = response.text
59
- # 尝试Base64解码(大多数订阅是Base64编码的)
60
- try:
61
- decoded = base64.b64decode(content).decode('utf-8')
62
- content = decoded
63
- except:
64
- # 非Base64编码或已经解码,使用原始内容
65
- pass
66
-
67
- # 解析节点信息(根据订阅内容类型)
68
- if content.startswith('{'): # JSON格式
69
- try:
70
- data = json.loads(content)
71
- return self._parse_json_subscription(data)
72
- except:
73
- pass
74
-
75
- elif 'proxies:' in content: # YAML (Clash)格式
76
- try:
77
- data = yaml.safe_load(content)
78
- return self._parse_yaml_subscription(data)
79
- except:
80
- pass
81
-
82
- else: # 文本格式(每行一个节点链接)
83
- return self._parse_text_subscription(content)
84
-
85
- return []
86
- except Exception as e:
87
- print(f"获取订阅内容失败: {str(e)}")
88
- return []
89
-
90
- def _parse_json_subscription(self, data):
91
- """解析JSON格式的订阅内容"""
92
- nodes = []
93
-
94
- # 支持常见JSON订阅格式
95
- if 'proxies' in data:
96
- return data['proxies'] # Clash格式
97
- elif 'servers' in data:
98
- # SS格式
99
- for server in data['servers']:
100
- nodes.append({
101
- 'type': 'ss',
102
- 'name': server.get('remarks', f"Server {len(nodes) + 1}"),
103
- 'server': server.get('server', ''),
104
- 'port': server.get('server_port', 0),
105
- 'password': server.get('password', ''),
106
- 'cipher': server.get('method', 'aes-256-gcm')
107
- })
108
-
109
- return nodes
110
-
111
- def _parse_yaml_subscription(self, data):
112
- """解析YAML格式的订阅内容"""
113
- if 'proxies' in data and isinstance(data['proxies'], list):
114
- return data['proxies']
115
- return []
116
-
117
- def _parse_text_subscription(self, content):
118
- """解析文本格式的订阅内容(每行一个URI)"""
119
- nodes = []
120
- for line in content.splitlines():
121
- line = line.strip()
122
- if not line:
123
- continue
124
-
125
- # 尝试解析不同协议的URI
126
- if line.startswith('ss://'):
127
- node = self._parse_ss_uri(line)
128
- if node:
129
- nodes.append(node)
130
- elif line.startswith('ssr://'):
131
- node = self._parse_ssr_uri(line)
132
- if node:
133
- nodes.append(node)
134
- elif line.startswith('vmess://'):
135
- node = self._parse_vmess_uri(line)
136
- if node:
137
- nodes.append(node)
138
- elif line.startswith('trojan://'):
139
- node = self._parse_trojan_uri(line)
140
- if node:
141
- nodes.append(node)
142
-
143
- return nodes
144
-
145
- def _parse_ss_uri(self, uri):
146
- """解析 SS 协议URI"""
147
- try:
148
- if '#' in uri:
149
- encoded_part, name = uri.split('#', 1)
150
- name = name.strip()
151
- else:
152
- encoded_part = uri
153
- name = f"SS {uri[:8]}"
154
-
155
- encoded_part = encoded_part.replace('ss://', '')
156
-
157
- # 处理不同格式的SS链接
158
- if '@' in encoded_part:
159
- # ss://method:password@server:port
160
- auth_part, server_part = encoded_part.split('@', 1)
161
-
162
- # 处理method:password部分 (可能需要解码)
163
- if ':' not in auth_part:
164
- try:
165
- auth_part = base64.b64decode(auth_part).decode('utf-8')
166
- except:
167
- pass
168
-
169
- if ':' in auth_part:
170
- method, password = auth_part.split(':', 1)
171
- else:
172
- return None
173
-
174
- # 处理server:port部分
175
- server, port = server_part.split(':', 1)
176
- port = int(port)
177
- else:
178
- # ss://BASE64(method:password@server:port)
179
- try:
180
- decoded = base64.b64decode(encoded_part).decode('utf-8')
181
- if '@' in decoded:
182
- auth_part, server_part = decoded.split('@', 1)
183
- method, password = auth_part.split(':', 1)
184
- server, port_str = server_part.split(':', 1)
185
- port = int(port_str)
186
- else:
187
- return None
188
- except:
189
- return None
190
-
191
- return {
192
- 'type': 'ss',
193
- 'name': name,
194
- 'server': server,
195
- 'port': port,
196
- 'password': password,
197
- 'cipher': method
198
- }
199
- except:
200
- return None
201
-
202
- def _parse_ssr_uri(self, uri):
203
- """解析 SSR 协议URI"""
204
- try:
205
- encoded_part = uri.replace('ssr://', '')
206
-
207
- # SSR链接格式: ssr://BASE64(server:port:protocol:method:obfs:BASE64(password)/?params)
208
- try:
209
- decoded = base64.b64decode(encoded_part).decode('utf-8')
210
- except:
211
- return None
212
-
213
- # 分离主要部分和参数部分
214
- if '/' in decoded:
215
- main_part, params_part = decoded.split('/', 1)
216
- params = parse_qs(params_part.lstrip('?'))
217
- else:
218
- main_part = decoded
219
- params = {}
220
-
221
- # 解析主要部分
222
- parts = main_part.split(':')
223
- if len(parts) < 6:
224
- return None
225
-
226
- server, port, protocol, method, obfs = parts[:5]
227
- password_base64 = parts[5]
228
- try:
229
- password = base64.b64decode(password_base64).decode('utf-8')
230
- except:
231
- password = password_base64
232
-
233
- # 获取参数
234
- obfs_param = base64.b64decode(params.get('obfsparam', [''])[0]).decode('utf-8') if 'obfsparam' in params else ''
235
- protocol_param = base64.b64decode(params.get('protoparam', [''])[0]).decode('utf-8') if 'protoparam' in params else ''
236
- remarks = base64.b64decode(params.get('remarks', [''])[0]).decode('utf-8') if 'remarks' in params else f"SSR {server[:8]}"
237
-
238
- return {
239
- 'type': 'ssr',
240
- 'name': remarks,
241
- 'server': server,
242
- 'port': int(port),
243
- 'password': password,
244
- 'cipher': method,
245
- 'protocol': protocol,
246
- 'protocol-param': protocol_param,
247
- 'obfs': obfs,
248
- 'obfs-param': obfs_param
249
- }
250
- except:
251
- return None
252
-
253
- def _parse_vmess_uri(self, uri):
254
- """解析 VMess 协议URI"""
255
- try:
256
- encoded_part = uri.replace('vmess://', '')
257
-
258
- # VMess链接通常是Base64编码的JSON
259
- try:
260
- decoded = base64.b64decode(encoded_part).decode('utf-8')
261
- config = json.loads(decoded)
262
- except:
263
- return None
264
-
265
- # 标准格式包含v,ps,add,port,id,aid,net等字段
266
- return {
267
- 'type': 'vmess',
268
- 'name': config.get('ps', f"VMess Server"),
269
- 'server': config.get('add', ''),
270
- 'port': int(config.get('port', 0)),
271
- 'uuid': config.get('id', ''),
272
- 'alterId': int(config.get('aid', 0)),
273
- 'cipher': 'auto',
274
- 'network': config.get('net', 'tcp'),
275
- 'tls': True if config.get('tls') == 'tls' else False,
276
- 'ws-path': config.get('path', ''),
277
- 'ws-headers': {'Host': config.get('host', '')} if config.get('host') else {}
278
- }
279
- except:
280
- return None
281
-
282
- def _parse_trojan_uri(self, uri):
283
- """解析 Trojan 协议URI"""
284
- try:
285
- uri = uri.replace('trojan://', '')
286
-
287
- # trojan://password@server:port?allowInsecure=1&peer=example.com#name
288
- match = re.match(r'^([^@]+)@([^:]+):(\d+)(.*)$', uri)
289
- if not match:
290
- return None
291
-
292
- password, server, port, params_part = match.groups()
293
-
294
- # 解析参数
295
- name = ''
296
- sni = ''
297
- allow_insecure = False
298
-
299
- if '#' in params_part:
300
- params_part, name = params_part.split('#', 1)
301
-
302
- if '?' in params_part:
303
- params_str = params_part.lstrip('?')
304
- params = parse_qs(params_str)
305
- sni = params.get('peer', [''])[0] or params.get('sni', [''])[0]
306
- allow_insecure = params.get('allowInsecure', ['0'])[0] == '1'
307
-
308
- return {
309
- 'type': 'trojan',
310
- 'name': name or f"Trojan {server[:8]}",
311
- 'server': server,
312
- 'port': int(port),
313
- 'password': password,
314
- 'sni': sni,
315
- 'skip-cert-verify': allow_insecure
316
- }
317
- except:
318
- return None
319
-
320
- def _fetch_config(self, config_url):
321
- """获取配置文件内容"""
322
- try:
323
- response = requests.get(config_url, timeout=15)
324
- if response.status_code != 200:
325
- return {}
326
-
327
- content = response.text
328
-
329
- # 检测配置文件格式并解析
330
- if content.startswith('{'): # JSON
331
- return json.loads(content)
332
- elif '[' in content and ']' in content: # INI
333
- return self._parse_ini_config(content)
334
- else: # 尝试作为YAML解析
335
- try:
336
- return yaml.safe_load(content) or {}
337
- except:
338
- return {}
339
- except:
340
- return {}
341
-
342
- def _parse_ini_config(self, content):
343
- """解析INI格式的配置文件"""
344
- config = {
345
- 'rules': [],
346
- 'groups': []
347
- }
348
-
349
- current_section = None
350
-
351
- for line in content.splitlines():
352
- line = line.strip()
353
- if not line or line.startswith(';') or line.startswith('#'):
354
- continue
355
-
356
- # 检测节
357
- if line.startswith('[') and line.endswith(']'):
358
- current_section = line[1:-1].strip()
359
- continue
360
-
361
- # 处理Rule节
362
- if current_section == 'Rule':
363
- config['rules'].append(line)
364
- # 处理Proxy Group节
365
- elif current_section and 'Group' in current_section:
366
- config['groups'].append(line)
367
-
368
- return config
369
-
370
- def _convert_to_clash(self, nodes, config):
371
- """转换为Clash配置格式"""
372
- # 创建基本结构
373
- clash_config = {
374
- 'port': 7890,
375
- 'socks-port': 7891,
376
- 'allow-lan': True,
377
- 'mode': 'Rule',
378
- 'log-level': 'info',
379
- 'external-controller': '127.0.0.1:9090',
380
- 'proxies': nodes,
381
- 'proxy-groups': [],
382
- 'rules': []
383
- }
384
-
385
- # 根据配置创建代理组
386
- if config.get('groups'):
387
- default_groups = [
388
- {
389
- 'name': '🚀 节点选择',
390
- 'type': 'select',
391
- 'proxies': ['DIRECT'] + [node['name'] for node in nodes]
392
- },
393
- {
394
- 'name': '🌍 国外媒体',
395
- 'type': 'select',
396
- 'proxies': ['🚀 节点选择'] + [node['name'] for node in nodes]
397
- },
398
- {
399
- 'name': '📲 电报信息',
400
- 'type': 'select',
401
- 'proxies': ['🚀 节点选择'] + [node['name'] for node in nodes]
402
- },
403
- {
404
- 'name': '🍎 苹果服务',
405
- 'type': 'select',
406
- 'proxies': ['DIRECT', '🚀 节点选择']
407
- },
408
- {
409
- 'name': '🎯 全球直连',
410
- 'type': 'select',
411
- 'proxies': ['DIRECT', '🚀 节点选择']
412
- },
413
- {
414
- 'name': '🛑 全球拦截',
415
- 'type': 'select',
416
- 'proxies': ['REJECT', 'DIRECT']
417
- },
418
- {
419
- 'name': '⚓ 漏网之鱼',
420
- 'type': 'select',
421
- 'proxies': ['🚀 节点选择', 'DIRECT']
422
- }
423
- ]
424
- clash_config['proxy-groups'] = default_groups
425
-
426
- # 添加规则
427
- if config.get('rules'):
428
- clash_config['rules'] = config.get('rules', [])
429
- else:
430
- # 默认规则
431
- clash_config['rules'] = [
432
- 'DOMAIN-SUFFIX,google.com,🚀 节点选择',
433
- 'DOMAIN-KEYWORD,google,🚀 节点选择',
434
- 'DOMAIN-SUFFIX,ad.com,🛑 全球拦截',
435
- 'DOMAIN-SUFFIX,apple.com,🍎 苹果服务',
436
- 'DOMAIN-SUFFIX,telegram.org,📲 电报信息',
437
- 'DOMAIN-SUFFIX,youtube.com,🌍 国外媒体',
438
- 'DOMAIN-SUFFIX,netflix.com,🌍 国外媒体',
439
- 'GEOIP,CN,🎯 全球直连',
440
- 'MATCH,⚓ 漏网之鱼'
441
- ]
442
-
443
- # 转换成YAML格式文本
444
- yaml_str = yaml.dump(clash_config, allow_unicode=True, sort_keys=False)
445
- return yaml_str
446
-
447
- def _convert_to_v2ray(self, nodes):
448
- """转换为V2Ray格式"""
449
- # 实际情况下,可能需要更复杂的处理逻辑
450
- v2ray_config = {
451
- "outbounds": []
452
- }
453
-
454
- for node in nodes:
455
- if node['type'] == 'vmess':
456
- outbound = {
457
- "protocol": "vmess",
458
- "settings": {
459
- "vnext": [
460
- {
461
- "address": node['server'],
462
- "port": node['port'],
463
- "users": [
464
- {
465
- "id": node['uuid'],
466
- "alterId": node.get('alterId', 0),
467
- "security": node.get('cipher', 'auto')
468
- }
469
- ]
470
- }
471
- ]
472
- },
473
- "tag": node['name']
474
- }
475
-
476
- # 添加额外设置
477
- if node.get('network') == 'ws':
478
- outbound["streamSettings"] = {
479
- "network": "ws",
480
- "security": "tls" if node.get('tls') else "none",
481
- "wsSettings": {
482
- "path": node.get('ws-path', ''),
483
- "headers": node.get('ws-headers', {})
484
- }
485
- }
486
-
487
- v2ray_config["outbounds"].append(outbound)
488
-
489
- return json.dumps(v2ray_config, ensure_ascii=False, indent=2)
490
-
491
- # 实例化供使用
492
- converter = SubscriptionConverter()