Skip to content

Commit d186c33

Browse files
committed
feat: Add support for base64 http/https subscribe links
feat: Add support for Trojan protocol fix: duplicate short link in R2 fix: the route of clash config
1 parent 34f2d7b commit d186c33

6 files changed

+239
-114
lines changed

README.md

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Sublink Worker
22

3-
这是一个可部署在Cloudflare Worker的轻量级订阅转换工具,用于将各种代理协议的订阅链接转换为不同客户端可用的配置格式
3+
这是一个可部署在Cloudflare Worker的轻量级订阅转换工具,用于将各种代理协议的分享url转换为不同客户端可用的订阅链接
44

55
![image](/doc/image.png)
66

@@ -9,14 +9,26 @@
99
1010
## 功能特点
1111

12-
- 支持协议:SS、VMess、VLESS、Hysteria2
12+
- 支持协议:SS、VMess、VLESS、Hysteria2、Trojan
13+
- 支持从其他base64订阅链接获取分享url
1314
- 支持客户端:
1415
- Sing-Box
1516
- Clash
1617
- XRay/V2Ray
1718
- 提供Web界面用于便捷操作
1819
- 支持短链接生成(基于R2)
1920
- 浅色/深色主题切换
21+
- **新功能:支持 HTTP/HTTPS 的 Base64 订阅链接**
22+
- **新功能:支持 Trojan 协议**
23+
- **各种优化和性能提升**
24+
25+
## 最近更新(7.28)
26+
27+
我很高兴宣布以下新功能和改进:
28+
29+
1. 新增对 HTTP/HTTPS 的 Base64 订阅链接的支持
30+
2. 新增对 Trojan 协议的支持
31+
3. 优化了页面引导和整体性能
2032

2133
## 部署
2234

src/ClashConfigBuilder.js

+122-78
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,38 @@ import { CLASH_CONFIG, SELECTORS_LIST } from './config.js';
44
import { DeepCopy } from './utils.js';
55

