diff --git a/background.js b/background.js index daf9d40..6576915 100644 --- a/background.js +++ b/background.js @@ -12,6 +12,17 @@ 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 = { @@ -78,7 +89,18 @@ function initConnection(socketId) { 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) { @@ -90,7 +112,15 @@ function removeConnection(socketId) { } 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) { @@ -102,11 +132,25 @@ function handleSocksData(clientSocketId, data) { // 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'); + 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; @@ -343,7 +387,14 @@ function handleDestinationData(destSocketId, data) { return; } - console.log('[Tunnel] Destination -> Client: ' + data.byteLength + ' bytes'); + 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); @@ -356,6 +407,12 @@ function handleDestinationData(destSocketId, data) { 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; } }); } @@ -462,8 +519,51 @@ function initializeProxy() { 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'); @@ -500,6 +600,13 @@ chrome.app.window.onClosed.addListener(function() { // 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 // ============================================================================ @@ -512,6 +619,9 @@ chrome.alarms.onAlarm.addListener(function(alarm) { // 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...'); @@ -523,6 +633,64 @@ chrome.alarms.onAlarm.addListener(function(alarm) { } }); +// 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...'); diff --git a/window.html b/window.html new file mode 100644 index 0000000..b74db0d --- /dev/null +++ b/window.html @@ -0,0 +1,202 @@ + + + + + Chrome's Hammer + + + +
+

🔨 Chrome's Hammer

+ +
+
+ Status + + + Running + +
+
+ Host + 127.0.0.1 +
+
+ Port + 1080 +
+
+ Protocol + SOCKS5 +
+ +
+
+
Active Connections
+
0
+
+
+
Total Connections
+
0
+
+
+
Data Received
+
0 B
+
+
+
Data Sent
+
0 B
+
+
+ +
+ Uptime + 0s +
+
+ + + +
+ Keep this window open to maintain connection. +
+ +
+ Last heartbeat: -- +
+
+ + + + diff --git a/window.js b/window.js new file mode 100644 index 0000000..d0d3dc9 --- /dev/null +++ b/window.js @@ -0,0 +1,34 @@ +// Listen for status updates from background script +chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { + if (message.type === 'status' && message.stats) { + var s = message.stats; + document.getElementById('status').textContent = message.active ? 'Running' : 'Inactive'; + document.getElementById('indicator').className = 'indicator ' + (message.active ? 'active' : 'inactive'); + document.getElementById('activeConnections').textContent = s.activeConnections; + document.getElementById('totalConnections').textContent = s.totalConnections; + document.getElementById('bytesReceived').textContent = s.bytesReceived; + document.getElementById('bytesSent').textContent = s.bytesSent; + document.getElementById('uptime').textContent = s.uptime; + + // Show connection durations + var list = document.getElementById('connectionsList'); + if (s.connectionDurations && s.connectionDurations.length > 0) { + list.style.display = 'block'; + list.innerHTML = '
Active Connections
'; + s.connectionDurations.forEach(function(duration) { + var div = document.createElement('div'); + div.className = 'connection-item'; + div.textContent = 'Connection ' + (s.connectionDurations.indexOf(duration) + 1) + ': ' + duration; + list.appendChild(div); + }); + } else { + list.style.display = 'none'; + } + } else if (message.type === 'heartbeat') { + var now = new Date(); + document.getElementById('heartbeat').textContent = 'Last heartbeat: ' + now.toLocaleTimeString(); + } +}); + +// Request initial status +chrome.runtime.sendMessage({ type: 'getStatus' });