Files
chromes_hammer/background.js
Joerg Dorgeist 97487ce060 Status Window
2026-01-14 11:57:57 +01:00

711 lines
21 KiB
JavaScript

/**
* Chrome's Hammer - 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
var appWindow = null; // Keep app window reference to prevent suspension
// Connection statistics
var stats = {
totalConnections: 0,
activeConnections: 0,
bytesReceived: 0,
bytesSent: 0,
startTime: Date.now(),
connections: {} // socketId -> { connectTime, bytesReceived, bytesSent }
};
// 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
};
// Track statistics
stats.totalConnections++;
stats.activeConnections++;
stats.connections[socketId] = {
connectTime: Date.now(),
bytesReceived: 0,
bytesSent: 0
};
console.log('[SOCKS] Initialized connection ' + socketId);
sendStatusToWindow();
}
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];
// Update statistics
if (stats.connections[socketId]) {
delete stats.connections[socketId];
}
stats.activeConnections--;
console.log('[SOCKS] Removed connection ' + socketId);
sendStatusToWindow();
}
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) {
var byteLength = data.byteLength;
console.log('[Tunnel] Client -> Destination: ' + byteLength + ' bytes');
// Track statistics
stats.bytesReceived += byteLength;
if (stats.connections[clientSocketId]) {
stats.connections[clientSocketId].bytesReceived += byteLength;
}
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;
}
// Track sent bytes
stats.bytesSent += byteLength;
if (stats.connections[clientSocketId]) {
stats.connections[clientSocketId].bytesSent += byteLength;
}
});
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;
}
var byteLength = data.byteLength;
console.log('[Tunnel] Destination -> Client: ' + byteLength + ' bytes');
// Track statistics
stats.bytesReceived += byteLength;
if (stats.connections[clientSocketId]) {
stats.connections[clientSocketId].bytesReceived += byteLength;
}
// 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);
return;
}
// Track sent bytes
stats.bytesSent += byteLength;
if (stats.connections[clientSocketId]) {
stats.connections[clientSocketId].bytesSent += byteLength;
}
});
}
// ============================================================================
// MAIN INITIALIZATION
// ============================================================================
function setupConnectionHandlers() {
// Handle new connections from clients
chrome.sockets.tcpServer.onAccept.addListener(function(acceptInfo) {
console.log('[Hammer] 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('[Hammer] 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('[Hammer] Socket ' + socketId + ' connection closed');
} else {
console.error('[Hammer] 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('[Hammer] 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('[Hammer] 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('[Hammer] Received data from unknown socket: ' + socketId);
}
});
}
function initializeProxy() {
// Prevent double initialization
if (isInitialized) {
console.log('[Hammer] Already initialized or initializing...');
return;
}
isInitialized = true;
console.log('[Hammer] Initializing Chrome\'s Hammer (with tunneling)...');
createServer(PROXY_HOST, PROXY_PORT, function(error, socketId) {
if (error) {
console.error('[Hammer] Failed to initialize:', error);
isInitialized = false; // Allow retry on failure
return;
}
console.log('[Hammer] Chrome\'s Hammer 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('[Hammer] App launched');
initializeProxy();
createAppWindow();
});
// Create a minimal app window to prevent Chrome from suspending the app
function createAppWindow() {
// Only create if we don't already have one
if (appWindow) {
try {
appWindow.focus();
return;
} catch(e) {
appWindow = null;
}
}
chrome.app.window.create('window.html', {
'outerBounds': {
'width': 400,
'height': 500,
'minWidth': 300,
'minHeight': 400
},
'resizable': true,
'alwaysOnTop': false
}, function(createdWindow) {
if (chrome.runtime.lastError) {
console.error('[Hammer] Failed to create window:', chrome.runtime.lastError);
return;
}
appWindow = createdWindow;
console.log('[Hammer] App window created - prevents suspension');
// Re-create window if closed
appWindow.onClosed.addListener(function() {
console.log('[Hammer] Window closed, recreating in 2 seconds...');
appWindow = null;
setTimeout(function() {
if (!appWindow) {
createAppWindow();
}
}, 2000);
});
});
}
// Chrome App lifecycle - restart when app is restarted
chrome.app.runtime.onRestarted.addListener(function() {
console.log('[Hammer] 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('[Hammer] 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();
// Create window on startup to prevent suspension
setTimeout(function() {
if (!appWindow) {
createAppWindow();
}
}, 1000);
// ============================================================================
// 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('[Hammer] Heartbeat - Proxy is ' + (isListening ? 'active' : 'inactive'));
// Send status to window
sendStatusToWindow();
// Check if server is still listening, restart if needed
if (!isListening && !isInitialized) {
console.log('[Hammer] Server not listening, reinitializing...');
initializeProxy();
}
// Request keep awake to prevent system sleep (optional)
chrome.power.requestKeepAwake('system');
}
});
// Send status to app window
function sendStatusToWindow() {
// Format bytes for display
function formatBytes(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
}
// Format duration for display
function formatDuration(ms) {
var seconds = Math.floor(ms / 1000);
var minutes = Math.floor(seconds / 60);
var hours = Math.floor(minutes / 60);
if (hours > 0) return hours + 'h ' + (minutes % 60) + 'm';
if (minutes > 0) return minutes + 'm ' + (seconds % 60) + 's';
return seconds + 's';
}
// Calculate connection durations
var connectionDurations = [];
for (var socketId in stats.connections) {
var conn = stats.connections[socketId];
var duration = Date.now() - conn.connectTime;
connectionDurations.push(formatDuration(duration));
}
chrome.runtime.sendMessage({
type: 'status',
active: isListening,
stats: {
totalConnections: stats.totalConnections,
activeConnections: stats.activeConnections,
bytesReceived: formatBytes(stats.bytesReceived),
bytesSent: formatBytes(stats.bytesSent),
uptime: formatDuration(Date.now() - stats.startTime),
connectionDurations: connectionDurations
}
});
chrome.runtime.sendMessage({
type: 'heartbeat'
});
}
// Listen for status requests from window
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message.type === 'getStatus') {
sendResponse({
type: 'status',
active: isListening,
stats: {
totalConnections: stats.totalConnections,
activeConnections: stats.activeConnections
}
});
}
});
// Handle when app is suspended and resumed
chrome.runtime.onSuspend.addListener(function() {
console.log('[Hammer] App being suspended...');
});
chrome.runtime.onSuspendCanceled.addListener(function() {
console.log('[Hammer] Suspend canceled - app is active again');
// Reinitialize if needed after unsuspend
if (!isListening) {
console.log('[Hammer] Reinitializing after unsuspend...');
isInitialized = false;
initializeProxy();
}
});
console.log('[Hammer] Chrome\'s Hammer background script loaded');