543 lines
16 KiB
JavaScript
543 lines
16 KiB
JavaScript
/**
|
|
* Chrome App - SOCKS5 Proxy with Bidirectional Tunneling
|
|
* Supports both HTTP and HTTPS through true SOCKS5 tunneling
|
|
*/
|
|
|
|
// Configuration
|
|
var PROXY_HOST = '127.0.0.1';
|
|
var PROXY_PORT = 1080;
|
|
|
|
// TCP Server Management
|
|
var serverSocketId = null;
|
|
var isListening = false;
|
|
var isInitialized = false; // Prevent double initialization
|
|
var handlersSetup = false; // Prevent duplicate event listeners
|
|
|
|
// SOCKS5 Connection States
|
|
var ConnectionState = {
|
|
GREETING: 'greeting',
|
|
REQUEST: 'request',
|
|
TUNNEL: 'tunnel', // NEW: Tunneling state
|
|
CLOSED: 'closed'
|
|
};
|
|
|
|
// Track connections: clientSocketId -> { state, buffer, destinationSocketId }
|
|
var connections = {};
|
|
|
|
// Track reverse mapping: destinationSocketId -> clientSocketId
|
|
var reverseConnections = {};
|
|
|
|
// ============================================================================
|
|
// TCP SERVER MANAGEMENT
|
|
// ============================================================================
|
|
|
|
function createServer(host, port, callback) {
|
|
chrome.sockets.tcpServer.create({}, function(createInfo) {
|
|
if (chrome.runtime.lastError) {
|
|
callback(chrome.runtime.lastError, null);
|
|
return;
|
|
}
|
|
|
|
serverSocketId = createInfo.socketId;
|
|
console.log('[TCP Server] Created server socket: ' + serverSocketId);
|
|
|
|
chrome.sockets.tcpServer.listen(serverSocketId, host, port, function(result) {
|
|
if (result < 0) {
|
|
callback(new Error('Failed to listen on ' + host + ':' + port + ', code: ' + result), null);
|
|
return;
|
|
}
|
|
|
|
isListening = true;
|
|
console.log('[TCP Server] Listening on ' + host + ':' + port);
|
|
callback(null, serverSocketId);
|
|
});
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// SOCKS5 PROTOCOL HANDLER
|
|
// ============================================================================
|
|
|
|
// SOCKS5 constants
|
|
var SOCKS_VERSION = 0x05;
|
|
var SOCKS_CMD_CONNECT = 0x01;
|
|
var SOCKS_ATYP_IPV4 = 0x01;
|
|
var SOCKS_ATYP_DOMAIN = 0x03;
|
|
var SOCKS_ATYP_IPV6 = 0x04;
|
|
var SOCKS_AUTH_NONE = 0x00;
|
|
var SOCKS_REPLY_SUCCESS = 0x00;
|
|
var SOCKS_REPLY_GENERAL_FAILURE = 0x01;
|
|
var SOCKS_REPLY_CONNECTION_NOT_ALLOWED = 0x02;
|
|
var SOCKS_REPLY_NETWORK_UNREACHABLE = 0x03;
|
|
var SOCKS_REPLY_HOST_UNREACHABLE = 0x04;
|
|
var SOCKS_REPLY_CONNECTION_REFUSED = 0x05;
|
|
|
|
function initConnection(socketId) {
|
|
connections[socketId] = {
|
|
state: ConnectionState.GREETING,
|
|
buffer: new Uint8Array(0),
|
|
destinationSocketId: null
|
|
};
|
|
console.log('[SOCKS] Initialized connection ' + socketId);
|
|
}
|
|
|
|
function removeConnection(socketId) {
|
|
var conn = connections[socketId];
|
|
if (conn && conn.destinationSocketId) {
|
|
delete reverseConnections[conn.destinationSocketId];
|
|
try {
|
|
chrome.sockets.tcp.close(conn.destinationSocketId);
|
|
} catch(e) {}
|
|
}
|
|
delete connections[socketId];
|
|
console.log('[SOCKS] Removed connection ' + socketId);
|
|
}
|
|
|
|
function handleSocksData(clientSocketId, data) {
|
|
var conn = connections[clientSocketId];
|
|
if (!conn) {
|
|
console.error('[SOCKS] No connection state for ' + clientSocketId);
|
|
return;
|
|
}
|
|
|
|
// If we're in tunnel mode, forward data to destination
|
|
if (conn.state === ConnectionState.TUNNEL && conn.destinationSocketId) {
|
|
console.log('[Tunnel] Client -> Destination: ' + data.byteLength + ' bytes');
|
|
chrome.sockets.tcp.send(conn.destinationSocketId, data, function(sendInfo) {
|
|
if (sendInfo.resultCode < 0) {
|
|
console.error('[Tunnel] Failed to send to destination: ' + sendInfo.resultCode);
|
|
closeConnection(clientSocketId);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Append new data to buffer
|
|
var newData = new Uint8Array(data);
|
|
var newBuffer = new Uint8Array(conn.buffer.length + newData.length);
|
|
newBuffer.set(conn.buffer);
|
|
newBuffer.set(newData, conn.buffer.length);
|
|
conn.buffer = newBuffer;
|
|
|
|
// Process based on current state
|
|
if (conn.state === ConnectionState.GREETING) {
|
|
handleGreeting(clientSocketId, conn);
|
|
} else if (conn.state === ConnectionState.REQUEST) {
|
|
handleRequest(clientSocketId, conn);
|
|
}
|
|
}
|
|
|
|
function handleGreeting(socketId, conn) {
|
|
var data = conn.buffer;
|
|
|
|
if (data.length < 3) {
|
|
return; // Need more data
|
|
}
|
|
|
|
var version = data[0];
|
|
var numMethods = data[1];
|
|
|
|
if (version !== SOCKS_VERSION) {
|
|
console.error('[SOCKS] Invalid SOCKS version: ' + version);
|
|
closeConnection(socketId);
|
|
return;
|
|
}
|
|
|
|
if (data.length < 2 + numMethods) {
|
|
return; // Need more data
|
|
}
|
|
|
|
// Check if no authentication (method 0x00) is supported
|
|
var methodSupported = false;
|
|
for (var i = 0; i < numMethods; i++) {
|
|
if (data[2 + i] === SOCKS_AUTH_NONE) {
|
|
methodSupported = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (methodSupported) {
|
|
sendGreetingResponse(socketId, SOCKS_AUTH_NONE);
|
|
conn.state = ConnectionState.REQUEST;
|
|
// Clear buffer after greeting
|
|
conn.buffer = new Uint8Array(0);
|
|
} else {
|
|
closeConnection(socketId);
|
|
}
|
|
}
|
|
|
|
function sendGreetingResponse(socketId, method) {
|
|
var response = new Uint8Array([SOCKS_VERSION, method]);
|
|
chrome.sockets.tcp.send(socketId, response.buffer, function(sendInfo) {
|
|
if (sendInfo.resultCode < 0) {
|
|
console.error('[SOCKS] Failed to send greeting response: ' + sendInfo.resultCode);
|
|
} else {
|
|
console.log('[SOCKS] Sent greeting response, method: ' + method);
|
|
}
|
|
});
|
|
}
|
|
|
|
function handleRequest(clientSocketId, conn) {
|
|
var data = conn.buffer;
|
|
|
|
if (data.length < 4) {
|
|
return; // Need more data
|
|
}
|
|
|
|
var version = data[0];
|
|
var command = data[1];
|
|
var atyp = data[3];
|
|
|
|
if (version !== SOCKS_VERSION) {
|
|
console.error('[SOCKS] Invalid SOCKS version in request: ' + version);
|
|
sendRequestResponse(clientSocketId, SOCKS_REPLY_GENERAL_FAILURE);
|
|
closeConnection(clientSocketId);
|
|
return;
|
|
}
|
|
|
|
if (command !== SOCKS_CMD_CONNECT) {
|
|
console.error('[SOCKS] Unsupported command: ' + command);
|
|
sendRequestResponse(clientSocketId, SOCKS_REPLY_COMMAND_NOT_SUPPORTED);
|
|
closeConnection(clientSocketId);
|
|
return;
|
|
}
|
|
|
|
// Parse address
|
|
var host = '';
|
|
var port = 0;
|
|
var consumed = 0;
|
|
|
|
if (atyp === SOCKS_ATYP_IPV4) {
|
|
if (data.length < 10) {
|
|
return; // Need more data
|
|
}
|
|
host = data[4] + '.' + data[5] + '.' + data[6] + '.' + data[7];
|
|
port = (data[8] << 8) | data[9];
|
|
consumed = 10;
|
|
} else if (atyp === SOCKS_ATYP_DOMAIN) {
|
|
var domainLen = data[4];
|
|
if (data.length < 7 + domainLen) {
|
|
return; // Need more data
|
|
}
|
|
host = '';
|
|
for (var i = 0; i < domainLen; i++) {
|
|
host += String.fromCharCode(data[5 + i]);
|
|
}
|
|
port = (data[5 + domainLen] << 8) | data[6 + domainLen];
|
|
consumed = 7 + domainLen;
|
|
} else if (atyp === SOCKS_ATYP_IPV6) {
|
|
if (data.length < 22) {
|
|
return; // Need more data
|
|
}
|
|
// Parse IPv6 (simplified)
|
|
var parts = [];
|
|
for (var j = 0; j < 16; j += 2) {
|
|
parts.push(((data[4 + j] << 8) | data[5 + j]).toString(16));
|
|
}
|
|
host = parts.join(':');
|
|
port = (data[20] << 8) | data[21];
|
|
consumed = 22;
|
|
} else {
|
|
console.error('[SOCKS] Unsupported address type: ' + atyp);
|
|
sendRequestResponse(clientSocketId, SOCKS_REPLY_ADDRESS_TYPE_NOT_SUPPORTED);
|
|
closeConnection(clientSocketId);
|
|
return;
|
|
}
|
|
|
|
console.log('[SOCKS] CONNECT request: ' + host + ':' + port);
|
|
|
|
// Create tunnel to destination
|
|
createTunnel(clientSocketId, host, port, consumed);
|
|
}
|
|
|
|
function createTunnel(clientSocketId, host, port, consumed) {
|
|
// Clear the request buffer
|
|
var conn = connections[clientSocketId];
|
|
conn.buffer = new Uint8Array(0);
|
|
|
|
// Create TCP connection to destination
|
|
chrome.sockets.tcp.create({}, function(createInfo) {
|
|
if (chrome.runtime.lastError || !createInfo) {
|
|
console.error('[Tunnel] Failed to create socket:', chrome.runtime.lastError);
|
|
sendRequestResponse(clientSocketId, SOCKS_REPLY_GENERAL_FAILURE);
|
|
closeConnection(clientSocketId);
|
|
return;
|
|
}
|
|
|
|
var destSocketId = createInfo.socketId;
|
|
conn.destinationSocketId = destSocketId;
|
|
reverseConnections[destSocketId] = clientSocketId;
|
|
|
|
console.log('[Tunnel] Created destination socket: ' + destSocketId);
|
|
|
|
// Connect to destination
|
|
chrome.sockets.tcp.connect(destSocketId, host, port, function(result) {
|
|
if (result < 0) {
|
|
console.error('[Tunnel] Failed to connect to ' + host + ':' + port + ', code: ' + result);
|
|
|
|
// Map error codes
|
|
var reply = SOCKS_REPLY_GENERAL_FAILURE;
|
|
if (result === -109) { // Host unreachable
|
|
reply = SOCKS_REPLY_HOST_UNREACHABLE;
|
|
} else if (result === -111) { // Connection refused
|
|
reply = SOCKS_REPLY_CONNECTION_REFUSED;
|
|
} else if (result === -102) { // Network unreachable
|
|
reply = SOCKS_REPLY_NETWORK_UNREACHABLE;
|
|
}
|
|
|
|
sendRequestResponse(clientSocketId, reply);
|
|
closeConnection(clientSocketId);
|
|
return;
|
|
}
|
|
|
|
console.log('[Tunnel] Connected to ' + host + ':' + port);
|
|
|
|
// Send success response to client
|
|
sendRequestResponse(clientSocketId, SOCKS_REPLY_SUCCESS);
|
|
|
|
// Set to tunnel mode
|
|
conn.state = ConnectionState.TUNNEL;
|
|
|
|
// Unpause destination socket to start receiving data
|
|
chrome.sockets.tcp.setPaused(destSocketId, false);
|
|
|
|
console.log('[Tunnel] Tunnel established: client=' + clientSocketId + ' -> dest=' + destSocketId);
|
|
});
|
|
});
|
|
}
|
|
|
|
function sendRequestResponse(socketId, reply) {
|
|
var response = new Uint8Array([
|
|
SOCKS_VERSION, // VER
|
|
reply, // REP
|
|
0x00, // RSV
|
|
SOCKS_ATYP_IPV4, // ATYP
|
|
0x00, 0x00, 0x00, 0x00, // BND.ADDR
|
|
0x00, 0x00 // BND.PORT
|
|
]);
|
|
|
|
chrome.sockets.tcp.send(socketId, response.buffer, function(sendInfo) {
|
|
if (sendInfo.resultCode < 0) {
|
|
console.error('[SOCKS] Failed to send request response: ' + sendInfo.resultCode);
|
|
} else {
|
|
console.log('[SOCKS] Sent request response, reply: ' + reply);
|
|
}
|
|
});
|
|
}
|
|
|
|
function closeConnection(socketId) {
|
|
chrome.sockets.tcp.close(socketId);
|
|
removeConnection(socketId);
|
|
}
|
|
|
|
// ============================================================================
|
|
// BIDIRECTIONAL TUNNEL DATA FORWARDING
|
|
// ============================================================================
|
|
|
|
// Handle data from destination - forward to client
|
|
function handleDestinationData(destSocketId, data) {
|
|
var clientSocketId = reverseConnections[destSocketId];
|
|
|
|
if (!clientSocketId) {
|
|
console.warn('[Tunnel] No client for destination socket: ' + destSocketId);
|
|
return;
|
|
}
|
|
|
|
console.log('[Tunnel] Destination -> Client: ' + data.byteLength + ' bytes');
|
|
|
|
// Pause destination to prevent event spam while sending
|
|
chrome.sockets.tcp.setPaused(destSocketId, true);
|
|
|
|
chrome.sockets.tcp.send(clientSocketId, data, function(sendInfo) {
|
|
// Resume destination after sending
|
|
chrome.sockets.tcp.setPaused(destSocketId, false);
|
|
|
|
if (sendInfo.resultCode < 0) {
|
|
console.error('[Tunnel] Failed to send to client: ' + sendInfo.resultCode);
|
|
// Close both connections
|
|
closeConnection(clientSocketId);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAIN INITIALIZATION
|
|
// ============================================================================
|
|
|
|
function setupConnectionHandlers() {
|
|
// Handle new connections from clients
|
|
chrome.sockets.tcpServer.onAccept.addListener(function(acceptInfo) {
|
|
console.log('[Proxy] New client connected: ' + acceptInfo.clientSocketId);
|
|
|
|
// Initialize SOCKS state for this connection
|
|
initConnection(acceptInfo.clientSocketId);
|
|
|
|
// Unpause socket to start receiving data
|
|
chrome.sockets.tcp.setPaused(acceptInfo.clientSocketId, false);
|
|
});
|
|
|
|
// Handle accept errors
|
|
chrome.sockets.tcpServer.onAcceptError.addListener(function(errorInfo) {
|
|
console.error('[Proxy] Accept error:', errorInfo);
|
|
});
|
|
|
|
// Handle receive errors
|
|
chrome.sockets.tcp.onReceiveError.addListener(function(info) {
|
|
var socketId = info.socketId;
|
|
var resultCode = info.resultCode;
|
|
|
|
// Error code -100 is ERR_CONNECTION_CLOSED - normal disconnect, not an error
|
|
if (resultCode === -100) {
|
|
console.log('[Proxy] Socket ' + socketId + ' connection closed');
|
|
} else {
|
|
console.error('[Proxy] Receive error on socket ' + socketId + ': ' + resultCode);
|
|
}
|
|
|
|
// Clean up connections
|
|
if (connections[socketId]) {
|
|
closeConnection(socketId);
|
|
} else if (reverseConnections[socketId]) {
|
|
closeConnection(reverseConnections[socketId]);
|
|
}
|
|
});
|
|
|
|
// Handle disconnects (EOF detection - zero byte receive)
|
|
chrome.sockets.tcp.onReceive.addListener(function(receiveInfo) {
|
|
if (receiveInfo.data.byteLength === 0) {
|
|
var socketId = receiveInfo.socketId;
|
|
console.log('[Proxy] Socket ' + socketId + ' disconnected (EOF)');
|
|
if (connections[socketId]) {
|
|
closeConnection(socketId);
|
|
} else if (reverseConnections[socketId]) {
|
|
closeConnection(reverseConnections[socketId]);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// If not EOF, let the main onReceive handler handle the data
|
|
var socketId = receiveInfo.socketId;
|
|
|
|
// Check if this is data from a client or destination
|
|
if (connections[socketId]) {
|
|
// Data from client
|
|
console.log('[Proxy] Received from client ' + socketId + ': ' + receiveInfo.data.byteLength + ' bytes');
|
|
handleSocksData(socketId, receiveInfo.data);
|
|
} else if (reverseConnections[socketId]) {
|
|
// Data from destination
|
|
handleDestinationData(socketId, receiveInfo.data);
|
|
} else {
|
|
console.warn('[Proxy] Received data from unknown socket: ' + socketId);
|
|
}
|
|
});
|
|
}
|
|
|
|
function initializeProxy() {
|
|
// Prevent double initialization
|
|
if (isInitialized) {
|
|
console.log('[Proxy] Already initialized or initializing...');
|
|
return;
|
|
}
|
|
isInitialized = true;
|
|
|
|
console.log('[Proxy] Initializing Chrome SOCKS Proxy (with tunneling)...');
|
|
|
|
createServer(PROXY_HOST, PROXY_PORT, function(error, socketId) {
|
|
if (error) {
|
|
console.error('[Proxy] Failed to initialize:', error);
|
|
isInitialized = false; // Allow retry on failure
|
|
return;
|
|
}
|
|
|
|
console.log('[Proxy] SOCKS proxy is running on ' + PROXY_HOST + ':' + PROXY_PORT);
|
|
|
|
// Setup handlers only once
|
|
if (!handlersSetup) {
|
|
setupConnectionHandlers();
|
|
handlersSetup = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Chrome App lifecycle events
|
|
chrome.app.runtime.onLaunched.addListener(function() {
|
|
console.log('[Proxy] App launched');
|
|
initializeProxy();
|
|
});
|
|
|
|
// Chrome App lifecycle - restart when app is restarted
|
|
chrome.app.runtime.onRestarted.addListener(function() {
|
|
console.log('[Proxy] App restarted');
|
|
// Reset state and reinitialize
|
|
isListening = false;
|
|
isInitialized = false;
|
|
serverSocketId = null;
|
|
initializeProxy();
|
|
});
|
|
|
|
// Handle the app window being closed
|
|
chrome.app.window.onClosed.addListener(function() {
|
|
console.log('[Proxy] App window closed');
|
|
// Clean up connections
|
|
for (var socketId in connections) {
|
|
try {
|
|
chrome.sockets.tcp.close(parseInt(socketId));
|
|
} catch(e) {}
|
|
}
|
|
connections = {};
|
|
reverseConnections = {};
|
|
|
|
// Close server socket
|
|
if (serverSocketId !== null) {
|
|
try {
|
|
chrome.sockets.tcpServer.close(serverSocketId);
|
|
} catch(e) {}
|
|
serverSocketId = null;
|
|
}
|
|
isListening = false;
|
|
isInitialized = false;
|
|
});
|
|
|
|
// Also initialize on startup (for apps that are already running)
|
|
initializeProxy();
|
|
|
|
// ============================================================================
|
|
// KEEP-ALIVE MECHANISM
|
|
// ============================================================================
|
|
|
|
// Setup keep-alive alarm to prevent Chrome from suspending the app
|
|
chrome.alarms.create('keepAlive', { periodInMinutes: 1 });
|
|
|
|
chrome.alarms.onAlarm.addListener(function(alarm) {
|
|
if (alarm.name === 'keepAlive') {
|
|
// Heartbeat: log activity to keep the app alive
|
|
console.log('[Proxy] Heartbeat - Proxy is ' + (isListening ? 'active' : 'inactive'));
|
|
|
|
// Check if server is still listening, restart if needed
|
|
if (!isListening && !isInitialized) {
|
|
console.log('[Proxy] Server not listening, reinitializing...');
|
|
initializeProxy();
|
|
}
|
|
|
|
// Request keep awake to prevent system sleep (optional)
|
|
chrome.power.requestKeepAwake('system');
|
|
}
|
|
});
|
|
|
|
// Handle when app is suspended and resumed
|
|
chrome.runtime.onSuspend.addListener(function() {
|
|
console.log('[Proxy] App being suspended...');
|
|
});
|
|
|
|
chrome.runtime.onSuspendCanceled.addListener(function() {
|
|
console.log('[Proxy] Suspend canceled - app is active again');
|
|
|
|
// Reinitialize if needed after unsuspend
|
|
if (!isListening) {
|
|
console.log('[Proxy] Reinitializing after unsuspend...');
|
|
isInitialized = false;
|
|
initializeProxy();
|
|
}
|
|
});
|
|
|
|
console.log('[Proxy] Background script loaded');
|