UNPKG

vless-to-xray

Version:

Convert VLESS subscription URLs to Xray configuration

367 lines (314 loc) 10.5 kB
#!/usr/bin/env node 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); }); }