2603.7 added dark/light mode
This commit is contained in:
@@ -1,2 +1,2 @@
|
|||||||
"""portspoof_py — asyncio Python rewrite of the portspoof TCP honeypot."""
|
"""portspoof_py — asyncio Python rewrite of the portspoof TCP honeypot."""
|
||||||
__version__ = '2603.6'
|
__version__ = '2603.7'
|
||||||
|
|||||||
@@ -82,10 +82,11 @@ _PASSWD_HTML = """\
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>portspoof — change password</title>
|
<title>portspoof — change password</title>
|
||||||
<style>{css}</style>
|
<style>{css}</style>
|
||||||
|
{theme_script}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>portspoof admin</h1>
|
<h1>portspoof admin</h1>
|
||||||
<p class="sub"><a href="/">← dashboard</a></p>
|
<p class="sub"><a href="/">← dashboard</a> {theme_btn}</p>
|
||||||
|
|
||||||
{banner_html}
|
{banner_html}
|
||||||
|
|
||||||
@@ -126,7 +127,8 @@ def _render_passwd(msg: str = '', msg_ok: bool = True) -> str:
|
|||||||
if msg:
|
if msg:
|
||||||
cls = 'ok' if msg_ok else 'err'
|
cls = 'ok' if msg_ok else 'err'
|
||||||
banner_html = f'<div class="banner {cls}">{html.escape(msg)}</div>'
|
banner_html = f'<div class="banner {cls}">{html.escape(msg)}</div>'
|
||||||
return _PASSWD_HTML.format(css=_CONFIG_CSS, banner_html=banner_html)
|
return _PASSWD_HTML.format(css=_CONFIG_CSS, banner_html=banner_html,
|
||||||
|
theme_script=_THEME_SCRIPT, theme_btn=_THEME_BTN)
|
||||||
|
|
||||||
|
|
||||||
# ── settings page ─────────────────────────────────────────────────────────────
|
# ── settings page ─────────────────────────────────────────────────────────────
|
||||||
@@ -139,10 +141,11 @@ _SETTINGS_HTML = """\
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>portspoof — settings</title>
|
<title>portspoof — settings</title>
|
||||||
<style>{css}</style>
|
<style>{css}</style>
|
||||||
|
{theme_script}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>portspoof admin</h1>
|
<h1>portspoof admin</h1>
|
||||||
<p class="sub"><a href="/">← dashboard</a></p>
|
<p class="sub"><a href="/">← dashboard</a> {theme_btn}</p>
|
||||||
|
|
||||||
{banner_html}
|
{banner_html}
|
||||||
|
|
||||||
@@ -182,45 +185,66 @@ def _render_settings(settings: Settings, msg: str = '', msg_ok: bool = True) ->
|
|||||||
banner_html=banner_html,
|
banner_html=banner_html,
|
||||||
delay_min=settings.delay_min,
|
delay_min=settings.delay_min,
|
||||||
delay_max=settings.delay_max,
|
delay_max=settings.delay_max,
|
||||||
|
theme_script=_THEME_SCRIPT,
|
||||||
|
theme_btn=_THEME_BTN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── config page ───────────────────────────────────────────────────────────────
|
# ── config page ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_CONFIG_CSS = """
|
_CONFIG_CSS = """
|
||||||
|
:root {
|
||||||
|
--bg: #0d1117; --bg2: #161b22; --border: #30363d;
|
||||||
|
--text: #c9d1d9; --dim: #8b949e; --heading: #e6edf3; --blue: #58a6ff;
|
||||||
|
--green: #238636; --green2: #2ea043;
|
||||||
|
--btn-sec: #21262d; --btn-sec2: #30363d;
|
||||||
|
--ok-bg: #0f2a1a; --ok-border: #238636; --ok-text: #3fb950;
|
||||||
|
--err-bg: #2d1212; --err-border: #f85149; --err-text: #f85149;
|
||||||
|
}
|
||||||
|
html.light {
|
||||||
|
--bg: #f6f8fa; --bg2: #ffffff; --border: #d0d7de;
|
||||||
|
--text: #24292f; --dim: #57606a; --heading: #1f2328; --blue: #0969da;
|
||||||
|
--green: #1f883d; --green2: #2ea043;
|
||||||
|
--btn-sec: #f6f8fa; --btn-sec2: #eaeef2;
|
||||||
|
--ok-bg: #dafbe1; --ok-border: #1f883d; --ok-text: #1a7f37;
|
||||||
|
--err-bg: #fff0ee; --err-border: #ff8182; --err-text: #cf222e;
|
||||||
|
}
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
body { font-family: 'Courier New', monospace; background: #0d1117; color: #c9d1d9;
|
body { font-family: 'Courier New', monospace; background: var(--bg); color: var(--text);
|
||||||
padding: 24px; font-size: 13px; line-height: 1.6; }
|
padding: 24px; font-size: 13px; line-height: 1.6; }
|
||||||
a { color: #58a6ff; text-decoration: none; }
|
a { color: var(--blue); text-decoration: none; }
|
||||||
h1 { color: #58a6ff; font-size: 20px; margin-bottom: 4px; }
|
h1 { color: var(--blue); font-size: 20px; margin-bottom: 4px; }
|
||||||
h2 { color: #e6edf3; font-size: 14px; margin: 24px 0 12px; }
|
h2 { color: var(--heading); font-size: 14px; margin: 24px 0 12px; }
|
||||||
.sub { color: #8b949e; font-size: 12px; margin-bottom: 24px; }
|
.sub { color: var(--dim); font-size: 12px; margin-bottom: 24px; }
|
||||||
.card { background: #161b22; border: 1px solid #30363d; border-radius: 6px;
|
.card { background: var(--bg2); border: 1px solid var(--border); border-radius: 6px;
|
||||||
padding: 20px; max-width: 600px; }
|
padding: 20px; max-width: 600px; }
|
||||||
.field { margin-bottom: 14px; }
|
.field { margin-bottom: 14px; }
|
||||||
label { display: block; color: #8b949e; font-size: 11px; text-transform: uppercase;
|
label { display: block; color: var(--dim); font-size: 11px; text-transform: uppercase;
|
||||||
letter-spacing: 1px; margin-bottom: 4px; }
|
letter-spacing: 1px; margin-bottom: 4px; }
|
||||||
input[type=text], input[type=number], input[type=password], input[type=email] {
|
input[type=text], input[type=number], input[type=password], input[type=email] {
|
||||||
width: 100%; background: #0d1117; border: 1px solid #30363d; color: #c9d1d9;
|
width: 100%; background: var(--bg); border: 1px solid var(--border); color: var(--text);
|
||||||
padding: 7px 10px; border-radius: 4px; font-family: inherit; font-size: 13px; }
|
padding: 7px 10px; border-radius: 4px; font-family: inherit; font-size: 13px; }
|
||||||
input[type=text]:focus, input[type=number]:focus,
|
input[type=text]:focus, input[type=number]:focus,
|
||||||
input[type=password]:focus, input[type=email]:focus {
|
input[type=password]:focus, input[type=email]:focus {
|
||||||
outline: none; border-color: #58a6ff; }
|
outline: none; border-color: var(--blue); }
|
||||||
.hint { color: #8b949e; font-size: 11px; margin-top: 3px; }
|
.hint { color: var(--dim); font-size: 11px; margin-top: 3px; }
|
||||||
.row2 { display: flex; gap: 12px; }
|
.row2 { display: flex; gap: 12px; }
|
||||||
.row2 .field { flex: 1; }
|
.row2 .field { flex: 1; }
|
||||||
.check-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
|
.check-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
|
||||||
input[type=checkbox] { width: 15px; height: 15px; accent-color: #238636; }
|
input[type=checkbox] { width: 15px; height: 15px; accent-color: var(--green); }
|
||||||
.actions { display: flex; gap: 10px; margin-top: 20px; flex-wrap: wrap; }
|
.actions { display: flex; gap: 10px; margin-top: 20px; flex-wrap: wrap; }
|
||||||
button { background: #238636; border: 1px solid #2ea043; color: #fff;
|
button { background: var(--green); border: 1px solid var(--green2); color: #fff;
|
||||||
padding: 7px 16px; border-radius: 4px; cursor: pointer;
|
padding: 7px 16px; border-radius: 4px; cursor: pointer;
|
||||||
font-family: inherit; font-size: 13px; }
|
font-family: inherit; font-size: 13px; }
|
||||||
button:hover { background: #2ea043; }
|
button:hover { background: var(--green2); }
|
||||||
button.secondary { background: #21262d; border-color: #30363d; color: #c9d1d9; }
|
button.secondary { background: var(--btn-sec); border-color: var(--border); color: var(--text); }
|
||||||
button.secondary:hover { background: #30363d; }
|
button.secondary:hover { background: var(--btn-sec2); }
|
||||||
.banner { padding: 10px 14px; border-radius: 4px; margin-bottom: 16px; font-size: 12px; }
|
.banner { padding: 10px 14px; border-radius: 4px; margin-bottom: 16px; font-size: 12px; }
|
||||||
.banner.ok { background: #0f2a1a; border: 1px solid #238636; color: #3fb950; }
|
.banner.ok { background: var(--ok-bg); border: 1px solid var(--ok-border); color: var(--ok-text); }
|
||||||
.banner.err { background: #2d1212; border: 1px solid #f85149; color: #f85149; }
|
.banner.err { background: var(--err-bg); border: 1px solid var(--err-border); color: var(--err-text); }
|
||||||
|
.link-btn { background: none; border: none; color: var(--blue); padding: 0;
|
||||||
|
font-family: inherit; font-size: 13px; cursor: pointer; }
|
||||||
|
.link-btn:hover { text-decoration: underline; }
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_CONFIG_HTML = """\
|
_CONFIG_HTML = """\
|
||||||
@@ -231,10 +255,11 @@ _CONFIG_HTML = """\
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>portspoof — email alerts</title>
|
<title>portspoof — email alerts</title>
|
||||||
<style>{css}</style>
|
<style>{css}</style>
|
||||||
|
{theme_script}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>portspoof admin</h1>
|
<h1>portspoof admin</h1>
|
||||||
<p class="sub"><a href="/">← dashboard</a></p>
|
<p class="sub"><a href="/">← dashboard</a> {theme_btn}</p>
|
||||||
|
|
||||||
{banner_html}
|
{banner_html}
|
||||||
|
|
||||||
@@ -338,6 +363,8 @@ def _render_config(notifier: Notifier, msg: str = '', msg_ok: bool = True) -> st
|
|||||||
return _CONFIG_HTML.format(
|
return _CONFIG_HTML.format(
|
||||||
css=_CONFIG_CSS,
|
css=_CONFIG_CSS,
|
||||||
banner_html=banner_html,
|
banner_html=banner_html,
|
||||||
|
theme_script=_THEME_SCRIPT,
|
||||||
|
theme_btn=_THEME_BTN,
|
||||||
enabled_chk='checked' if cfg.get('enabled') else '',
|
enabled_chk='checked' if cfg.get('enabled') else '',
|
||||||
smtp_host=html.escape(cfg.get('smtp_host', '')),
|
smtp_host=html.escape(cfg.get('smtp_host', '')),
|
||||||
smtp_port=cfg.get('smtp_port', 587),
|
smtp_port=cfg.get('smtp_port', 587),
|
||||||
@@ -377,58 +404,84 @@ def _redirect(location: str) -> bytes:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── shared theme script ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_THEME_SCRIPT = """\
|
||||||
|
<script>
|
||||||
|
(function(){{if(localStorage.getItem('theme')==='light')document.documentElement.classList.add('light')}})();
|
||||||
|
function toggleTheme(){{var e=document.documentElement;e.classList.toggle('light');localStorage.setItem('theme',e.classList.contains('light')?'light':'dark');}}
|
||||||
|
</script>"""
|
||||||
|
|
||||||
|
_THEME_BTN = '· <button type="button" class="link-btn" onclick="toggleTheme()">light/dark</button>'
|
||||||
|
|
||||||
# ── HTML template ─────────────────────────────────────────────────────────────
|
# ── HTML template ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_CSS = """
|
_CSS = """
|
||||||
|
:root {
|
||||||
|
--bg: #0d1117; --bg2: #161b22; --border: #30363d;
|
||||||
|
--text: #c9d1d9; --dim: #8b949e; --heading: #e6edf3; --blue: #58a6ff;
|
||||||
|
--red: #f78166; --ip-color: #79c0ff;
|
||||||
|
--hover: #1c2128; --badge: #21262d; --sep: #21262d; --footer: #484f58;
|
||||||
|
}
|
||||||
|
html.light {
|
||||||
|
--bg: #f6f8fa; --bg2: #ffffff; --border: #d0d7de;
|
||||||
|
--text: #24292f; --dim: #57606a; --heading: #1f2328; --blue: #0969da;
|
||||||
|
--red: #cf222e; --ip-color: #0550ae;
|
||||||
|
--hover: #f3f4f6; --badge: #eaeef2; --sep: #d8dee4; --footer: #8c959f;
|
||||||
|
}
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
body {
|
body {
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
background: #0d1117; color: #c9d1d9;
|
background: var(--bg); color: var(--text);
|
||||||
padding: 24px; font-size: 13px; line-height: 1.5;
|
padding: 24px; font-size: 13px; line-height: 1.5;
|
||||||
}
|
}
|
||||||
a { color: #58a6ff; text-decoration: none; }
|
a { color: var(--blue); text-decoration: none; }
|
||||||
h1 { color: #58a6ff; font-size: 20px; margin-bottom: 4px; }
|
h1 { color: var(--blue); font-size: 20px; margin-bottom: 4px; }
|
||||||
.sub { color: #8b949e; font-size: 12px; margin-bottom: 24px; }
|
.sub { color: var(--dim); font-size: 12px; margin-bottom: 24px; }
|
||||||
.row { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 20px; }
|
.row { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 20px; }
|
||||||
.card {
|
.card {
|
||||||
background: #161b22; border: 1px solid #30363d;
|
background: var(--bg2); border: 1px solid var(--border);
|
||||||
border-radius: 6px; padding: 16px;
|
border-radius: 6px; padding: 16px;
|
||||||
}
|
}
|
||||||
.card.stat { min-width: 155px; flex: 1; }
|
.card.stat { min-width: 155px; flex: 1; }
|
||||||
.card.half { flex: 1; min-width: 260px; }
|
.card.half { flex: 1; min-width: 260px; }
|
||||||
.card.full { width: 100%; }
|
.card.full { width: 100%; }
|
||||||
.card h3 {
|
.card h3 {
|
||||||
color: #8b949e; font-size: 11px;
|
color: var(--dim); font-size: 11px;
|
||||||
text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px;
|
text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.card .val { font-size: 28px; color: #58a6ff; font-weight: bold; }
|
.card .val { font-size: 28px; color: var(--blue); font-weight: bold; }
|
||||||
.card .val.last { font-size: 13px; margin-top: 4px; }
|
.card .val.last { font-size: 13px; margin-top: 4px; }
|
||||||
h2 { color: #e6edf3; font-size: 14px; margin-bottom: 12px; }
|
h2 { color: var(--heading); font-size: 14px; margin-bottom: 12px; }
|
||||||
table { width: 100%; border-collapse: collapse; }
|
table { width: 100%; border-collapse: collapse; }
|
||||||
th {
|
th {
|
||||||
color: #8b949e; font-size: 11px; text-transform: uppercase;
|
color: var(--dim); font-size: 11px; text-transform: uppercase;
|
||||||
letter-spacing: 0.5px; padding: 6px 10px; text-align: left;
|
letter-spacing: 0.5px; padding: 6px 10px; text-align: left;
|
||||||
border-bottom: 1px solid #21262d;
|
border-bottom: 1px solid var(--sep);
|
||||||
}
|
}
|
||||||
td { padding: 5px 10px; border-bottom: 1px solid #21262d; }
|
td { padding: 5px 10px; border-bottom: 1px solid var(--sep); }
|
||||||
tr:last-child td { border-bottom: none; }
|
tr:last-child td { border-bottom: none; }
|
||||||
tr:hover td { background: #1c2128; }
|
tr:hover td { background: var(--hover); }
|
||||||
.port { color: #f78166; }
|
.port { color: var(--red); }
|
||||||
.ip { color: #79c0ff; }
|
.ip { color: var(--ip-color); }
|
||||||
.ts { color: #8b949e; font-size: 11px; }
|
.ts { color: var(--dim); font-size: 11px; }
|
||||||
.cc { color: #8b949e; font-size: 11px; }
|
.cc { color: var(--dim); font-size: 11px; }
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block; background: #21262d;
|
display: inline-block; background: var(--badge);
|
||||||
border-radius: 3px; padding: 1px 7px; font-size: 11px;
|
border-radius: 3px; padding: 1px 7px; font-size: 11px;
|
||||||
margin-left: 6px; vertical-align: middle;
|
margin-left: 6px; vertical-align: middle;
|
||||||
}
|
}
|
||||||
.empty { color: #8b949e; font-style: italic; padding: 8px 0; }
|
.empty { color: var(--dim); font-style: italic; padding: 8px 0; }
|
||||||
.footer { color: #484f58; font-size: 11px; margin-top: 24px; }
|
.footer { color: var(--footer); font-size: 11px; margin-top: 24px; }
|
||||||
.link-btn {
|
.link-btn {
|
||||||
background: none; border: none; color: #58a6ff; padding: 0;
|
background: none; border: none; color: var(--blue); padding: 0;
|
||||||
font-family: inherit; font-size: inherit; cursor: pointer;
|
font-family: inherit; font-size: inherit; cursor: pointer;
|
||||||
}
|
}
|
||||||
.link-btn:hover { text-decoration: underline; }
|
.link-btn:hover { text-decoration: underline; }
|
||||||
|
html.light svg line { stroke: var(--sep); }
|
||||||
|
html.light svg text { fill: var(--dim); }
|
||||||
|
html.light svg polygon { fill: rgba(9,105,218,0.12); opacity: 1; }
|
||||||
|
html.light svg polyline { stroke: var(--blue); }
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_HTML = """\
|
_HTML = """\
|
||||||
@@ -440,6 +493,7 @@ _HTML = """\
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>portspoof admin</title>
|
<title>portspoof admin</title>
|
||||||
<style>{css}</style>
|
<style>{css}</style>
|
||||||
|
{theme_script}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>portspoof admin</h1>
|
<h1>portspoof admin</h1>
|
||||||
@@ -451,7 +505,7 @@ _HTML = """\
|
|||||||
· <a href="/settings">settings</a> ·
|
· <a href="/settings">settings</a> ·
|
||||||
<a href="/config">email alerts</a> ·
|
<a href="/config">email alerts</a> ·
|
||||||
<a href="/api/stats" target="_blank">JSON stats</a> ·
|
<a href="/api/stats" target="_blank">JSON stats</a> ·
|
||||||
<a href="/passwd">change password</a>
|
<a href="/passwd">change password</a> {theme_btn}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- stat cards -->
|
<!-- stat cards -->
|
||||||
@@ -627,6 +681,8 @@ async def _render_dashboard(stats: Stats, cfg: Config) -> str:
|
|||||||
|
|
||||||
return _HTML.format(
|
return _HTML.format(
|
||||||
css=_CSS,
|
css=_CSS,
|
||||||
|
theme_script=_THEME_SCRIPT,
|
||||||
|
theme_btn=_THEME_BTN,
|
||||||
total=stats.total,
|
total=stats.total,
|
||||||
cpm=stats.connections_per_minute(),
|
cpm=stats.connections_per_minute(),
|
||||||
uptime=stats.uptime_str(),
|
uptime=stats.uptime_str(),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "portspoof-py"
|
name = "portspoof-py"
|
||||||
version = "2603.6"
|
version = "2603.7"
|
||||||
description = "Python asyncio rewrite of the portspoof TCP honeypot"
|
description = "Python asyncio rewrite of the portspoof TCP honeypot"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
Reference in New Issue
Block a user