在浏览器端实现vless和vmess客户端可行吗 #4769
Replies: 4 comments 1 reply
-
|
不行。除非你写一个出来康康。 |
Beta Was this translation helpful? Give feedback.
-
|
可行。 服务器端:https://github.com/zizifn/edgetunnel 客户端(Manus 写的): /**
* VLESS 浏览器端代理客户端
* 基于 WebSocket 实现的 VLESS 协议客户端
*/
// 常量定义
const VERSION = 0; // VLESS 协议版本
const CMD_TCP = 1; // TCP 命令
const CMD_UDP = 2; // UDP 命令
const ADDR_TYPE_IPV4 = 1; // IPv4 地址类型
const ADDR_TYPE_DOMAIN = 2; // 域名地址类型
const ADDR_TYPE_IPV6 = 3; // IPv6 地址类型
/**
* 传输层 - WebSocket 通信
*/
class WebSocketTransport {
/**
* 创建 WebSocket 传输实例
* @param {string} serverUrl - 服务器 WebSocket URL
* @param {Object} options - 配置选项
*/
constructor(serverUrl, options = {}) {
this.serverUrl = serverUrl;
this.options = options;
this.ws = null;
this.onMessage = null;
this.onOpen = null;
this.onClose = null;
this.onError = null;
this.connected = false;
this.connecting = false;
this.earlyData = options.earlyData || null;
}
/**
* 建立 WebSocket 连接
* @returns {Promise} 连接建立的 Promise
*/
connect() {
if (this.connected || this.connecting) {
return Promise.reject(new Error('WebSocket 已连接或正在连接中'));
}
this.connecting = true;
return new Promise((resolve, reject) => {
try {
// 如果有早期数据,使用 sec-websocket-protocol 头传递
const protocols = this.earlyData ? [this.earlyData] : undefined;
this.ws = new WebSocket(this.serverUrl, protocols);
this.ws.binaryType = 'arraybuffer';
this.ws.onopen = (event) => {
this.connected = true;
this.connecting = false;
if (this.onOpen) this.onOpen(event);
resolve(event);
};
this.ws.onmessage = (event) => {
if (this.onMessage) this.onMessage(event.data);
};
this.ws.onclose = (event) => {
this.connected = false;
this.connecting = false;
if (this.onClose) this.onClose(event);
};
this.ws.onerror = (event) => {
this.connecting = false;
if (this.onError) this.onError(event);
reject(event);
};
} catch (error) {
this.connecting = false;
reject(error);
}
});
}
/**
* 发送数据
* @param {ArrayBuffer} data - 要发送的二进制数据
* @returns {boolean} 是否成功发送
*/
send(data) {
if (!this.connected || !this.ws) {
return false;
}
try {
this.ws.send(data);
return true;
} catch (error) {
console.error('WebSocket 发送数据失败:', error);
return false;
}
}
/**
* 关闭 WebSocket 连接
*/
close() {
if (this.ws) {
try {
this.ws.close();
} catch (error) {
console.error('WebSocket 关闭失败:', error);
}
this.ws = null;
}
this.connected = false;
this.connecting = false;
}
}
/**
* 协议层 - VLESS 协议实现
*/
class VLESSProtocol {
/**
* 创建 VLESS 协议实例
* @param {string} userID - 用户 UUID
*/
constructor(userID) {
this.userID = userID;
this.version = new Uint8Array([VERSION]);
}
/**
* 将 UUID 字符串转换为字节数组
* @param {string} uuid - UUID 字符串
* @returns {Uint8Array} UUID 字节数组
*/
static parseUUID(uuid) {
uuid = uuid.replace(/-/g, '');
const bytes = new Uint8Array(16);
for (let i = 0; i < 16; i++) {
bytes[i] = parseInt(uuid.substr(i * 2, 2), 16);
}
return bytes;
}
/**
* 创建 VLESS 请求头
* @param {number} addressType - 地址类型
* @param {string} address - 目标地址
* @param {number} port - 目标端口
* @param {number} command - 命令类型
* @returns {ArrayBuffer} VLESS 请求头
*/
createHeader(addressType, address, port, command = CMD_TCP) {
// 计算地址部分的长度
let addressLength = 0;
let addressValue;
switch (addressType) {
case ADDR_TYPE_IPV4:
addressLength = 4;
addressValue = new Uint8Array(addressLength);
address.split('.').forEach((part, index) => {
addressValue[index] = parseInt(part, 10);
});
break;
case ADDR_TYPE_DOMAIN:
addressLength = address.length;
addressValue = new TextEncoder().encode(address);
break;
case ADDR_TYPE_IPV6:
addressLength = 16;
addressValue = new Uint8Array(addressLength);
// 简化处理,实际应用中需要正确解析 IPv6
break;
default:
throw new Error(`不支持的地址类型: ${addressType}`);
}
// 计算头部总长度
// 版本(1) + UUID(16) + 附加信息长度(1) + 命令(1) + 端口(2) + 地址类型(1) + 地址值(变长)
const headerLength = 1 + 16 + 1 + 1 + 2 + 1 + (addressType === ADDR_TYPE_DOMAIN ? 1 : 0) + addressLength;
// 创建头部缓冲区
const header = new ArrayBuffer(headerLength);
const headerView = new DataView(header);
const headerBytes = new Uint8Array(header);
let offset = 0;
// 写入版本
headerBytes[offset++] = this.version[0];
// 写入 UUID
const uuidBytes = VLESSProtocol.parseUUID(this.userID);
headerBytes.set(uuidBytes, offset);
offset += 16;
// 写入附加信息长度(当前为0)
headerBytes[offset++] = 0;
// 写入命令
headerBytes[offset++] = command;
// 写入端口(大端序)
headerView.setUint16(offset, port, false);
offset += 2;
// 写入地址类型
headerBytes[offset++] = addressType;
// 写入地址
if (addressType === ADDR_TYPE_DOMAIN) {
// 域名需要先写入长度
headerBytes[offset++] = addressLength;
}
headerBytes.set(addressValue, offset);
return header;
}
/**
* 解析 VLESS 响应
* @param {ArrayBuffer} data - 响应数据
* @returns {Object} 解析结果
*/
parseResponse(data) {
const dataView = new DataView(data);
const version = dataView.getUint8(0);
const optLength = dataView.getUint8(1);
// 响应头长度 = 版本(1) + 附加信息长度(1) + 附加信息(变长)
const headerLength = 2 + optLength;
return {
version,
optLength,
headerLength,
data: data.slice(headerLength)
};
}
/**
* 将请求头和数据合并
* @param {ArrayBuffer} header - VLESS 请求头
* @param {ArrayBuffer} data - 请求数据
* @returns {ArrayBuffer} 合并后的数据
*/
static mergeHeaderAndData(header, data) {
const merged = new Uint8Array(header.byteLength + data.byteLength);
merged.set(new Uint8Array(header), 0);
merged.set(new Uint8Array(data), header.byteLength);
return merged.buffer;
}
}
/**
* 代理控制层 - 管理连接和数据流
*/
class ProxyController {
/**
* 创建代理控制器实例
* @param {Object} config - 配置信息
*/
constructor(config) {
this.config = config;
this.transport = null;
this.protocol = null;
this.status = 'disconnected';
this.retryCount = 0;
this.maxRetries = config.maxRetries || 3;
this.retryDelay = config.retryDelay || 1000;
this.onStatusChange = null;
this.onData = null;
this.onError = null;
}
/**
* 初始化代理控制器
*/
init() {
// 创建协议实例
this.protocol = new VLESSProtocol(this.config.userID);
// 创建传输实例
const wsUrl = this.config.secure ? `wss://${this.config.server}` : `ws://${this.config.server}`;
this.transport = new WebSocketTransport(wsUrl, {
earlyData: this.config.earlyData
});
// 设置事件处理
this.transport.onOpen = () => this._handleConnectionOpen();
this.transport.onMessage = (data) => this._handleMessage(data);
this.transport.onClose = () => this._handleConnectionClose();
this.transport.onError = (error) => this._handleError(error);
}
/**
* 连接到代理服务器
* @returns {Promise} 连接结果
*/
connect() {
if (!this.transport) {
this.init();
}
this._updateStatus('connecting');
return this.transport.connect()
.catch(error => {
this._handleError(error);
return Promise.reject(error);
});
}
/**
* 发送代理请求
* @param {number} addressType - 地址类型
* @param {string} address - 目标地址
* @param {number} port - 目标端口
* @param {ArrayBuffer} data - 请求数据
* @returns {boolean} 是否成功发送
*/
sendRequest(addressType, address, port, data) {
if (this.status !== 'connected') {
this._handleError(new Error('代理未连接'));
return false;
}
try {
// 创建 VLESS 请求头
const header = this.protocol.createHeader(addressType, address, port);
// 合并头部和数据
const mergedData = VLESSProtocol.mergeHeaderAndData(header, data);
// 发送数据
return this.transport.send(mergedData);
} catch (error) {
this._handleError(error);
return false;
}
}
/**
* 处理连接打开事件
* @private
*/
_handleConnectionOpen() {
this._updateStatus('connected');
this.retryCount = 0;
}
/**
* 处理收到消息事件
* @param {ArrayBuffer} data - 收到的数据
* @private
*/
_handleMessage(data) {
try {
// 解析 VLESS 响应
const response = this.protocol.parseResponse(data);
// 处理响应数据
if (this.onData) {
this.onData(response.data);
}
} catch (error) {
this._handleError(error);
}
}
/**
* 处理连接关闭事件
* @private
*/
_handleConnectionClose() {
const wasConnected = this.status === 'connected';
this._updateStatus('disconnected');
// 如果之前是连接状态,尝试重连
if (wasConnected && this.config.autoReconnect && this.retryCount < this.maxRetries) {
this.retryCount++;
const delay = this.retryDelay * Math.pow(2, this.retryCount - 1);
setTimeout(() => {
this.connect().catch(() => {
// 重连失败,已在 connect 中处理
});
}, delay);
}
}
/**
* 处理错误事件
* @param {Error} error - 错误对象
* @private
*/
_handleError(error) {
this._updateStatus('error');
if (this.onError) {
this.onError(error);
}
console.error('代理错误:', error);
}
/**
* 更新状态
* @param {string} status - 新状态
* @private
*/
_updateStatus(status) {
this.status = status;
if (this.onStatusChange) {
this.onStatusChange(status);
}
}
/**
* 断开连接
*/
disconnect() {
if (this.transport) {
this.transport.close();
}
this._updateStatus('disconnected');
}
}
/**
* 用户界面层 - 提供用户交互界面
*/
class ProxyUI {
/**
* 创建用户界面实例
* @param {ProxyController} controller - 代理控制器
* @param {Object} options - 配置选项
*/
constructor(controller, options = {}) {
this.controller = controller;
this.options = options;
this.elements = {};
this.isInitialized = false;
}
/**
* 初始化用户界面
* @param {string} containerId - 容器元素 ID
*/
init(containerId) {
if (this.isInitialized) {
return;
}
const container = document.getElementById(containerId);
if (!container) {
throw new Error(`找不到容器元素: ${containerId}`);
}
// 创建 UI 元素
this._createElements(container);
// 绑定事件
this._bindEvents();
// 设置控制器事件处理
this.controller.onStatusChange = (status) => this._updateStatus(status);
this.controller.onError = (error) => this._showError(error);
this.isInitialized = true;
}
/**
* 创建 UI 元素
* @param {HTMLElement} container - 容器元素
* @private
*/
_createElements(container) {
// 清空容器
container.innerHTML = '';
// 创建标题
const title = document.createElement('h2');
title.textContent = 'VLESS 浏览器代理客户端';
container.appendChild(title);
// 创建配置区域
const configSection = document.createElement('div');
configSection.className = 'config-section';
// 服务器地址
const serverGroup = this._createFormGroup('server', '服务器地址:', 'text', this.controller.config.server || '');
configSection.appendChild(serverGroup);
// 用户 ID
const userIdGroup = this._createFormGroup('userId', '用户 ID:', 'text', this.controller.config.userID || '');
configSection.appendChild(userIdGroup);
// 安全连接
const secureGroup = this._createCheckboxGroup('secure', '使用安全连接 (WSS):', this.controller.config.secure !== false);
configSection.appendChild(secureGroup);
// 自动重连
const autoReconnectGroup = this._createCheckboxGroup('autoReconnect', '自动重连:', this.controller.config.autoReconnect !== false);
configSection.appendChild(autoReconnectGroup);
container.appendChild(configSection);
// 创建控制区域
const controlSection = document.createElement('div');
controlSection.className = 'control-section';
// 连接按钮
const connectBtn = document.createElement('button');
connectBtn.id = 'connectBtn';
connectBtn.textContent = '连接';
controlSection.appendChild(connectBtn);
// 断开按钮
const disconnectBtn = document.createElement('button');
disconnectBtn.id = 'disconnectBtn';
disconnectBtn.textContent = '断开';
disconnectBtn.disabled = true;
controlSection.appendChild(disconnectBtn);
// 测试按钮
const testBtn = document.createElement('button');
testBtn.id = 'testBtn';
testBtn.textContent = '测试连接';
testBtn.disabled = true;
controlSection.appendChild(testBtn);
container.appendChild(controlSection);
// 创建状态区域
const statusSection = document.createElement('div');
statusSection.className = 'status-section';
// 状态显示
const statusDisplay = document.createElement('div');
statusDisplay.id = 'statusDisplay';
statusDisplay.className = 'status-disconnected';
statusDisplay.textContent = '未连接';
statusSection.appendChild(statusDisplay);
// 错误信息
const errorDisplay = document.createElement('div');
errorDisplay.id = 'errorDisplay';
errorDisplay.className = 'error-display';
errorDisplay.style.display = 'none';
statusSection.appendChild(errorDisplay);
container.appendChild(statusSection);
// 创建日志区域
const logSection = document.createElement('div');
logSection.className = 'log-section';
// 日志标题
const logTitle = document.createElement('h3');
logTitle.textContent = '日志';
logSection.appendChild(logTitle);
// 日志内容
const logContent = document.createElement('div');
logContent.id = 'logContent';
logContent.className = 'log-content';
logSection.appendChild(logContent);
// 清除日志按钮
const clearLogBtn = document.createElement('button');
clearLogBtn.id = 'clearLogBtn';
clearLogBtn.textContent = '清除日志';
logSection.appendChild(clearLogBtn);
container.appendChild(logSection);
// 保存元素引用
this.elements = {
server: document.getElementById('server'),
userId: document.getElementById('userId'),
secure: document.getElementById('secure'),
autoReconnect: document.getElementById('autoReconnect'),
connectBtn: document.getElementById('connectBtn'),
disconnectBtn: document.getElementById('disconnectBtn'),
testBtn: document.getElementById('testBtn'),
statusDisplay: document.getElementById('statusDisplay'),
errorDisplay: document.getElementById('errorDisplay'),
logContent: document.getElementById('logContent'),
clearLogBtn: document.getElementById('clearLogBtn')
};
// 添加样式
this._addStyles();
}
/**
* 创建表单组
* @param {string} id - 元素 ID
* @param {string} label - 标签文本
* @param {string} type - 输入类型
* @param {string} value - 初始值
* @returns {HTMLElement} 表单组元素
* @private
*/
_createFormGroup(id, label, type, value) {
const group = document.createElement('div');
group.className = 'form-group';
const labelElement = document.createElement('label');
labelElement.htmlFor = id;
labelElement.textContent = label;
group.appendChild(labelElement);
const input = document.createElement('input');
input.type = type;
input.id = id;
input.value = value;
group.appendChild(input);
return group;
}
/**
* 创建复选框组
* @param {string} id - 元素 ID
* @param {string} label - 标签文本
* @param {boolean} checked - 是否选中
* @returns {HTMLElement} 复选框组元素
* @private
*/
_createCheckboxGroup(id, label, checked) {
const group = document.createElement('div');
group.className = 'form-group checkbox-group';
const input = document.createElement('input');
input.type = 'checkbox';
input.id = id;
input.checked = checked;
group.appendChild(input);
const labelElement = document.createElement('label');
labelElement.htmlFor = id;
labelElement.textContent = label;
group.appendChild(labelElement);
return group;
}
/**
* 添加样式
* @private
*/
_addStyles() {
const style = document.createElement('style');
style.textContent = `
.config-section, .control-section, .status-section, .log-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.form-group {
margin-bottom: 10px;
}
.form-group label {
display: inline-block;
width: 150px;
}
.form-group input[type="text"] {
width: 300px;
padding: 5px;
}
.checkbox-group {
display: flex;
align-items: center;
}
.checkbox-group label {
margin-left: 5px;
}
button {
margin-right: 10px;
padding: 8px 15px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.status-connected {
color: green;
font-weight: bold;
}
.status-disconnected {
color: gray;
}
.status-connecting {
color: blue;
}
.status-error {
color: red;
}
.error-display {
color: red;
margin-top: 10px;
padding: 10px;
background-color: #ffeeee;
border: 1px solid #ffcccc;
border-radius: 4px;
}
.log-content {
height: 200px;
overflow-y: auto;
padding: 10px;
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
margin-bottom: 10px;
}
`;
document.head.appendChild(style);
}
/**
* 绑定事件处理
* @private
*/
_bindEvents() {
// 连接按钮
this.elements.connectBtn.addEventListener('click', () => {
this._updateConfig();
this.controller.connect()
.catch(error => {
// 错误已在控制器中处理
});
});
// 断开按钮
this.elements.disconnectBtn.addEventListener('click', () => {
this.controller.disconnect();
});
// 测试按钮
this.elements.testBtn.addEventListener('click', () => {
this._testConnection();
});
// 清除日志按钮
this.elements.clearLogBtn.addEventListener('click', () => {
this.elements.logContent.innerHTML = '';
});
}
/**
* 更新配置
* @private
*/
_updateConfig() {
this.controller.config.server = this.elements.server.value;
this.controller.config.userID = this.elements.userId.value;
this.controller.config.secure = this.elements.secure.checked;
this.controller.config.autoReconnect = this.elements.autoReconnect.checked;
// 重新初始化控制器
this.controller.init();
}
/**
* 测试连接
* @private
*/
_testConnection() {
// 发送测试请求到 example.com
this.log('发送测试请求到 example.com:80...');
const testData = new TextEncoder().encode('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n');
const success = this.controller.sendRequest(ADDR_TYPE_DOMAIN, 'example.com', 80, testData.buffer);
if (!success) {
this.log('测试请求发送失败');
} else {
this.log('测试请求已发送,等待响应...');
}
}
/**
* 更新状态显示
* @param {string} status - 状态
* @private
*/
_updateStatus(status) {
// 更新状态显示
this.elements.statusDisplay.className = `status-${status}`;
switch (status) {
case 'connected':
this.elements.statusDisplay.textContent = '已连接';
this.elements.connectBtn.disabled = true;
this.elements.disconnectBtn.disabled = false;
this.elements.testBtn.disabled = false;
this.log('代理已连接');
break;
case 'connecting':
this.elements.statusDisplay.textContent = '连接中...';
this.elements.connectBtn.disabled = true;
this.elements.disconnectBtn.disabled = true;
this.elements.testBtn.disabled = true;
this.log('正在连接代理...');
break;
case 'disconnected':
this.elements.statusDisplay.textContent = '未连接';
this.elements.connectBtn.disabled = false;
this.elements.disconnectBtn.disabled = true;
this.elements.testBtn.disabled = true;
this.log('代理已断开连接');
break;
case 'error':
this.elements.statusDisplay.textContent = '错误';
this.elements.connectBtn.disabled = false;
this.elements.disconnectBtn.disabled = true;
this.elements.testBtn.disabled = true;
break;
}
// 隐藏错误显示
if (status !== 'error') {
this.elements.errorDisplay.style.display = 'none';
}
}
/**
* 显示错误
* @param {Error} error - 错误对象
* @private
*/
_showError(error) {
this.elements.errorDisplay.textContent = `错误: ${error.message}`;
this.elements.errorDisplay.style.display = 'block';
this.log(`错误: ${error.message}`, 'error');
}
/**
* 添加日志
* @param {string} message - 日志消息
* @param {string} type - 日志类型
*/
log(message, type = 'info') {
const logEntry = document.createElement('div');
logEntry.className = `log-entry log-${type}`;
const timestamp = new Date().toLocaleTimeString();
logEntry.textContent = `[${timestamp}] ${message}`;
this.elements.logContent.appendChild(logEntry);
this.elements.logContent.scrollTop = this.elements.logContent.scrollHeight;
}
}
/**
* VLESS 浏览器代理客户端主类
*/
class VLESSBrowserClient {
/**
* 创建 VLESS 浏览器代理客户端
* @param {Object} config - 配置信息
*/
constructor(config = {}) {
this.config = Object.assign({
server: '',
userID: '',
secure: true,
autoReconnect: true,
maxRetries: 3,
retryDelay: 1000
}, config);
this.controller = new ProxyController(this.config);
this.ui = new ProxyUI(this.controller);
}
/**
* 初始化客户端
* @param {string} containerId - UI 容器元素 ID
*/
init(containerId) {
this.controller.init();
this.ui.init(containerId);
}
}
// 导出模块
window.VLESSBrowserClient = VLESSBrowserClient;
window.WebSocketTransport = WebSocketTransport;
window.VLESSProtocol = VLESSProtocol;
window.ProxyController = ProxyController;
window.ProxyUI = ProxyUI;<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VLESS 浏览器代理客户端测试</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
color: #333;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: #f9f9f9;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: #2c3e50;
}
.test-section {
margin-top: 30px;
}
#proxyClient {
margin-top: 30px;
}
</style>
</head>
<body>
<div class="container">
<h1>VLESS 浏览器代理客户端测试</h1>
<div class="test-section">
<h2>功能测试</h2>
<p>本页面用于测试 VLESS 浏览器代理客户端的功能和安全性。</p>
<p>请在下方配置服务器信息并进行连接测试。</p>
</div>
<div id="proxyClient"></div>
</div>
<script src="vless-browser-client.js"></script>
<script>
// 页面加载完成后初始化客户端
document.addEventListener('DOMContentLoaded', function() {
// 创建客户端实例
const client = new VLESSBrowserClient({
server: 'your-server-domain.com',
userID: 'd342d11e-d424-4583-b36e-524ab1f0afa4', // 默认示例 UUID
secure: true,
autoReconnect: true
});
// 初始化客户端 UI
client.init('proxyClient');
// 添加数据处理回调
client.controller.onData = function(data) {
const decoder = new TextDecoder();
const text = decoder.decode(data);
client.ui.log('收到数据: ' + (text.length > 100 ? text.substring(0, 100) + '...' : text));
};
// 添加到全局变量以便控制台调试
window.vlessClient = client;
console.log('VLESS 浏览器代理客户端已初始化');
});
</script>
</body>
</html> |
Beta Was this translation helpful? Give feedback.
-
|
好吧。我错了。 |
Beta Was this translation helpful? Give feedback.
-
Related DiscussionsDiscovered several related threads in the community — seems like there's strong interest in this space! Similar Proposals
Related Feature Requests
Implementation ProgressI've started working on this — PR #7654 addresses the localhost auth / DNS rebinding security issues as a first step. The workspace encryption piece is next. Would love to collaborate with anyone interested in this area. 🔐 |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
浏览器js支持websocket接口
Beta Was this translation helpful? Give feedback.
All reactions