2603.5 - Speed in web settings
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
"""portspoof_py — asyncio Python rewrite of the portspoof TCP honeypot."""
|
||||
__version__ = '2603.4'
|
||||
__version__ = '2603.5'
|
||||
|
||||
@@ -22,6 +22,7 @@ from . import __version__
|
||||
from .config import Config
|
||||
from .geo import country as _geo_country
|
||||
from .notifier import Notifier
|
||||
from .settings import Settings
|
||||
from .stats import Stats
|
||||
|
||||
# ── credentials ───────────────────────────────────────────────────────────────
|
||||
@@ -128,6 +129,62 @@ def _render_passwd(msg: str = '', msg_ok: bool = True) -> str:
|
||||
return _PASSWD_HTML.format(css=_CONFIG_CSS, banner_html=banner_html)
|
||||
|
||||
|
||||
# ── settings page ─────────────────────────────────────────────────────────────
|
||||
|
||||
_SETTINGS_HTML = """\
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>portspoof — settings</title>
|
||||
<style>{css}</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>portspoof admin</h1>
|
||||
<p class="sub"><a href="/">← dashboard</a></p>
|
||||
|
||||
{banner_html}
|
||||
|
||||
<div class="card">
|
||||
<h2>Connection delay</h2>
|
||||
<form method="post" action="/api/settings">
|
||||
<div class="row2">
|
||||
<div class="field">
|
||||
<label>Min delay (seconds)</label>
|
||||
<input type="number" name="delay_min" value="{delay_min}"
|
||||
min="0" max="60" step="0.1">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Max delay (seconds)</label>
|
||||
<input type="number" name="delay_max" value="{delay_max}"
|
||||
min="0" max="60" step="0.1">
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">Random delay applied before sending a banner to each connecting client.</p>
|
||||
<div class="actions">
|
||||
<button type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def _render_settings(settings: Settings, msg: str = '', msg_ok: bool = True) -> str:
|
||||
banner_html = ''
|
||||
if msg:
|
||||
cls = 'ok' if msg_ok else 'err'
|
||||
banner_html = f'<div class="banner {cls}">{html.escape(msg)}</div>'
|
||||
return _SETTINGS_HTML.format(
|
||||
css=_CONFIG_CSS,
|
||||
banner_html=banner_html,
|
||||
delay_min=settings.delay_min,
|
||||
delay_max=settings.delay_max,
|
||||
)
|
||||
|
||||
|
||||
# ── config page ───────────────────────────────────────────────────────────────
|
||||
|
||||
_CONFIG_CSS = """
|
||||
@@ -391,7 +448,8 @@ _HTML = """\
|
||||
<form method="post" action="/api/stats/reset" style="display:inline">
|
||||
<button type="submit" class="link-btn">reset stats</button>
|
||||
</form>
|
||||
· <a href="/config">email alerts</a> ·
|
||||
· <a href="/settings">settings</a> ·
|
||||
<a href="/config">email alerts</a> ·
|
||||
<a href="/api/stats" target="_blank">JSON stats</a> ·
|
||||
<a href="/passwd">change password</a>
|
||||
</p>
|
||||
@@ -592,6 +650,7 @@ async def _handle(
|
||||
cfg: Config,
|
||||
creds_mut: list,
|
||||
passwd_file: str,
|
||||
settings: Settings,
|
||||
notifier: Optional['Notifier'] = None,
|
||||
) -> None:
|
||||
try:
|
||||
@@ -704,6 +763,33 @@ async def _handle(
|
||||
body = json.dumps(notifier.get_config_safe(), indent=2)
|
||||
ct = 'application/json'
|
||||
|
||||
# ── settings ──────────────────────────────────────────────────────────
|
||||
elif path == '/settings':
|
||||
msg = ''
|
||||
msg_ok = True
|
||||
if 'saved' in qs:
|
||||
msg = 'Settings saved.'
|
||||
elif 'err' in qs:
|
||||
msg = 'Invalid values — min must be ≥ 0 and max must be ≥ min.'
|
||||
msg_ok = False
|
||||
body = _render_settings(settings, msg, msg_ok)
|
||||
ct = 'text/html; charset=utf-8'
|
||||
|
||||
elif path == '/api/settings' and method == 'POST':
|
||||
form = _parse_post_body(text)
|
||||
try:
|
||||
d_min = float(form.get('delay_min', settings.delay_min))
|
||||
d_max = float(form.get('delay_max', settings.delay_max))
|
||||
if d_min < 0 or d_max < d_min:
|
||||
raise ValueError
|
||||
settings.delay_min = d_min
|
||||
settings.delay_max = d_max
|
||||
writer.write(_redirect('/settings?saved=1'))
|
||||
except (ValueError, TypeError):
|
||||
writer.write(_redirect('/settings?err=1'))
|
||||
await writer.drain()
|
||||
return
|
||||
|
||||
# ── password change ───────────────────────────────────────────────────
|
||||
elif path == '/passwd':
|
||||
msg = ''
|
||||
@@ -815,12 +901,15 @@ async def run_admin(
|
||||
notifier: Optional['Notifier'] = None,
|
||||
ssl_context: Optional[ssl.SSLContext] = None,
|
||||
passwd_file: str = 'admin.passwd',
|
||||
settings: Optional[Settings] = None,
|
||||
) -> None:
|
||||
"""Start the admin HTTP(S) server and run until stop_event is set."""
|
||||
creds_mut = list(creds) # mutable so password changes propagate across requests
|
||||
if settings is None:
|
||||
settings = Settings()
|
||||
|
||||
def handler(r, w):
|
||||
asyncio.create_task(_handle(r, w, stats, cfg, creds_mut, passwd_file, notifier))
|
||||
asyncio.create_task(_handle(r, w, stats, cfg, creds_mut, passwd_file, settings, notifier))
|
||||
|
||||
server = await asyncio.start_server(
|
||||
handler,
|
||||
|
||||
@@ -15,6 +15,7 @@ from .iptables import add_rules, check_root, remove_rules
|
||||
from .logger import Logger
|
||||
from .notifier import Notifier
|
||||
from .server import run_server
|
||||
from .settings import Settings
|
||||
from .stats import Stats
|
||||
|
||||
|
||||
@@ -150,6 +151,7 @@ def main(argv=None) -> int:
|
||||
notifier: Notifier = Notifier(args.email_config)
|
||||
print(f'[portspoof] email config: {args.email_config}')
|
||||
|
||||
settings = Settings()
|
||||
stats = Stats()
|
||||
log = Logger(args.log_file, verbose=args.verbose, stats=stats, notifier=notifier)
|
||||
|
||||
@@ -171,13 +173,13 @@ def main(argv=None) -> int:
|
||||
try:
|
||||
bind_ip: Optional[str] = args.bind_ip or None
|
||||
tasks.append(asyncio.create_task(
|
||||
run_server(cfg, log, bind_ip, args.port, args.verbose, stop_event)
|
||||
run_server(cfg, log, bind_ip, args.port, args.verbose, stop_event, settings)
|
||||
))
|
||||
if args.admin_port:
|
||||
tasks.append(asyncio.create_task(
|
||||
run_admin(stats, cfg, args.admin_host, args.admin_port, creds, stop_event,
|
||||
notifier=notifier, ssl_context=ssl_context,
|
||||
passwd_file=args.admin_passwd)
|
||||
passwd_file=args.admin_passwd, settings=settings)
|
||||
))
|
||||
await asyncio.gather(*tasks)
|
||||
finally:
|
||||
|
||||
@@ -7,11 +7,9 @@ import socket
|
||||
import struct
|
||||
from typing import Optional
|
||||
|
||||
_DELAY_MIN = 1.0 # seconds
|
||||
_DELAY_MAX = 5.0 # seconds
|
||||
|
||||
from .config import Config
|
||||
from .logger import Logger
|
||||
from .settings import Settings
|
||||
|
||||
# Linux kernel constants
|
||||
SOL_IP = 0
|
||||
@@ -39,6 +37,7 @@ async def _handle(
|
||||
cfg: Config,
|
||||
log: Logger,
|
||||
verbose: bool,
|
||||
settings: Settings,
|
||||
) -> None:
|
||||
sock = writer.get_extra_info('socket')
|
||||
peername = writer.get_extra_info('peername') or ('?.?.?.?', 0)
|
||||
@@ -59,7 +58,7 @@ async def _handle(
|
||||
)
|
||||
|
||||
try:
|
||||
await asyncio.sleep(random.uniform(_DELAY_MIN, _DELAY_MAX))
|
||||
await asyncio.sleep(random.uniform(settings.delay_min, settings.delay_max))
|
||||
if banner:
|
||||
writer.write(banner)
|
||||
await writer.drain()
|
||||
@@ -82,11 +81,12 @@ async def run_server(
|
||||
port: int,
|
||||
verbose: bool,
|
||||
stop_event: asyncio.Event,
|
||||
settings: Settings,
|
||||
) -> None:
|
||||
"""Start the asyncio TCP server and run until stop_event is set."""
|
||||
|
||||
def handler(r, w):
|
||||
asyncio.create_task(_handle(r, w, cfg, log, verbose))
|
||||
asyncio.create_task(_handle(r, w, cfg, log, verbose, settings))
|
||||
|
||||
server = await asyncio.start_server(
|
||||
handler,
|
||||
|
||||
8
portspoof_py/settings.py
Normal file
8
portspoof_py/settings.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""Mutable runtime settings shared between the server and admin interface."""
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
delay_min: float = 1.0 # seconds to wait before sending banner (minimum)
|
||||
delay_max: float = 5.0 # seconds to wait before sending banner (maximum)
|
||||
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
|
||||
|
||||
[project]
|
||||
name = "portspoof-py"
|
||||
version = "2603.4"
|
||||
version = "2603.5"
|
||||
description = "Python asyncio rewrite of the portspoof TCP honeypot"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
Reference in New Issue
Block a user