Skip to content

Commit 65fe28d

Browse files
committed
fix: double encode base64 in xray links #232
1 parent c8e190d commit 65fe28d

3 files changed

Lines changed: 89 additions & 50 deletions

File tree

src/BaseConfigBuilder.js

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ProxyParser } from './ProxyParsers.js';
2-
import { DeepCopy, decodeBase64 } from './utils.js';
2+
import { DeepCopy, tryDecodeSubscriptionLines } from './utils.js';
33
import { t, setLanguage } from './i18n/index.js';
44
import { generateRules, getOutbounds, PREDEFINED_RULE_SETS } from './config.js';
55

@@ -26,7 +26,7 @@ export class BaseConfigBuilder {
2626

2727
for (const url of urls) {
2828
// Try to decode if it might be base64
29-
let processedUrls = this.tryDecodeBase64(url);
29+
let processedUrls = tryDecodeSubscriptionLines(url);
3030

3131
// Handle single URL or array of URLs
3232
if(!Array.isArray(processedUrls)){
@@ -52,37 +52,6 @@ export class BaseConfigBuilder {
5252
return parsedItems;
5353
}
5454

55-
tryDecodeBase64(str) {
56-
// If the string already has a protocol prefix, return as is
57-
if (str.includes('://')) {
58-
return str;
59-
}
60-
61-
try {
62-
// Try to decode as base64
63-
const decoded = decodeBase64(str);
64-
65-
// Check if decoded content contains multiple links
66-
if (decoded.includes('\n')) {
67-
// Split by newline and filter out empty lines
68-
const multipleUrls = decoded.split('\n').filter(url => url.trim() !== '');
69-
70-
// Check if at least one URL is valid
71-
if (multipleUrls.some(url => url.includes('://'))) {
72-
return multipleUrls;
73-
}
74-
}
75-
76-
// Check if the decoded string looks like a valid URL
77-
if (decoded.includes('://')) {
78-
return decoded;
79-
}
80-
} catch (e) {
81-
// If decoding fails, return original string
82-
}
83-
return str;
84-
}
85-
8655
getOutboundsList() {
8756
let outbounds;
8857
if (typeof this.selectedRules === 'string' && PREDEFINED_RULE_SETS[this.selectedRules]) {

src/index.js

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { SingboxConfigBuilder } from './SingboxConfigBuilder.js';
22
import { generateHtml } from './htmlBuilder.js';
33
import { ClashConfigBuilder } from './ClashConfigBuilder.js';
44
import { SurgeConfigBuilder } from './SurgeConfigBuilder.js';
5-
import { decodeBase64, encodeBase64, GenerateWebPath } from './utils.js';
5+
import { encodeBase64, GenerateWebPath, tryDecodeSubscriptionLines } from './utils.js';
66
import { PREDEFINED_RULE_SETS } from './config.js';
77
import { t, setLanguage } from './i18n/index.js';
88
import yaml from 'js-yaml';
@@ -156,38 +156,48 @@ async function handleRequest(request) {
156156
} else if (url.pathname.startsWith('/xray')) {
157157
// Handle Xray config requests
158158
const inputString = url.searchParams.get('config');
159-
const proxylist = inputString.split('\n');
159+
if (!inputString) {
160+
return new Response('Missing config parameter', { status: 400 });
161+
}
160162

163+
const proxylist = inputString.split('\n');
161164
const finalProxyList = [];
162165
// Use custom UserAgent (for Xray) Hmmm...
163166
let userAgent = url.searchParams.get('ua');
164167
if (!userAgent) {
165168
userAgent = 'curl/7.74.0';
166169
}
167-
let headers = new Headers({
168-
"User-Agent" : userAgent
170+
const headers = new Headers({
171+
'User-Agent': userAgent
169172
});
170173

171174
for (const proxy of proxylist) {
172-
if (proxy.startsWith('http://') || proxy.startsWith('https://')) {
175+
const trimmedProxy = proxy.trim();
176+
if (!trimmedProxy) {
177+
continue;
178+
}
179+
180+
if (trimmedProxy.startsWith('http://') || trimmedProxy.startsWith('https://')) {
173181
try {
174-
const response = await fetch(proxy, {
175-
method : 'GET',
176-
headers : headers
177-
})
182+
const response = await fetch(trimmedProxy, {
183+
method: 'GET',
184+
headers
185+
});
178186
const text = await response.text();
179-
let decodedText;
180-
decodedText = decodeBase64(text.trim());
181-
// Check if the decoded text needs URL decoding
182-
if (decodedText.includes('%')) {
183-
decodedText = decodeURIComponent(decodedText);
187+
let processed = tryDecodeSubscriptionLines(text, { decodeUriComponent: true });
188+
if (!Array.isArray(processed)) {
189+
processed = [processed];
184190
}
185-
finalProxyList.push(...decodedText.split('\n'));
191+
finalProxyList.push(...processed.filter(item => typeof item === 'string' && item.trim() !== ''));
186192
} catch (e) {
187193
console.warn('Failed to fetch the proxy:', e);
188194
}
189195
} else {
190-
finalProxyList.push(proxy);
196+
let processed = tryDecodeSubscriptionLines(trimmedProxy);
197+
if (!Array.isArray(processed)) {
198+
processed = [processed];
199+
}
200+
finalProxyList.push(...processed.filter(item => typeof item === 'string' && item.trim() !== ''));
191201
}
192202
}
193203

@@ -292,4 +302,4 @@ async function handleRequest(request) {
292302
console.error('Error processing request:', error);
293303
return new Response(t('internalError'), { status: 500 });
294304
}
295-
}
305+
}

src/utils.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,66 @@ export function base64ToBinary(base64String) {
8989

9090
return binaryString;
9191
}
92+
93+
export function tryDecodeSubscriptionLines(input, { decodeUriComponent = false } = {}) {
94+
if (typeof input !== 'string') {
95+
return input;
96+
}
97+
98+
const trimmed = input.trim();
99+
if (trimmed === '') {
100+
return trimmed;
101+
}
102+
103+
const splitIfMultiple = (value) => {
104+
if (typeof value !== 'string') {
105+
return value;
106+
}
107+
108+
const normalized = value.replace(/\r\n/g, '\n');
109+
const segments = normalized
110+
.split('\n')
111+
.map(segment => segment.trim())
112+
.filter(segment => segment !== '');
113+
114+
if (segments.length > 1 && segments.some(segment => segment.includes('://'))) {
115+
return segments;
116+
}
117+
118+
return normalized.trim();
119+
};
120+
121+
const directResult = splitIfMultiple(trimmed);
122+
if (Array.isArray(directResult)) {
123+
return directResult;
124+
}
125+
if (typeof directResult === 'string' && directResult.includes('://')) {
126+
return directResult;
127+
}
128+
129+
try {
130+
let decoded = decodeBase64(trimmed);
131+
if (decodeUriComponent && decoded.includes('%')) {
132+
try {
133+
decoded = decodeURIComponent(decoded);
134+
} catch (_) {
135+
// ignore URI decode errors and fall back to the decoded string
136+
}
137+
}
138+
139+
const decodedResult = splitIfMultiple(decoded);
140+
if (Array.isArray(decodedResult)) {
141+
return decodedResult;
142+
}
143+
if (typeof decodedResult === 'string' && decodedResult.includes('://')) {
144+
return decodedResult;
145+
}
146+
} catch (_) {
147+
// ignore decoding errors and return the original trimmed input
148+
}
149+
150+
return trimmed;
151+
}
92152
export function DeepCopy(obj) {
93153
if (obj === null || typeof obj !== 'object') {
94154
return obj;

0 commit comments

Comments
 (0)