66
export class ClashConfigBuilder {
7-
constructor(inputString) {
8-
this.inputString = inputString;
9-
this.config = DeepCopy(CLASH_CONFIG);
10-
}
7+
constructor(inputString) {
8+
this.inputString = inputString;
9+
this.config = DeepCopy(CLASH_CONFIG);
10+
}
1111

12-
async build() {
13-
const customProxies = await this.parseCustomProxies();
14-
this.addCustomProxies(customProxies);
15-
return yaml.dump(this.config);
16-
}
12+
async build() {
13+
const customProxies = await this.parseCustomProxies();
14+
this.addCustomProxies(customProxies);
15+
return yaml.dump(this.config);
16+
}
1717

18-
async parseCustomProxies() {
19-
const urls = this.inputString.split('\n').filter(url => url.trim() !== '');
20-
return Promise.all(urls.map(url => ProxyParser.parse(url)));
21-
}
18+
async parseCustomProxies() {
19+
const urls = this.inputString.split('\n').filter(url => url.trim() !== '');
20+
const parsedProxies = [];
21+
22+
for (const url of urls) {
23+
const result = await ProxyParser.parse(url);
24+
if (Array.isArray(result)) {
25+
// If the result is an array, it's from an HTTP(S) source
26+
for (const subUrl of result) {
27+
const subResult = await ProxyParser.parse(subUrl);
28+
if (subResult) {
29+
parsedProxies.push(subResult);
30+
}
31+
}
32+
} else if (result) {
33+
parsedProxies.push(result);
34+
}
35+
}
36+
37+
return parsedProxies;
38+
}
2239

2340
addCustomProxies(customProxies) {
2441
customProxies.forEach(proxy => {
@@ -27,6 +44,7 @@ export class ClashConfigBuilder {
2744
}
2845
});
2946
const proxyList = customProxies.filter(proxy => proxy?.tag !== undefined).map(proxy => proxy?.tag);
47+
proxyList.push('DIRECT', 'REJECT');
3048
SELECTORS_LIST.forEach(selector => {
3149
if (!this.config['proxy-groups'].some(g => g.name === selector)) {
3250
this.config['proxy-groups'].push({
@@ -38,76 +56,102 @@ export class ClashConfigBuilder {
3856
});
3957
}
4058

41-
convertToClashProxy(proxy) {
42-
// Convert sing-box proxy format to Clash format
43-
switch(proxy.type) {
44-
case 'shadowsocks':
45-
return {
46-
name: proxy.tag,
47-
type: 'ss',
48-
server: proxy.server,
49-
port: proxy.server_port,
50-
cipher: proxy.method,
51-
password: proxy.password
52-
};
53-
case 'vmess':
54-
return {
55-
name: proxy.tag,
56-
type: proxy.type,
57-
server: proxy.server,
58-
port: proxy.server_port,
59-
uuid: proxy.uuid,
60-
alterId: proxy.alter_id,
61-
cipher: proxy.security,
62-
tls: proxy.tls?.enabled || false,
63-
servername: proxy.tls?.server_name || '',
64-
network: proxy.transport?.type || 'tcp',
65-
'ws-opts': proxy.transport?.type === 'ws' ? {
66-
path: proxy.transport.path,
67-
headers: proxy.transport.headers
68-
} : undefined
69-
};
70-
case 'vless':
71-
return {
72-
name: proxy.tag,
73-
type: proxy.type,
74-
server: proxy.server,
75-
port: proxy.server_port,
76-
uuid: proxy.uuid,
77-
cipher: proxy.security,
78-
tls: proxy.tls?.enabled || false,
79-
'client-fingerprint':proxy.tls.utls?.fingerprint,
80-
servername: proxy.tls?.server_name || '',
81-
network: proxy.transport?.type || 'tcp',
82-
'ws-opts': proxy.transport?.type === 'ws' ? {
83-
path: proxy.transport.path,
84-
headers: proxy.transport.headers
85-
}: undefined,
86-
'reality-opts': proxy.tls.reality?.enabled ? {
87-
'public-key': proxy.tls.reality.public_key,
88-
'short-id': proxy.tls.reality.short_id,
89-
} : undefined,
59+
convertToClashProxy(proxy) {
60+
switch(proxy.type) {
61+
case 'shadowsocks':
62+
return {
63+
name: proxy.tag,
64+
type: 'ss',
65+
server: proxy.server,
66+
port: proxy.server_port,
67+
cipher: proxy.method,
68+
password: proxy.password
69+
};
70+
case 'vmess':
71+
return {
72+
name: proxy.tag,
73+
type: proxy.type,
74+
server: proxy.server,
75+
port: proxy.server_port,
76+
uuid: proxy.uuid,
77+
alterId: proxy.alter_id,
78+
cipher: proxy.security,
79+
tls: proxy.tls?.enabled || false,
80+
servername: proxy.tls?.server_name || '',
81+
network: proxy.transport?.type || 'tcp',
82+
'ws-opts': proxy.transport?.type === 'ws' ? {
83+
path: proxy.transport.path,
84+
headers: proxy.transport.headers
85+
} : undefined
86+
};
87+
case 'vless':
88+
return {
89+
name: proxy.tag,
90+
type: proxy.type,
91+
server: proxy.server,
92+
port: proxy.server_port,
93+
uuid: proxy.uuid,
94+
cipher: proxy.security,
95+
tls: proxy.tls?.enabled || false,
96+
'client-fingerprint': proxy.tls.utls?.fingerprint,
97+
servername: proxy.tls?.server_name || '',
98+
network: proxy.transport?.type || 'tcp',
99+
'ws-opts': proxy.transport?.type === 'ws' ? {
100+
path: proxy.transport.path,
101+
headers: proxy.transport.headers
102+
}: undefined,
103+
'reality-opts': proxy.tls.reality?.enabled ? {
104+
'public-key': proxy.tls.reality.public_key,
105+
'short-id': proxy.tls.reality.short_id,
106+
} : undefined,
90107
'grpc-opts': proxy.transport?.type === 'grpc' ? {
91108
'grpc-mode': 'gun',
92109
'grpc-service-name': proxy.transport.service_name,
93110
} : undefined,
94111
tfo : proxy.tcp_fast_open,
95112
'skip-cert-verify': proxy.tls.insecure,
96113
'flow': proxy.flow ?? undefined,
97-
};
98-
case 'hysteria2':
114+
};
115+
case 'hysteria2':
116+
return {
117+
name: proxy.tag,
118+
type: proxy.type,
119+
server: proxy.server,
120+
port: proxy.server_port,
121+
password: proxy.password,
122+
auth: proxy.password,
123+
'skip-cert-verify': proxy.tls.insecure,
124+
};
125+
case 'trojan':
99126
return {
100-
name: proxy.tag,
101-
type: proxy.type,
102-
server: proxy.server,
103-
port: proxy.server_port,
104-
password: proxy.password,
105-
auth: proxy.password,
106-
'skip-cert-verify': proxy.tls.insecure,
107-
};
108-
109-
default:
110-
return proxy; // Return as-is if no specific conversion is defined
111-
}
112-
}
113-
}
127+
name: proxy.tag,
128+
type: proxy.type,
129+
server: proxy.server,
130+
port: proxy.server_port,
131+
password: proxy.password,
132+
cipher: proxy.security,
133+
tls: proxy.tls?.enabled || false,
134+
'client-fingerprint': proxy.tls.utls?.fingerprint,
135+
servername: proxy.tls?.server_name || '',
136+
network: proxy.transport?.type || 'tcp',
137+
'ws-opts': proxy.transport?.type === 'ws' ? {
138+
path: proxy.transport.path,
139+
headers: proxy.transport.headers
140+
}: undefined,
141+
'reality-opts': proxy.tls.reality?.enabled ? {
142+
'public-key': proxy.tls.reality.public_key,
143+
'short-id': proxy.tls.reality.short_id,
144+
} : undefined,
145+
'grpc-opts': proxy.transport?.type === 'grpc' ? {
146+
'grpc-mode': 'gun',
147+
'grpc-service-name': proxy.transport.service_name,
148+
} : undefined,
149+
tfo : proxy.tcp_fast_open,
150+
'skip-cert-verify': proxy.tls.insecure,
151+
'flow': proxy.flow ?? undefined,
152+
}
153+
default:
154+
return proxy; // Return as-is if no specific conversion is defined
155+
}
156+
}
157+
}

src/ProxyParsers.js

+51-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { parseServerInfo, parseUrlParams, createTlsConfig, createTransportConfig } from './utils.js';
1+
import { parseServerInfo, parseUrlParams, createTlsConfig, createTransportConfig, decodeBase64 } from './utils.js';
22

33

44
export class ProxyParser {
@@ -8,7 +8,9 @@ export class ProxyParser {
88
case 'ss': return new ShadowsocksParser().parse(url);
99
case 'vmess': return new VmessParser().parse(url);
1010
case 'vless': return new VlessParser().parse(url);
11-
case 'hysteria2': return new Hysteria2Parser().parse(url);
11+
case 'hysteria2': return new Hysteria2Parser().parse(url);
12+
case 'http' || 'https': return HttpParser.parse(url);
13+
case 'trojan': return new TrojanParser().parse(url);
1214
}
1315
}
1416
}
@@ -132,3 +134,50 @@ export class ProxyParser {
132134
};
133135
}
134136
}
137+
138+
class TrojanParser {
139+
parse(url) {
140+
// trojan://8jxfHkRdEV@diylink.qhr.icu:31190?security=reality&sni=yahoo.com&fp=random&pbk=KlaUGzBvKlBX0GqI7hCPioRZvDuLP3O5wozg-L8nCiw&sid=a9d30b07&spx=%2F&type=tcp&headerType=none#trojan-test-nuu4xgxm
141+
const { addressPart, params, name } = parseUrlParams(url);
142+
const [password, serverInfo] = addressPart.split('@');
143+
const { host, port } = parseServerInfo(serverInfo);
144+
145+
const parsedURL = parseServerInfo(addressPart);
146+
const tls = createTlsConfig(params);
147+
const transport = params.type !== 'tcp' ? createTransportConfig(params) : undefined;
148+
return {
149+
type: 'trojan',
150+
tag: name,
151+
server: host,
152+
server_port: port,
153+
password: password || parsedURL.username,
154+
network: "tcp",
155+
tcp_fast_open: false,
156+
tls: tls,
157+
transport: transport,
158+
flow: params.flow ?? undefined
159+
};
160+
}
161+
}
162+
//
163+
class HttpParser {
164+
static async parse(url) {
165+
try {
166+
const response = await fetch(url);
167+
if (!response.ok) {
168+
throw new Error(`HTTP error! status: ${response.status}`);
169+
}
170+
const text = await response.text();
171+
let decodedText;
172+
try {
173+
decodedText = decodeBase64(text.trim());
174+
} catch (e) {
175+
decodedText = text;
176+
}
177+
return decodedText.split('\n').filter(line => line.trim() !== '');
178+
} catch (error) {
179+
console.error('Error fetching or parsing HTTP(S) content:', error);
180+
return null;
181+
}
182+
}
183+
}

0 commit comments

Comments
 (0)