在浏览器端实现vless和vmess客户端可行吗 #4769
Unanswered
BigUint64Array
asked this question in
Q&A
Replies: 3 comments 1 reply
-
不行。除非你写一个出来康康。 |
Beta Was this translation helpful? Give feedback.
0 replies
-
可行。 服务器端: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.
1 reply
-
好吧。我错了。 |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
浏览器js支持websocket接口
Beta Was this translation helpful? Give feedback.
All reactions