-
-
Notifications
You must be signed in to change notification settings - Fork 53
Expand file tree
/
Copy pathportscanner_test.py
More file actions
177 lines (146 loc) · 6.08 KB
/
Copy pathportscanner_test.py
File metadata and controls
177 lines (146 loc) · 6.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#!/usr/bin/env python3
"""
python-scripts/portscanner_test.py
A self-contained demo that starts lightweight local test servers (HTTP, SSH-like,
SMTP greeting, and a MySQL-like handshake), runs the AsyncPortScanner against them,
and prints the JSON results and a short human-readable summary.
Purpose for learners:
- Demonstrates how to run the async scanner in a safe, local environment.
- Shows how banner-grabbing works against services that proactively send greetings,
and against HTTP where we send a light HEAD probe.
- Provides easy-to-copy commands to reproduce the proof-of-work for a PR.
IMPORTANT:
- This demo binds to localhost (127.0.0.1) on ports above 1024 (no root required).
- Only run on your machine or in an isolated VM. Do not point this demo at external targets.
"""
from __future__ import annotations
import asyncio
import json
from typing import Dict, List
from async_port_scanner import AsyncPortScanner # imports the scanner implemented in the repo
# -----------------------
# Lightweight test server handlers
# -----------------------
# Each handler is intentionally minimal: they send a small banner (or respond to a request)
# then close. This simulates common server greeting behavior without running full server stacks.
async def http_server(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
"""
Simulate an HTTP server: read incoming request (if any), respond with a short HTTP response.
Many real HTTP servers don't send data until a request is made, so this handler samples that behavior.
"""
try:
# Try to read any incoming data (the scanner will probe with HEAD). We wait briefly.
_ = await asyncio.wait_for(reader.read(1024), timeout=0.5)
except Exception:
# no incoming data or timeout; that's fine — we'll still send a response when asked
pass
response = b"HTTP/1.1 200 OK\r\nServer: DummyHTTP/1.0\r\nContent-Length: 2\r\n\r\nOK"
writer.write(response)
await writer.drain()
writer.close()
try:
await writer.wait_closed()
except Exception:
pass
async def ssh_server(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
"""
Simulate an SSH server banner: SSH servers usually send a banner immediately on connect.
Example: "SSH-2.0-OpenSSH_8.0"
"""
writer.write(b"SSH-2.0-OpenSSH_8.0p1 Demo\r\n")
await writer.drain()
# small sleep to allow scanner to read the banner before we close
await asyncio.sleep(0.05)
writer.close()
try:
await writer.wait_closed()
except Exception:
pass
async def smtp_server(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
"""
Simulate SMTP greeting: servers commonly send a 220 greeting immediately after connect.
"""
writer.write(b"220 localhost ESMTP DemoPostfix\r\n")
await writer.drain()
try:
# optionally read a client greeting/commands (not required for demo)
_ = await asyncio.wait_for(reader.read(1024), timeout=0.5)
except Exception:
pass
writer.close()
try:
await writer.wait_closed()
except Exception:
pass
async def mysql_like_server(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
"""
Simulate a MySQL-like handshake by sending a small binary blob containing 'mysql'.
Real MySQL handshake is binary; this simplified blob is enough for our fingerprint regex.
"""
# 0x0a starts MySQL protocol version string in real handshake; include "mysql_native_password"
writer.write(b"\x0a5.7.33\x00\x00\x00\x00mysql_native_password")
await writer.drain()
await asyncio.sleep(0.05)
writer.close()
try:
await writer.wait_closed()
except Exception:
pass
# -----------------------
# Demo runner
# -----------------------
async def start_servers(port_map: Dict[int, callable]) -> List[asyncio.AbstractServer]:
"""
Start asyncio servers for each (port -> handler) pair on localhost and return the server objects.
We return the servers so the caller can close them cleanly when done.
"""
servers: List[asyncio.AbstractServer] = []
for port, handler in port_map.items():
server = await asyncio.start_server(handler, "127.0.0.1", port)
servers.append(server)
return servers
async def run_demo() -> None:
"""
Main demo flow:
1. Start test servers on pre-defined ports.
2. Instantiate AsyncPortScanner and run a scan against localhost ports.
3. Print JSON results and a concise terminal summary.
4. Cleanly shut down demo servers.
"""
# choose ports > 1024 to avoid needing root
ports = [9001, 9002, 9003, 9004]
port_map = {
9001: http_server,
9002: ssh_server,
9003: smtp_server,
9004: mysql_like_server,
}
print("Starting demo servers on localhost:", ports)
servers = await start_servers(port_map)
# show where servers are listening (helpful when debugging)
for s in servers:
for sock in s.sockets:
print("Listening on", sock.getsockname())
# instantiate the scanner with a short timeout (demo is local and fast)
scanner = AsyncPortScanner(timeout=1.0, concurrency=100, read_bytes=2048)
print("Running scanner against localhost demo ports...")
results = await scanner.scan_multiple(["127.0.0.1"], ports)
# print the full JSON result (useful as proof-of-working logs)
print("\n=== JSON Results ===")
print(json.dumps(results, indent=2))
# print a compact human-readable summary for the terminal
print("\n=== Summary (open ports) ===")
for r in results:
if r.get("open"):
print(f" - {r['host']}:{r['port']} -> services={r.get('services')} banner={r.get('banner')!r}")
# gracefully stop demo servers
for s in servers:
s.close()
await s.wait_closed()
print("\nDemo finished. Servers stopped.")
if __name__ == "__main__":
# Run the demo. Use asyncio.run() to create an event loop and execute the coroutine.
try:
asyncio.run(run_demo())
except KeyboardInterrupt:
print("Demo aborted by user.")