vless-to-xray
Version:
Convert VLESS subscription URLs to Xray configuration
367 lines (314 loc) • 10.5 kB
JavaScript
import fs from "fs";
import { fileURLToPath } from "url";
/**
* Парсит строку конфигурации VLESS и возвращает объект с параметрами.
* @param {string} vlessString - Строка для парсинга, начинающаяся с "vless://".
* @returns {object|null} - Объект с параметрами или null в случае ошибки.
*/
export function parseVlessUrl(vlessString) {
if (!vlessString || !vlessString.startsWith("vless://")) {
console.error(
'Неверный формат строки. Она должна начинаться с "vless://".'
);
return null;
}
try {
// 1. Заменяем нестандартный протокол "vless://" на "dummy://", чтобы класс URL мог его обработать.
const parsableString = vlessString.replace("vless://", "dummy://");
const url = new URL(parsableString);
// 2. Извлекаем основные части из URL
const uuid = url.username; // UUID находится в поле "username"
const address = url.hostname; // Адрес сервера
const port = url.port; // Порт
// 3. Извлекаем все query-параметры (?...) в виде объекта
const queryParams = Object.fromEntries(url.searchParams.entries());
// 4. Извлекаем название/заметку из "хэша" (#...) и декодируем его
const remark = url.hash ? decodeURIComponent(url.hash.substring(1)) : "";
// 5. Собираем все в один результирующий объект
const result = {
protocol: "vless",
uuid,
address,
port,
params: queryParams,
remark,
};
return result;
} catch (error) {
console.error(`Ошибка при парсинге строки: ${error.message}`);
return null;
}
}
/**
* Make HTTP request using fetch and return response data
*/
async function fetchData(url) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
const response = await fetch(url, {
method: "GET",
signal: controller.signal,
headers: {
"User-Agent": "vless-to-xray/1.0.0",
},
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.text();
return data;
} catch (error) {
if (error.name === "AbortError") {
throw new Error("Request timeout");
}
throw error;
}
}
/**
* Generate clean tag name from server remark
*/
function generateTagFromRemark(remark, fallback) {
if (!remark) return fallback;
// Remove emojis and special characters, keep only alphanumeric and hyphens
return (
remark
.replace(
/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu,
""
) // Remove emojis
.replace(/[^a-zA-Z0-9\-\s]/g, "") // Remove special characters except hyphens and spaces
.trim()
.replace(/\s+/g, "-") // Replace spaces with hyphens
.toLowerCase()
.substring(0, 30) || fallback
); // Limit length and fallback if empty
}
/**
* Display servers table in console
*/
function displayServersTable(vlessConfigs) {
console.log(
`\n✅ Generated Xray configuration with ${vlessConfigs.length} servers:\n`
);
// Table header
console.log("Port Country/Server Address");
console.log("─".repeat(60));
// Table rows
vlessConfigs.forEach((config, index) => {
const port = 11081 + index;
const server = config.remark || `Server-${index + 1}`;
const address = `${config.address}:${config.port}`;
console.log(`${port.toString().padEnd(8)}${server.padEnd(28)}${address}`);
});
console.log("\n💡 Use these HTTP proxies in your apps:");
vlessConfigs.forEach((config, index) => {
const port = 11081 + index;
const server = config.remark || `Server-${index + 1}`;
console.log(` ${server}: 127.0.0.1:${port}`);
});
console.log("");
}
/**
* Generate Xray configuration from parsed VLESS data
*/
function generateXrayConfig(vlessConfigs) {
// Filter out configs without reality settings
const realityConfigs = vlessConfigs.filter(
(config) => config.params.security === "reality"
);
if (realityConfigs.length === 0) {
console.log(
"No configurations with reality settings found. No proxies created."
);
return {
log: { loglevel: "warning" },
inbounds: [],
outbounds: [],
routing: { rules: [] },
};
}
const config = {
log: {
loglevel: "warning",
},
inbounds: [],
outbounds: [],
routing: {
rules: [],
},
};
// Generate inbounds (HTTP proxy ports starting from 11081)
realityConfigs.forEach((vlessConfig, index) => {
const port = 11081 + index;
const tag = generateTagFromRemark(
vlessConfig.remark,
`server-${index + 1}`
);
config.inbounds.push({
listen: "0.0.0.0",
port: port,
protocol: "http",
tag: `http-${tag}`,
});
});
// Generate outbounds from VLESS configs
realityConfigs.forEach((vlessConfig, index) => {
const tag = generateTagFromRemark(
vlessConfig.remark,
`server-${index + 1}`
);
const outbound = {
protocol: "vless",
settings: {
vnext: [
{
address: vlessConfig.address,
port: parseInt(vlessConfig.port),
users: [
{
id: vlessConfig.uuid,
flow: vlessConfig.params.flow || "xtls-rprx-vision",
encryption: "none",
},
],
},
],
},
streamSettings: {
network: vlessConfig.params.type || "tcp",
security: vlessConfig.params.security || "reality",
},
tag: `proxy-${tag}`,
};
// Add reality settings if security is reality
if (vlessConfig.params.security === "reality") {
outbound.streamSettings.realitySettings = {
fingerprint: vlessConfig.params.fp || "chrome",
serverName: vlessConfig.params.sni || vlessConfig.address,
publicKey: vlessConfig.params.pbk || "",
shortId: vlessConfig.params.sid || "",
spiderX: vlessConfig.params.spx || "",
};
}
config.outbounds.push(outbound);
});
// Generate routing rules
realityConfigs.forEach((vlessConfig, index) => {
const tag = generateTagFromRemark(
vlessConfig.remark,
`server-${index + 1}`
);
config.routing.rules.push({
type: "field",
inboundTag: [`http-${tag}`],
outboundTag: `proxy-${tag}`,
});
});
return config;
}
/**
* Main CLI function
*/
async function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
console.log("Usage: vless-to-xray <subscription-url> [output-file]");
console.log("");
console.log("Arguments:");
console.log(" subscription-url URL to fetch VLESS subscription data");
console.log(
" output-file Optional: path to save Xray config (prints to console if omitted)"
);
console.log("");
console.log("Example:");
console.log(" vless-to-xray https://vless.com/sub/data ./config.json");
console.log(
" vless-to-xray https://vless.com/sub/data # prints to console"
);
process.exit(args.length === 0 ? 1 : 0);
}
const subscriptionUrl = args[0];
const outputFile = args[1] || "xray-config.json";
try {
// Validate URL
try {
new URL(subscriptionUrl);
} catch (err) {
throw new Error(`Invalid URL: ${subscriptionUrl}`);
}
console.log(`Fetching subscription from: ${subscriptionUrl}`);
// Fetch data from URL
const rawData = await fetchData(subscriptionUrl);
if (!rawData || rawData.trim() === "") {
throw new Error("Empty response from subscription URL");
}
console.log("Decoding base64 data...");
// Decode base64
let decodedData;
try {
decodedData = Buffer.from(rawData.trim(), "base64").toString("utf-8");
} catch (err) {
throw new Error("Failed to decode base64 data");
}
// Split into lines and filter empty ones
const vlessUrls = decodedData
.split("\n")
.filter((line) => line.trim() !== "");
if (vlessUrls.length === 0) {
throw new Error("No VLESS URLs found in decoded data");
}
console.log(`Found ${vlessUrls.length} VLESS URLs`);
// Parse each VLESS URL
const parsedConfigs = [];
vlessUrls.forEach((url, index) => {
const parsed = parseVlessUrl(url.trim());
if (parsed) {
parsedConfigs.push(parsed);
console.log(
`Parsed config ${index + 1}: ${parsed.remark || parsed.address}`
);
} else {
console.warn(
`Failed to parse URL ${index + 1}: ${url.substring(0, 50)}...`
);
}
});
if (parsedConfigs.length === 0) {
throw new Error("No valid VLESS configurations found");
}
console.log(`Successfully parsed ${parsedConfigs.length} configurations`);
// Generate Xray configuration
const xrayConfig = generateXrayConfig(parsedConfigs);
const configJson = JSON.stringify(xrayConfig, null, 2);
// Display servers table
displayServersTable(parsedConfigs);
// Output result
if (outputFile) {
fs.writeFileSync(outputFile, configJson, "utf-8");
console.log(`📄 Xray configuration saved to: ${outputFile}`);
} else {
console.log("\n--- Xray Configuration ---");
console.log(configJson);
}
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
}
// Check if this module is being run directly as CLI
const currentFile = fileURLToPath(import.meta.url);
// For ES modules, we need to check if the resolved path matches
const isMainModule =
process.argv[1] === currentFile ||
process.argv[1]?.endsWith("index.js") ||
process.argv[1]?.endsWith("vless-to-xray");
if (isMainModule) {
// Run CLI if this is the main module
main().catch((error) => {
console.error(`Unexpected error: ${error.message}`);
process.exit(1);
});
}