No One Can Stop Me Now
This commit is contained in:
@@ -48,11 +48,7 @@ def index():
|
||||
@defense_bp.route('/linux')
|
||||
@login_required
|
||||
def linux_index():
|
||||
from core.menu import MainMenu
|
||||
menu = MainMenu()
|
||||
menu.load_modules()
|
||||
modules = {k: v for k, v in menu.modules.items() if v.category == 'defense'}
|
||||
return render_template('defense_linux.html', modules=modules)
|
||||
return render_template('defense_linux.html')
|
||||
|
||||
|
||||
@defense_bp.route('/linux/audit', methods=['POST'])
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
"""Hack Hijack — web routes for scanning and taking over compromised systems."""
|
||||
|
||||
import json
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from flask import Blueprint, render_template, request, jsonify, Response
|
||||
from web.auth import login_required
|
||||
|
||||
hack_hijack_bp = Blueprint('hack_hijack', __name__)
|
||||
|
||||
# Running scans keyed by job_id
|
||||
# job_id -> {'q': Queue, 'result': dict|None, 'error': str|None, 'done': bool, 'cancel': bool}
|
||||
_running_scans: dict = {}
|
||||
|
||||
|
||||
@@ -37,34 +40,86 @@ def start_scan():
|
||||
if not target:
|
||||
return jsonify({'ok': False, 'error': 'Target IP required'})
|
||||
|
||||
# Validate scan type
|
||||
if scan_type not in ('quick', 'full', 'nmap', 'custom'):
|
||||
scan_type = 'quick'
|
||||
|
||||
job_id = str(uuid.uuid4())[:8]
|
||||
result_holder = {'result': None, 'error': None, 'done': False}
|
||||
_running_scans[job_id] = result_holder
|
||||
q = queue.Queue()
|
||||
job = {'q': q, 'result': None, 'error': None, 'done': False, 'cancel': False}
|
||||
_running_scans[job_id] = job
|
||||
|
||||
def _push(evt_type, **kw):
|
||||
kw['type'] = evt_type
|
||||
kw['ts'] = time.time()
|
||||
q.put(kw)
|
||||
|
||||
def do_scan():
|
||||
try:
|
||||
svc = _svc()
|
||||
# Build a progress callback that feeds the queue
|
||||
def progress_cb(current, total, message=''):
|
||||
_push('progress', current=current, total=total,
|
||||
pct=round(current * 100 / total) if total else 0,
|
||||
msg=message)
|
||||
|
||||
def port_found_cb(port_info):
|
||||
_push('port_found',
|
||||
port=port_info.get('port') or (port_info.port if hasattr(port_info, 'port') else 0),
|
||||
service=getattr(port_info, 'service', port_info.get('service', '')),
|
||||
banner=getattr(port_info, 'banner', port_info.get('banner', ''))[:80])
|
||||
|
||||
def status_cb(msg):
|
||||
_push('status', msg=msg)
|
||||
|
||||
r = svc.scan_target(
|
||||
target,
|
||||
scan_type=scan_type,
|
||||
custom_ports=custom_ports,
|
||||
timeout=3.0,
|
||||
progress_cb=progress_cb,
|
||||
port_found_cb=port_found_cb,
|
||||
status_cb=status_cb,
|
||||
)
|
||||
result_holder['result'] = r.to_dict()
|
||||
job['result'] = r.to_dict()
|
||||
except Exception as e:
|
||||
result_holder['error'] = str(e)
|
||||
job['error'] = str(e)
|
||||
_push('error', msg=str(e))
|
||||
finally:
|
||||
result_holder['done'] = True
|
||||
job['done'] = True
|
||||
_push('done', ok=job['error'] is None)
|
||||
|
||||
threading.Thread(target=do_scan, daemon=True).start()
|
||||
return jsonify({'ok': True, 'job_id': job_id,
|
||||
'message': f'Scan started on {target} ({scan_type})'})
|
||||
|
||||
|
||||
@hack_hijack_bp.route('/hack-hijack/scan/<job_id>/stream')
|
||||
@login_required
|
||||
def scan_stream(job_id):
|
||||
"""SSE stream for live scan progress."""
|
||||
job = _running_scans.get(job_id)
|
||||
if not job:
|
||||
def _err():
|
||||
yield f"data: {json.dumps({'type': 'error', 'msg': 'Job not found'})}\n\n"
|
||||
return Response(_err(), mimetype='text/event-stream')
|
||||
|
||||
def generate():
|
||||
q = job['q']
|
||||
while True:
|
||||
try:
|
||||
item = q.get(timeout=0.5)
|
||||
yield f"data: {json.dumps(item)}\n\n"
|
||||
if item.get('type') == 'done':
|
||||
break
|
||||
except queue.Empty:
|
||||
if job['done']:
|
||||
break
|
||||
yield ': keepalive\n\n'
|
||||
|
||||
return Response(generate(), mimetype='text/event-stream',
|
||||
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
|
||||
|
||||
|
||||
@hack_hijack_bp.route('/hack-hijack/scan/<job_id>', methods=['GET'])
|
||||
@login_required
|
||||
def scan_status(job_id):
|
||||
@@ -75,7 +130,6 @@ def scan_status(job_id):
|
||||
return jsonify({'ok': True, 'done': False, 'message': 'Scan in progress...'})
|
||||
if holder['error']:
|
||||
return jsonify({'ok': False, 'error': holder['error'], 'done': True})
|
||||
# Clean up
|
||||
_running_scans.pop(job_id, None)
|
||||
return jsonify({'ok': True, 'done': True, 'result': holder['result']})
|
||||
|
||||
|
||||
473
web/routes/port_scanner.py
Normal file
473
web/routes/port_scanner.py
Normal file
@@ -0,0 +1,473 @@
|
||||
"""Advanced Port Scanner — streaming SSE-based port scanner with nmap integration."""
|
||||
|
||||
import json
|
||||
import queue
|
||||
import socket
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from flask import Blueprint, render_template, request, jsonify, Response
|
||||
from web.auth import login_required
|
||||
|
||||
port_scanner_bp = Blueprint('port_scanner', __name__, url_prefix='/port-scanner')
|
||||
|
||||
# job_id -> {'q': Queue, 'result': dict|None, 'done': bool, 'cancel': bool}
|
||||
_jobs: dict = {}
|
||||
|
||||
# ── Common port lists ──────────────────────────────────────────────────────────
|
||||
|
||||
QUICK_PORTS = [
|
||||
21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995,
|
||||
1723, 3306, 3389, 5900, 8080, 8443, 8888,
|
||||
]
|
||||
|
||||
COMMON_PORTS = [
|
||||
20, 21, 22, 23, 25, 53, 67, 68, 69, 80, 88, 110, 111, 119, 123, 135,
|
||||
137, 138, 139, 143, 161, 162, 179, 194, 389, 443, 445, 465, 500, 514,
|
||||
515, 587, 593, 631, 636, 873, 902, 993, 995, 1080, 1194, 1433, 1521,
|
||||
1723, 1883, 2049, 2121, 2181, 2222, 2375, 2376, 2483, 2484, 3000, 3306,
|
||||
3389, 3690, 4000, 4040, 4333, 4444, 4567, 4899, 5000, 5432, 5601, 5672,
|
||||
5900, 5984, 6000, 6379, 6443, 6881, 7000, 7001, 7080, 7443, 7474, 8000,
|
||||
8001, 8008, 8080, 8081, 8082, 8083, 8088, 8089, 8161, 8333, 8443, 8444,
|
||||
8500, 8888, 8983, 9000, 9001, 9042, 9090, 9092, 9200, 9300, 9418, 9999,
|
||||
10000, 11211, 15432, 15672, 27017, 27018, 27019, 28017, 50000, 54321,
|
||||
]
|
||||
|
||||
SERVICE_MAP = {
|
||||
20: 'FTP-data', 21: 'FTP', 22: 'SSH', 23: 'Telnet', 25: 'SMTP',
|
||||
53: 'DNS', 67: 'DHCP', 68: 'DHCP', 69: 'TFTP', 80: 'HTTP',
|
||||
88: 'Kerberos', 110: 'POP3', 111: 'RPC', 119: 'NNTP', 123: 'NTP',
|
||||
135: 'MS-RPC', 137: 'NetBIOS-NS', 138: 'NetBIOS-DGM', 139: 'NetBIOS-SSN',
|
||||
143: 'IMAP', 161: 'SNMP', 162: 'SNMP-Trap', 179: 'BGP', 194: 'IRC',
|
||||
389: 'LDAP', 443: 'HTTPS', 445: 'SMB', 465: 'SMTPS', 500: 'IKE/ISAKMP',
|
||||
514: 'Syslog/RSH', 515: 'LPD', 587: 'SMTP-Submission', 631: 'IPP',
|
||||
636: 'LDAPS', 873: 'rsync', 993: 'IMAPS', 995: 'POP3S',
|
||||
1080: 'SOCKS', 1194: 'OpenVPN', 1433: 'MSSQL', 1521: 'Oracle',
|
||||
1723: 'PPTP', 1883: 'MQTT', 2049: 'NFS', 2181: 'Zookeeper',
|
||||
2222: 'SSH-alt', 2375: 'Docker', 2376: 'Docker-TLS', 3000: 'Grafana',
|
||||
3306: 'MySQL', 3389: 'RDP', 3690: 'SVN', 4444: 'Meterpreter',
|
||||
5000: 'Flask/Dev', 5432: 'PostgreSQL', 5601: 'Kibana', 5672: 'AMQP/RabbitMQ',
|
||||
5900: 'VNC', 5984: 'CouchDB', 6379: 'Redis', 6443: 'Kubernetes-API',
|
||||
7474: 'Neo4j', 8080: 'HTTP-Alt', 8443: 'HTTPS-Alt', 8500: 'Consul',
|
||||
8888: 'Jupyter/HTTP-Alt', 9000: 'SonarQube/PHP-FPM', 9001: 'Tor/Supervisor',
|
||||
9042: 'Cassandra', 9090: 'Prometheus/Cockpit', 9092: 'Kafka',
|
||||
9200: 'Elasticsearch', 9300: 'Elasticsearch-node', 9418: 'Git',
|
||||
10000: 'Webmin', 11211: 'Memcached', 15672: 'RabbitMQ-Mgmt',
|
||||
27017: 'MongoDB', 27018: 'MongoDB', 27019: 'MongoDB', 50000: 'DB2',
|
||||
}
|
||||
|
||||
PROBE_MAP = {
|
||||
21: b'',
|
||||
22: b'',
|
||||
23: b'',
|
||||
25: b'',
|
||||
80: b'HEAD / HTTP/1.0\r\nHost: localhost\r\n\r\n',
|
||||
110: b'',
|
||||
143: b'',
|
||||
443: b'',
|
||||
3306: b'',
|
||||
5432: b'',
|
||||
6379: b'INFO\r\n',
|
||||
8080: b'HEAD / HTTP/1.0\r\nHost: localhost\r\n\r\n',
|
||||
8443: b'',
|
||||
8888: b'HEAD / HTTP/1.0\r\nHost: localhost\r\n\r\n',
|
||||
9200: b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n',
|
||||
27017: b'',
|
||||
}
|
||||
|
||||
|
||||
def _push(q: queue.Queue, event_type: str, data: dict) -> None:
|
||||
data['type'] = event_type
|
||||
data['ts'] = time.time()
|
||||
q.put(data)
|
||||
|
||||
|
||||
def _grab_banner(sock: socket.socket, port: int, timeout: float = 2.0) -> str:
|
||||
try:
|
||||
sock.settimeout(timeout)
|
||||
probe = PROBE_MAP.get(port, b'')
|
||||
if probe:
|
||||
sock.sendall(probe)
|
||||
raw = sock.recv(2048)
|
||||
return raw.decode('utf-8', errors='replace').strip()[:512]
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
|
||||
def _identify_service(port: int, banner: str) -> str:
|
||||
bl = banner.lower()
|
||||
if 'ssh-' in bl:
|
||||
return 'SSH'
|
||||
if 'ftp' in bl or '220 ' in bl[:10]:
|
||||
return 'FTP'
|
||||
if 'smtp' in bl or ('220 ' in bl and 'mail' in bl):
|
||||
return 'SMTP'
|
||||
if 'http/' in bl or '<html' in bl or '<!doctype' in bl:
|
||||
return 'HTTP'
|
||||
if 'mysql' in bl:
|
||||
return 'MySQL'
|
||||
if 'redis' in bl:
|
||||
return 'Redis'
|
||||
if 'mongo' in bl:
|
||||
return 'MongoDB'
|
||||
if 'postgresql' in bl:
|
||||
return 'PostgreSQL'
|
||||
if 'rabbitmq' in bl:
|
||||
return 'RabbitMQ'
|
||||
if 'elastic' in bl:
|
||||
return 'Elasticsearch'
|
||||
return SERVICE_MAP.get(port, 'unknown')
|
||||
|
||||
|
||||
def _scan_port(host: str, port: int, timeout: float) -> Optional[dict]:
|
||||
"""TCP connect scan a single port. Returns port info dict or None if closed."""
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
err = sock.connect_ex((host, port))
|
||||
if err == 0:
|
||||
banner = _grab_banner(sock, port)
|
||||
sock.close()
|
||||
service = _identify_service(port, banner)
|
||||
return {
|
||||
'port': port,
|
||||
'protocol': 'tcp',
|
||||
'state': 'open',
|
||||
'service': service,
|
||||
'banner': banner,
|
||||
}
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _run_nmap_scan(host: str, ports: list, options: dict, q: queue.Queue,
|
||||
job: dict) -> Optional[list]:
|
||||
"""Run nmap and parse output. Returns list of port dicts or None if nmap unavailable."""
|
||||
import shutil
|
||||
nmap = shutil.which('nmap')
|
||||
if not nmap:
|
||||
_push(q, 'warning', {'msg': 'nmap not found — falling back to TCP connect scan'})
|
||||
return None
|
||||
|
||||
port_str = ','.join(str(p) for p in sorted(ports))
|
||||
cmd = [nmap, '-Pn', '--open', '-p', port_str]
|
||||
|
||||
if options.get('service_detection'):
|
||||
cmd += ['-sV', '--version-intensity', '5']
|
||||
if options.get('os_detection'):
|
||||
cmd += ['-O', '--osscan-guess']
|
||||
if options.get('aggressive'):
|
||||
cmd += ['-A']
|
||||
if options.get('timing'):
|
||||
cmd += [f'-T{options["timing"]}']
|
||||
else:
|
||||
cmd += ['-T4']
|
||||
|
||||
cmd += ['-oN', '-', '--host-timeout', '120s', host]
|
||||
|
||||
_push(q, 'nmap_start', {'cmd': ' '.join(cmd[:-1] + ['<target>'])})
|
||||
|
||||
open_ports = []
|
||||
os_guess = ''
|
||||
nmap_raw_lines = []
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, bufsize=1)
|
||||
for line in proc.stdout:
|
||||
line = line.rstrip()
|
||||
if job.get('cancel'):
|
||||
proc.terminate()
|
||||
break
|
||||
nmap_raw_lines.append(line)
|
||||
_push(q, 'nmap_line', {'line': line})
|
||||
|
||||
# Parse open port lines: "80/tcp open http Apache httpd 2.4.54"
|
||||
stripped = line.strip()
|
||||
if '/tcp' in stripped or '/udp' in stripped:
|
||||
parts = stripped.split()
|
||||
if len(parts) >= 2 and parts[1] == 'open':
|
||||
port_proto = parts[0].split('/')
|
||||
port_num = int(port_proto[0])
|
||||
proto = port_proto[1] if len(port_proto) > 1 else 'tcp'
|
||||
service = parts[2] if len(parts) > 2 else SERVICE_MAP.get(port_num, 'unknown')
|
||||
version = ' '.join(parts[3:]) if len(parts) > 3 else ''
|
||||
open_ports.append({
|
||||
'port': port_num,
|
||||
'protocol': proto,
|
||||
'state': 'open',
|
||||
'service': service,
|
||||
'banner': version,
|
||||
})
|
||||
_push(q, 'port_open', {
|
||||
'port': port_num, 'protocol': proto,
|
||||
'service': service, 'banner': version,
|
||||
})
|
||||
# OS detection line
|
||||
if 'OS details:' in line or 'Running:' in line:
|
||||
os_guess = line.split(':', 1)[-1].strip()
|
||||
|
||||
proc.wait(timeout=10)
|
||||
except Exception as exc:
|
||||
_push(q, 'warning', {'msg': f'nmap error: {exc} — falling back to TCP connect scan'})
|
||||
return None
|
||||
|
||||
return open_ports, os_guess, '\n'.join(nmap_raw_lines)
|
||||
|
||||
|
||||
def _socket_scan(host: str, ports: list, timeout: float, concurrency: int,
|
||||
q: queue.Queue, job: dict) -> list:
|
||||
"""Concurrent TCP connect scan. Returns list of open port dicts."""
|
||||
open_ports = []
|
||||
lock = threading.Lock()
|
||||
scanned = [0]
|
||||
total = len(ports)
|
||||
start = time.time()
|
||||
|
||||
def worker(port: int):
|
||||
if job.get('cancel'):
|
||||
return
|
||||
result = _scan_port(host, port, timeout)
|
||||
with lock:
|
||||
scanned[0] += 1
|
||||
done = scanned[0]
|
||||
if result:
|
||||
with lock:
|
||||
open_ports.append(result)
|
||||
_push(q, 'port_open', {
|
||||
'port': result['port'], 'protocol': result['protocol'],
|
||||
'service': result['service'], 'banner': result['banner'],
|
||||
})
|
||||
# Progress every 25 ports or on first/last
|
||||
if done == 1 or done % 25 == 0 or done == total:
|
||||
elapsed = time.time() - start
|
||||
rate = done / elapsed if elapsed > 0 else 0
|
||||
eta = int((total - done) / rate) if rate > 0 else 0
|
||||
_push(q, 'progress', {
|
||||
'current': done, 'total': total,
|
||||
'pct': round(done * 100 / total),
|
||||
'eta': f'{eta}s' if eta < 3600 else f'{eta//60}m',
|
||||
'port': port,
|
||||
'open_count': len(open_ports),
|
||||
})
|
||||
|
||||
sem = threading.Semaphore(concurrency)
|
||||
threads = []
|
||||
|
||||
for port in sorted(ports):
|
||||
if job.get('cancel'):
|
||||
break
|
||||
sem.acquire()
|
||||
def _run(p=port):
|
||||
try:
|
||||
worker(p)
|
||||
finally:
|
||||
sem.release()
|
||||
t = threading.Thread(target=_run, daemon=True)
|
||||
threads.append(t)
|
||||
t.start()
|
||||
|
||||
for t in threads:
|
||||
t.join(timeout=timeout + 1)
|
||||
|
||||
return sorted(open_ports, key=lambda x: x['port'])
|
||||
|
||||
|
||||
def _do_scan(job_id: str, host: str, ports: list, options: dict) -> None:
|
||||
"""Main scan worker — runs in a background thread."""
|
||||
job = _jobs[job_id]
|
||||
q = job['q']
|
||||
start = time.time()
|
||||
|
||||
_push(q, 'start', {
|
||||
'target': host, 'total_ports': len(ports),
|
||||
'mode': options.get('mode', 'custom'),
|
||||
})
|
||||
|
||||
# Resolve hostname
|
||||
ip = host
|
||||
try:
|
||||
ip = socket.gethostbyname(host)
|
||||
if ip != host:
|
||||
_push(q, 'info', {'msg': f'Resolved {host} → {ip}'})
|
||||
except Exception:
|
||||
_push(q, 'warning', {'msg': f'Could not resolve {host} — using as-is'})
|
||||
|
||||
open_ports = []
|
||||
os_guess = ''
|
||||
nmap_raw = ''
|
||||
|
||||
try:
|
||||
use_nmap = options.get('use_nmap', False)
|
||||
timeout = float(options.get('timeout', 1.0))
|
||||
concurrency = int(options.get('concurrency', 100))
|
||||
|
||||
if use_nmap:
|
||||
nmap_result = _run_nmap_scan(ip, ports, options, q, job)
|
||||
if nmap_result is not None:
|
||||
open_ports, os_guess, nmap_raw = nmap_result
|
||||
else:
|
||||
# fallback
|
||||
_push(q, 'info', {'msg': 'Running TCP connect scan fallback...'})
|
||||
open_ports = _socket_scan(ip, ports, timeout, concurrency, q, job)
|
||||
else:
|
||||
_push(q, 'info', {'msg': f'Scanning {len(ports)} ports on {ip} '
|
||||
f'(concurrency={concurrency}, timeout={timeout}s)'})
|
||||
open_ports = _socket_scan(ip, ports, timeout, concurrency, q, job)
|
||||
|
||||
duration = round(time.time() - start, 2)
|
||||
|
||||
result = {
|
||||
'target': host,
|
||||
'ip': ip,
|
||||
'scan_time': datetime.now(timezone.utc).isoformat(),
|
||||
'duration': duration,
|
||||
'ports_scanned': len(ports),
|
||||
'open_ports': open_ports,
|
||||
'os_guess': os_guess,
|
||||
'nmap_raw': nmap_raw,
|
||||
'options': options,
|
||||
}
|
||||
job['result'] = result
|
||||
|
||||
_push(q, 'done', {
|
||||
'open_count': len(open_ports),
|
||||
'ports_scanned': len(ports),
|
||||
'duration': duration,
|
||||
'os_guess': os_guess,
|
||||
})
|
||||
|
||||
except Exception as exc:
|
||||
_push(q, 'error', {'msg': str(exc)})
|
||||
finally:
|
||||
job['done'] = True
|
||||
|
||||
|
||||
# ── Routes ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@port_scanner_bp.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
return render_template('port_scanner.html')
|
||||
|
||||
|
||||
@port_scanner_bp.route('/start', methods=['POST'])
|
||||
@login_required
|
||||
def start_scan():
|
||||
data = request.get_json(silent=True) or {}
|
||||
host = data.get('target', '').strip()
|
||||
if not host:
|
||||
return jsonify({'ok': False, 'error': 'Target required'})
|
||||
|
||||
mode = data.get('mode', 'common')
|
||||
|
||||
if mode == 'quick':
|
||||
ports = list(QUICK_PORTS)
|
||||
elif mode == 'common':
|
||||
ports = list(COMMON_PORTS)
|
||||
elif mode == 'full':
|
||||
ports = list(range(1, 65536))
|
||||
elif mode == 'custom':
|
||||
raw = data.get('ports', '').strip()
|
||||
ports = _parse_port_spec(raw)
|
||||
if not ports:
|
||||
return jsonify({'ok': False, 'error': 'No valid ports in custom range'})
|
||||
else:
|
||||
ports = list(COMMON_PORTS)
|
||||
|
||||
options = {
|
||||
'mode': mode,
|
||||
'use_nmap': bool(data.get('use_nmap', False)),
|
||||
'service_detection': bool(data.get('service_detection', False)),
|
||||
'os_detection': bool(data.get('os_detection', False)),
|
||||
'aggressive': bool(data.get('aggressive', False)),
|
||||
'timing': data.get('timing', '4'),
|
||||
'timeout': float(data.get('timeout', 1.0)),
|
||||
'concurrency': min(int(data.get('concurrency', 200)), 500),
|
||||
}
|
||||
|
||||
job_id = str(uuid.uuid4())[:8]
|
||||
job = {'q': queue.Queue(), 'result': None, 'done': False, 'cancel': False}
|
||||
_jobs[job_id] = job
|
||||
|
||||
t = threading.Thread(target=_do_scan, args=(job_id, host, ports, options), daemon=True)
|
||||
t.start()
|
||||
|
||||
return jsonify({'ok': True, 'job_id': job_id, 'port_count': len(ports)})
|
||||
|
||||
|
||||
@port_scanner_bp.route('/stream/<job_id>')
|
||||
@login_required
|
||||
def stream(job_id):
|
||||
job = _jobs.get(job_id)
|
||||
if not job:
|
||||
def err_gen():
|
||||
yield f"data: {json.dumps({'type': 'error', 'msg': 'Job not found'})}\n\n"
|
||||
return Response(err_gen(), mimetype='text/event-stream')
|
||||
|
||||
def generate():
|
||||
q = job['q']
|
||||
while True:
|
||||
try:
|
||||
item = q.get(timeout=0.5)
|
||||
yield f"data: {json.dumps(item)}\n\n"
|
||||
if item.get('type') in ('done', 'error'):
|
||||
break
|
||||
except queue.Empty:
|
||||
if job['done']:
|
||||
break
|
||||
yield ': keepalive\n\n'
|
||||
|
||||
return Response(generate(), mimetype='text/event-stream',
|
||||
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
|
||||
|
||||
|
||||
@port_scanner_bp.route('/result/<job_id>')
|
||||
@login_required
|
||||
def get_result(job_id):
|
||||
job = _jobs.get(job_id)
|
||||
if not job:
|
||||
return jsonify({'ok': False, 'error': 'Job not found'})
|
||||
if not job['done']:
|
||||
return jsonify({'ok': True, 'done': False})
|
||||
return jsonify({'ok': True, 'done': True, 'result': job['result']})
|
||||
|
||||
|
||||
@port_scanner_bp.route('/cancel/<job_id>', methods=['POST'])
|
||||
@login_required
|
||||
def cancel_scan(job_id):
|
||||
job = _jobs.get(job_id)
|
||||
if job:
|
||||
job['cancel'] = True
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def _parse_port_spec(spec: str) -> list:
|
||||
"""Parse port specification: '22,80,443', '1-1024', '22,80-100,443'."""
|
||||
ports = set()
|
||||
for part in spec.split(','):
|
||||
part = part.strip()
|
||||
if '-' in part:
|
||||
try:
|
||||
lo, hi = part.split('-', 1)
|
||||
lo, hi = int(lo.strip()), int(hi.strip())
|
||||
if 1 <= lo <= hi <= 65535:
|
||||
ports.update(range(lo, hi + 1))
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
p = int(part)
|
||||
if 1 <= p <= 65535:
|
||||
ports.add(p)
|
||||
except ValueError:
|
||||
pass
|
||||
return sorted(ports)
|
||||
@@ -162,6 +162,38 @@ def mms():
|
||||
return jsonify({'ok': True, 'messages': msgs, 'count': len(msgs)})
|
||||
|
||||
|
||||
@rcs_tools_bp.route('/rcs-via-mms')
|
||||
@login_required
|
||||
def rcs_via_mms():
|
||||
thread_id = request.args.get('thread_id')
|
||||
tid = int(thread_id) if thread_id else None
|
||||
limit = int(request.args.get('limit', 200))
|
||||
msgs = _get_rcs().read_rcs_via_mms(tid, limit)
|
||||
rcs_count = sum(1 for m in msgs if m.get('is_rcs'))
|
||||
return jsonify({'ok': True, 'messages': msgs, 'count': len(msgs), 'rcs_count': rcs_count})
|
||||
|
||||
|
||||
@rcs_tools_bp.route('/rcs-only')
|
||||
@login_required
|
||||
def rcs_only():
|
||||
limit = int(request.args.get('limit', 200))
|
||||
msgs = _get_rcs().read_rcs_only(limit)
|
||||
return jsonify({'ok': True, 'messages': msgs, 'count': len(msgs)})
|
||||
|
||||
|
||||
@rcs_tools_bp.route('/rcs-threads')
|
||||
@login_required
|
||||
def rcs_threads():
|
||||
threads = _get_rcs().read_rcs_threads()
|
||||
return jsonify({'ok': True, 'threads': threads, 'count': len(threads)})
|
||||
|
||||
|
||||
@rcs_tools_bp.route('/backup-rcs-xml', methods=['POST'])
|
||||
@login_required
|
||||
def backup_rcs_xml():
|
||||
return jsonify(_get_rcs().backup_rcs_to_xml())
|
||||
|
||||
|
||||
@rcs_tools_bp.route('/drafts')
|
||||
@login_required
|
||||
def drafts():
|
||||
|
||||
@@ -21,42 +21,83 @@ _debug_enabled: bool = False
|
||||
_debug_handler_installed: bool = False
|
||||
|
||||
|
||||
def _buf_append(level: str, name: str, raw: str, msg: str, exc: str = '') -> None:
|
||||
"""Thread-safe append to the debug buffer."""
|
||||
entry: dict = {'ts': time.time(), 'level': level, 'name': name, 'raw': raw, 'msg': msg}
|
||||
if exc:
|
||||
entry['exc'] = exc
|
||||
_debug_buffer.append(entry)
|
||||
|
||||
|
||||
class _DebugBufferHandler(logging.Handler):
|
||||
"""Captures log records into the in-memory debug buffer."""
|
||||
"""Captures ALL log records into the in-memory debug buffer (always active)."""
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
if not _debug_enabled:
|
||||
return
|
||||
try:
|
||||
entry: dict = {
|
||||
'ts': record.created,
|
||||
'level': record.levelname,
|
||||
'name': record.name,
|
||||
'raw': record.getMessage(),
|
||||
'msg': self.format(record),
|
||||
}
|
||||
exc_text = ''
|
||||
if record.exc_info:
|
||||
import traceback as _tb
|
||||
entry['exc'] = ''.join(_tb.format_exception(*record.exc_info))
|
||||
_debug_buffer.append(entry)
|
||||
exc_text = ''.join(_tb.format_exception(*record.exc_info))
|
||||
_buf_append(
|
||||
level=record.levelname,
|
||||
name=record.name,
|
||||
raw=record.getMessage(),
|
||||
msg=self.format(record),
|
||||
exc=exc_text,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class _PrintCapture:
|
||||
"""Wraps sys.stdout or sys.stderr — passes through AND feeds lines to the debug buffer."""
|
||||
|
||||
def __init__(self, original, level: str = 'STDOUT'):
|
||||
self._orig = original
|
||||
self._level = level
|
||||
self._line_buf = ''
|
||||
|
||||
def write(self, text: str) -> int:
|
||||
self._orig.write(text)
|
||||
self._line_buf += text
|
||||
while '\n' in self._line_buf:
|
||||
line, self._line_buf = self._line_buf.split('\n', 1)
|
||||
if line.strip():
|
||||
_buf_append(self._level, 'print', line, line)
|
||||
return len(text)
|
||||
|
||||
def flush(self) -> None:
|
||||
self._orig.flush()
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._orig, name)
|
||||
|
||||
|
||||
def _ensure_debug_handler() -> None:
|
||||
"""Install logging handler + stdout/stderr capture once, at startup."""
|
||||
global _debug_handler_installed
|
||||
if _debug_handler_installed:
|
||||
return
|
||||
# Logging handler
|
||||
handler = _DebugBufferHandler()
|
||||
handler.setLevel(logging.DEBUG)
|
||||
handler.setFormatter(logging.Formatter('%(name)s — %(message)s'))
|
||||
root = logging.getLogger()
|
||||
root.addHandler(handler)
|
||||
# Lower root level to DEBUG so records reach the handler
|
||||
if root.level == logging.NOTSET or root.level > logging.DEBUG:
|
||||
root.setLevel(logging.DEBUG)
|
||||
# stdout / stderr capture
|
||||
import sys as _sys
|
||||
if not isinstance(_sys.stdout, _PrintCapture):
|
||||
_sys.stdout = _PrintCapture(_sys.stdout, 'STDOUT')
|
||||
if not isinstance(_sys.stderr, _PrintCapture):
|
||||
_sys.stderr = _PrintCapture(_sys.stderr, 'STDERR')
|
||||
_debug_handler_installed = True
|
||||
|
||||
|
||||
# Install immediately so we capture from process start, not just after toggle
|
||||
_ensure_debug_handler()
|
||||
|
||||
settings_bp = Blueprint('settings', __name__, url_prefix='/settings')
|
||||
|
||||
|
||||
@@ -429,28 +470,42 @@ def discovery_stop():
|
||||
@settings_bp.route('/debug/toggle', methods=['POST'])
|
||||
@login_required
|
||||
def debug_toggle():
|
||||
"""Enable or disable the debug log capture."""
|
||||
"""Enable or disable the debug console UI (capture always runs)."""
|
||||
global _debug_enabled
|
||||
data = request.get_json(silent=True) or {}
|
||||
_debug_enabled = bool(data.get('enabled', False))
|
||||
if _debug_enabled:
|
||||
_ensure_debug_handler()
|
||||
logging.getLogger('autarch.debug').info('Debug console enabled')
|
||||
logging.getLogger('autarch.debug').info('Debug console opened')
|
||||
return jsonify({'ok': True, 'enabled': _debug_enabled})
|
||||
|
||||
|
||||
@settings_bp.route('/debug/stream')
|
||||
@login_required
|
||||
def debug_stream():
|
||||
"""SSE stream — pushes new log records to the browser as they arrive."""
|
||||
"""SSE stream — pushes log records to the browser as they arrive.
|
||||
|
||||
On connect: sends the last 200 buffered entries as history, then streams
|
||||
new entries live. Handles deque wrap-around correctly.
|
||||
"""
|
||||
def generate():
|
||||
sent = 0
|
||||
buf = list(_debug_buffer)
|
||||
# Send last 200 entries as catch-up history
|
||||
history_start = max(0, len(buf) - 200)
|
||||
for entry in buf[history_start:]:
|
||||
yield f"data: {json.dumps(entry)}\n\n"
|
||||
sent = len(buf)
|
||||
|
||||
while True:
|
||||
time.sleep(0.2)
|
||||
buf = list(_debug_buffer)
|
||||
while sent < len(buf):
|
||||
n = len(buf)
|
||||
if sent > n:
|
||||
# deque wrapped; re-orient to current tail
|
||||
sent = n
|
||||
while sent < n:
|
||||
yield f"data: {json.dumps(buf[sent])}\n\n"
|
||||
sent += 1
|
||||
time.sleep(0.25)
|
||||
yield ': keepalive\n\n'
|
||||
|
||||
return Response(generate(), mimetype='text/event-stream',
|
||||
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
|
||||
|
||||
Reference in New Issue
Block a user