Created
July 3, 2025 14:35
-
-
Save supersational/ccf03454fe2f7e3fc3af1f284e711d81 to your computer and use it in GitHub Desktop.
Localhost server scanner
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// to run: deno run --allow-net --allow-run severfinder.ts | |
import { serve } from "https://deno.land/[email protected]/http/server.ts"; | |
// adjust range or concurrency here | |
const PORT_START = 1; | |
const PORT_END = 65535; | |
const CONCURRENCY = 100; | |
const SERVE_PORT = 3000; | |
const TIMEOUT_MS = 1000; | |
interface PortInfo { | |
port: number; | |
process?: string; | |
pid?: string; | |
command?: string; | |
} | |
async function isHttpAlive(port: number): Promise<boolean> { | |
const controller = new AbortController(); | |
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS); | |
try { | |
const resp = await fetch(`http://127.0.0.1:${port}`, { | |
method: "HEAD", | |
signal: controller.signal, | |
}); | |
return resp.ok; | |
} catch { | |
return false; | |
} finally { | |
clearTimeout(timeout); | |
} | |
} | |
async function getPortDetails(port: number): Promise<PortInfo> { | |
try { | |
const command = new Deno.Command("lsof", { | |
args: ["-i", `:${port}`, "-t"], | |
stdout: "piped", | |
stderr: "piped", | |
}); | |
const { code, stdout } = await command.output(); | |
if (code === 0) { | |
const pids = new TextDecoder().decode(stdout).trim().split('\n').filter(p => p); | |
if (pids.length > 0) { | |
// Get process details for the first PID | |
const pid = pids[0]; | |
const psCommand = new Deno.Command("ps", { | |
args: ["-p", pid, "-o", "comm=,args="], | |
stdout: "piped", | |
stderr: "piped", | |
}); | |
const { code: psCode, stdout: psStdout } = await psCommand.output(); | |
if (psCode === 0) { | |
const processInfo = new TextDecoder().decode(psStdout).trim(); | |
const [process, ...args] = processInfo.split(/\s+/); | |
return { | |
port, | |
process: process || "unknown", | |
pid, | |
command: args.join(' ') || process | |
}; | |
} | |
} | |
} | |
} catch (error) { | |
console.log(`Error getting details for port ${port}:`, error); | |
} | |
return { port }; | |
} | |
function makePortItemHtml(info: PortInfo): string { | |
const processInfo = info.process | |
? `<br><small style="color: #666;">PID: ${info.pid} | Process: ${info.process}<br>Command: ${info.command}</small>` | |
: '<br><small style="color: #999;">No process details available</small>'; | |
return `<li id="port-${info.port}" style="margin-bottom: 15px; padding: 10px; background: #f8fafc; border-left: 3px solid #2563eb; border-radius: 4px;"> | |
<a href="http://localhost:${info.port}" target="_blank" style="font-weight: bold; color: #2563eb;">localhost:${info.port}</a> | |
${processInfo} | |
</li>`; | |
} | |
function getBaseHtml(): string { | |
return `<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<title>Live localhost ports</title> | |
<style> | |
body { | |
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
max-width: 800px; | |
margin: 40px auto; | |
padding: 20px; | |
line-height: 1.6; | |
background: #ffffff; | |
} | |
h1 { color: #1f2937; } | |
a { text-decoration: none; } | |
a:hover { text-decoration: underline; } | |
button { | |
padding: 10px 20px; | |
background: #2563eb; | |
color: white; | |
border: none; | |
border-radius: 5px; | |
cursor: pointer; | |
font-size: 14px; | |
} | |
button:hover:not(:disabled) { background: #1d4ed8; } | |
button:disabled { background: #ccc; cursor: not-allowed; } | |
#progress { | |
margin: 20px 0; | |
padding: 15px; | |
background: #eff6ff; | |
border: 1px solid #bfdbfe; | |
border-radius: 6px; | |
} | |
#results { | |
margin-top: 20px; | |
} | |
ul { | |
list-style: none; | |
padding: 0; | |
} | |
.status { | |
color: #059669; | |
font-weight: 500; | |
} | |
.error { | |
color: #dc2626; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Live HTTP Servers on Localhost</h1> | |
<div style="margin-bottom: 20px;"> | |
<button onclick="rescan()">Rescan Ports</button> | |
</div> | |
<div id="progress" style="display: none;"> | |
<div class="status">🔍 Scanning ports...</div> | |
<div id="scan-status">Starting scan...</div> | |
</div> | |
<div id="results"> | |
<ul id="port-list"></ul> | |
<div id="summary" style="margin-top: 20px; color: #666;"></div> | |
</div> | |
<script> | |
let portCount = 0; | |
function updateProgress(message) { | |
document.getElementById('scan-status').textContent = message; | |
} | |
function addPort(portHtml) { | |
const portList = document.getElementById('port-list'); | |
portList.insertAdjacentHTML('beforeend', portHtml); | |
portCount++; | |
updateProgress(\`Found \${portCount} HTTP server\${portCount === 1 ? '' : 's'} so far...\`); | |
} | |
function finishScan(message) { | |
document.getElementById('progress').style.display = 'none'; | |
document.getElementById('summary').innerHTML = message; | |
document.querySelector('button').disabled = false; | |
document.querySelector('button').textContent = 'Rescan Ports'; | |
} | |
async function rescan() { | |
const button = document.querySelector('button'); | |
const progress = document.getElementById('progress'); | |
const portList = document.getElementById('port-list'); | |
const summary = document.getElementById('summary'); | |
button.disabled = true; | |
button.textContent = 'Scanning...'; | |
progress.style.display = 'block'; | |
portList.innerHTML = ''; | |
summary.innerHTML = ''; | |
portCount = 0; | |
updateProgress('Starting scan...'); | |
try { | |
const response = await fetch('/scan'); | |
const reader = response.body.getReader(); | |
const decoder = new TextDecoder(); | |
while (true) { | |
const { done, value } = await reader.read(); | |
if (done) break; | |
const chunk = decoder.decode(value); | |
const lines = chunk.split('\\n'); | |
for (const line of lines) { | |
if (line.trim()) { | |
try { | |
const data = JSON.parse(line); | |
if (data.type === 'port') { | |
addPort(data.html); | |
} else if (data.type === 'progress') { | |
updateProgress(data.message); | |
} else if (data.type === 'complete') { | |
finishScan(data.message); | |
} | |
} catch (e) { | |
console.log('Parsing chunk:', line); | |
} | |
} | |
} | |
} | |
} catch (error) { | |
console.error('Scan failed:', error); | |
finishScan('<span class="error">❌ Scan failed. Please try again.</span>'); | |
} | |
} | |
// Auto-scan on page load | |
rescan(); | |
</script> | |
</body> | |
</html>`; | |
} | |
async function streamingScan(controller: ReadableStreamDefaultController<Uint8Array>) { | |
const encoder = new TextEncoder(); | |
let foundPorts = 0; | |
let scannedPorts = 0; | |
const totalPorts = PORT_END - PORT_START + 1; | |
const sendUpdate = (data: any) => { | |
controller.enqueue(encoder.encode(JSON.stringify(data) + '\n')); | |
}; | |
sendUpdate({ | |
type: 'progress', | |
message: `Scanning ${totalPorts.toLocaleString()} ports...` | |
}); | |
const openPorts: number[] = []; | |
const ports = Array.from({ length: totalPorts }, (_, i) => PORT_START + i); | |
let idx = 0; | |
async function worker() { | |
while (idx < ports.length) { | |
const port = ports[idx++]; | |
scannedPorts++; | |
if (scannedPorts % 1000 === 0) { | |
sendUpdate({ | |
type: 'progress', | |
message: `Scanned ${scannedPorts.toLocaleString()}/${totalPorts.toLocaleString()} ports... Found ${foundPorts} HTTP servers` | |
}); | |
} | |
if (await isHttpAlive(port)) { | |
openPorts.push(port); | |
foundPorts++; | |
// Get details for this port immediately | |
const portInfo = await getPortDetails(port); | |
const portHtml = makePortItemHtml(portInfo); | |
sendUpdate({ | |
type: 'port', | |
html: portHtml | |
}); | |
console.log(`Found HTTP server on port ${port}`); | |
} | |
} | |
} | |
const tasks: Promise<void>[] = []; | |
for (let i = 0; i < CONCURRENCY; i++) { | |
tasks.push(worker()); | |
} | |
await Promise.all(tasks); | |
const message = foundPorts > 0 | |
? `<span class="status">✅ Scan complete! Found ${foundPorts} HTTP server${foundPorts === 1 ? '' : 's'} out of ${totalPorts.toLocaleString()} ports checked.</span>` | |
: `<span style="color: #666;">ℹ️ Scan complete. No HTTP servers found on localhost ports ${PORT_START}-${PORT_END}.</span>`; | |
sendUpdate({ | |
type: 'complete', | |
message: message | |
}); | |
} | |
async function handleRequest(req: Request): Promise<Response> { | |
const url = new URL(req.url); | |
if (url.pathname === '/scan') { | |
const stream = new ReadableStream({ | |
async start(controller) { | |
try { | |
await streamingScan(controller); | |
} catch (error) { | |
console.error("Scan error:", error); | |
const encoder = new TextEncoder(); | |
controller.enqueue(encoder.encode(JSON.stringify({ | |
type: 'complete', | |
message: '<span class="error">❌ Scan failed due to an error.</span>' | |
}) + '\n')); | |
} finally { | |
controller.close(); | |
} | |
} | |
}); | |
return new Response(stream, { | |
headers: { | |
"content-type": "text/plain; charset=utf-8", | |
"cache-control": "no-cache" | |
} | |
}); | |
} | |
// Default GET request - return the base HTML | |
return new Response(getBaseHtml(), { | |
status: 200, | |
headers: { "content-type": "text/html; charset=utf-8" }, | |
}); | |
} | |
async function main() { | |
console.log(`🚀 Severfinder starting on http://localhost:${SERVE_PORT}`); | |
console.log("Visit the webpage to start scanning for HTTP servers!"); | |
serve(handleRequest, { port: SERVE_PORT }); | |
} | |
main(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
port-scanner.mov