From 646754c04d3e98e1bd97735efb9eb1acd186805f Mon Sep 17 00:00:00 2001 From: MasonLiu <2857911564@qq.com> Date: Thu, 10 Sep 2026 00:22:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=AE=8C=E5=96=84=E7=BD=91?= =?UTF-8?q?=E9=A1=B5=E9=80=BB=E8=BE=91=EF=BC=8C=E5=B7=A5=E5=85=B7=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E8=87=B3html=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/_guard.php | 1 + admin/articles.php | 5 +- admin/content.php | 32 +- admin/exttools.php | 4 +- admin/login.php | 14 +- admin/nav.php | 4 +- admin/tools.php | 4 +- admin/visits.php | 502 ++++++++++++++++++++++++++ api/site.php | 12 + assets/css/common.css | 64 ++++ assets/img/favicon.ico | Bin 0 -> 38700 bytes assets/js/codec.js | 11 +- assets/js/common.js | 182 +++++++++- assets/js/gmcrypto.js | 19 +- assets/js/ip.js | 2 +- func/{av.php => av.html} | 36 +- func/{codec.php => codec.html} | 34 +- func/{gmcodec.php => gmcodec.html} | 39 +- func/{ip.php => ip.html} | 62 ++-- func/{password.php => password.html} | 36 +- func/{persist.php => persist.html} | 40 ++- func/{qrcode.php => qrcode.html} | 42 ++- includes/db.php | 519 ++++++++++++++++++++++++++- includes/layout.php | 69 ++++ index.php | 32 +- nav.php | 8 +- 26 files changed, 1606 insertions(+), 167 deletions(-) create mode 100644 admin/visits.php create mode 100644 api/site.php create mode 100644 assets/img/favicon.ico rename func/{av.php => av.html} (77%) rename func/{codec.php => codec.html} (80%) rename func/{gmcodec.php => gmcodec.html} (86%) rename func/{ip.php => ip.html} (62%) rename func/{password.php => password.html} (78%) rename func/{persist.php => persist.html} (87%) rename func/{qrcode.php => qrcode.html} (74%) diff --git a/admin/_guard.php b/admin/_guard.php index 65fce23..1a19aca 100644 --- a/admin/_guard.php +++ b/admin/_guard.php @@ -23,6 +23,7 @@ function admin_topbar(string $active = ''): void 'articles' => ['articles.php', '文章管理'], 'exttools' => ['exttools.php', '外部工具'], 'content' => ['content.php', '内容与顺序'], + 'visits' => ['visits.php', '访问信息'], ]; foreach ($links as $k => $lk) { $cls = ($active === $k) ? 'active' : ''; diff --git a/admin/articles.php b/admin/articles.php index f2095f1..fce61f3 100644 --- a/admin/articles.php +++ b/admin/articles.php @@ -33,7 +33,8 @@ function renumber_sorts(PDO $pdo): void function msg_block(string $kind, string $text): string { - return '
' . he($text) . '
'; + // 改为右下角 Toast:注入 hpToastMsg,由 common.js 在页面加载后自动弹出 + return ''; } $msgKind = ''; @@ -236,7 +237,7 @@ admin_topbar('articles');

文章库(第五大板块)的文章维护:正文为标准 Markdown,图片统一上传到 data/articles/img 目录。

-
保存成功,文章库前台已同步更新。
+ diff --git a/admin/content.php b/admin/content.php index 70aaac7..6efb9ea 100644 --- a/admin/content.php +++ b/admin/content.php @@ -31,7 +31,8 @@ function remove_old_logo(): void function alert_html(string $kind, string $msg): void { - echo '
' . he($msg) . '
'; + // 改为右下角 Toast:注入 hpToastMsg,由 common.js 在页面加载后自动弹出 + echo ''; } $msgKind = ''; @@ -152,6 +153,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $msgKind = 'ok'; $msgText = '首页顶部标识栏背景已保存。'; break; + case 'hero_mode': + $mode = (string)($_POST['hero_mode'] ?? 'weather'); + if (!in_array($mode, ['weather', 'info', 'blank'], true)) { + $mode = 'weather'; + } + settings_set('hero_mode', $mode); + touch_last_updated(); + $modeText = ['weather' => '天气模式', 'info' => '访客信息模式', 'blank' => '空白精简模式'][$mode]; + $msgKind = 'ok'; + $msgText = '首页顶部组件模式已保存:' . $modeText . '。'; + break; case 'cdn': settings_set('cdn_ranges', trim((string)($_POST['cdn_ranges'] ?? ''))); touch_last_updated(); @@ -401,6 +413,24 @@ admin_topbar('content'); + +
+
首页顶部组件(Hero 下方横幅区)
+ +
+ +
+ + + +
+
+
+

+ 天气模式(默认):横幅展示第三方天气条;访客信息模式:横幅显示当前访问者 IP、浏览器、操作系统、内核、屏幕分辨率与系统语言;空白模式:隐藏横幅,仅保留站点信息与时间信息,顶部高度自动缩减。 +

+
+
底部信息
diff --git a/admin/exttools.php b/admin/exttools.php index 9c0b7df..720b0c8 100644 --- a/admin/exttools.php +++ b/admin/exttools.php @@ -135,10 +135,10 @@ admin_topbar('exttools');

-
保存成功,库首页外部工具区已同步更新。
+ -
+ diff --git a/admin/login.php b/admin/login.php index f7d726c..b41f10d 100644 --- a/admin/login.php +++ b/admin/login.php @@ -8,6 +8,7 @@ require_once dirname(__DIR__) . '/includes/layout.php'; $err = ''; $loggedOut = isset($_GET['out']); +$curIp = client_ip(); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $username = trim((string)($_POST['username'] ?? '')); @@ -15,12 +16,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($username === '' || $password === '') { $err = '请输入用户名和密码。'; } elseif (hp_login($username, $password)) { + // 正常登录成功:清零本 IP 风控计数(避免携带历史失败次数,防止管理员自误封) + risk_clear_for_ip($curIp); header('Location: nav.php'); exit; } else { - // 轻量防爆破延时 + // 轻量防爆破延时(命中黑名单的 IP 在访问本页前即被 access_boot 拦截) usleep(500000); - $err = '用户名或密码错误。'; + $risk = risk_login_fail($curIp); + if ($risk[1]) { + $err = '用户名或密码错误。'; + // $err = '登录失败次数过多(本次累计 ' . $risk[0] . ' 次,达到阈值 ' . $risk[2] . ' 次),当前 IP 已被风控自动封禁,全站(含后台登录)访问均被拒绝。请稍后从未被封禁的网络进入后台「访问信息 → IP 黑名单」移除该规则解除,或联系站长处理。'; + } else { + $err = '用户名或密码错误。'; + } } } @@ -47,7 +56,6 @@ layout_head('后台登录');
-

初始账号:admin / admin123(首次登录后请在“内容与顺序”中修改)

diff --git a/admin/nav.php b/admin/nav.php index 6583070..111e710 100644 --- a/admin/nav.php +++ b/admin/nav.php @@ -162,10 +162,10 @@ admin_topbar('nav'); -
保存成功,前台页面已同步更新。
+ -
+
diff --git a/admin/tools.php b/admin/tools.php index ba17919..851ed14 100644 --- a/admin/tools.php +++ b/admin/tools.php @@ -148,10 +148,10 @@ admin_topbar('tools');

-
保存成功,功能区首页已同步更新。
+ -
+
diff --git a/admin/visits.php b/admin/visits.php new file mode 100644 index 0000000..2d4f145 --- /dev/null +++ b/admin/visits.php @@ -0,0 +1,502 @@ +prepare('INSERT INTO settings (key, value) VALUES (?, ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value'); + $st->execute([$key, $value]); +} + +/* 批量删除黑名单(按 id 列表),同步清除单 IP 规则的风控计数;返回删除条数 */ +function bl_delete_ids(PDO $pdo, array $ids): int +{ + $ids = array_values(array_unique(array_filter(array_map('intval', $ids), function ($v) { return $v > 0; }))); + if (!$ids) return 0; + $ph = implode(',', array_fill(0, count($ids), '?')); + $st = $pdo->prepare('SELECT rule FROM ip_blacklist WHERE id IN (' . $ph . ')'); + $st->execute($ids); + $rules = $st->fetchAll(); + $pdo->prepare('DELETE FROM ip_blacklist WHERE id IN (' . $ph . ')')->execute($ids); + foreach ($rules as $r) { + if (filter_var((string)$r['rule'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + risk_clear_for_ip((string)$r['rule']); + } + } + return count($rules); +} + +/* ---------- POST:黑名单增删 / 清空日志 / 风控规则 ---------- */ +$msgKind = ''; +$msgText = ''; +$banChanged = false; +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!csrf_verify()) { + $msgKind = 'err'; + $msgText = '安全校验失败,请刷新页面重试。'; + } else { + $act = (string)($_POST['act'] ?? ''); + switch ($act) { + case 'ban_add': + $rule = trim((string)($_POST['rule'] ?? '')); + $note = trim((string)($_POST['note'] ?? '')); + if ($rule === '') { + $msgKind = 'err'; + $msgText = '请填写要拦截的 IP / 网段。'; + break; + } + $parsed = blacklist_parse_rule($rule); + if (!$parsed[0]) { + $msgKind = 'err'; + $msgText = $parsed[3]; + break; + } + $st = $pdo->prepare('INSERT INTO ip_blacklist (rule, note, created_at, source) VALUES (?,?,?,?)'); + $st->execute([$rule, $note, date('Y-m-d H:i:s'), 'manual']); + $banChanged = true; + $msgKind = 'ok'; + $msgText = '黑名单规则已添加,匹配的访问将立即被拒绝。'; + break; + case 'ban_del': + $id = max(0, (int)($_POST['id'] ?? 0)); + if ($id > 0) { + $bs = $pdo->prepare('SELECT rule FROM ip_blacklist WHERE id = ? LIMIT 1'); + $bs->execute([$id]); + $brow = $bs->fetch(); + $pdo->prepare('DELETE FROM ip_blacklist WHERE id = ?')->execute([$id]); + // 解封单 IP 时同步清除其风控计数,避免“刚解封又被历史计数封回” + if ($brow && filter_var((string)$brow['rule'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + risk_clear_for_ip((string)$brow['rule']); + } + $banChanged = true; + $msgKind = 'ok'; + $msgText = '黑名单规则已移除。'; + } else { + $msgKind = 'err'; + $msgText = '无效的黑名单记录。'; + } + break; + case 'log_clear': + @file_put_contents(ACCESS_LOG_FILE, '', LOCK_EX); + $msgKind = 'ok'; + $msgText = '访问日志已清空(data/vistors.log)。'; + break; + case 'risk_save': + $clip = function ($v, $d) { $raw = trim((string)($v ?? '')); $x = ($raw === '') ? (int)$d : (int)$raw; return max(1, min(1000000, $x)); }; + settings_set('risk_login_on', isset($_POST['r_login_on']) ? '1' : '0'); + settings_set('risk_login_n', (string)$clip($_POST['r_login_n'] ?? null, 10)); + settings_set('risk_404_on', isset($_POST['r_404_on']) ? '1' : '0'); + settings_set('risk_404_n', (string)$clip($_POST['r_404_n'] ?? null, 100)); + settings_set('risk_rate_on', isset($_POST['r_rate_on']) ? '1' : '0'); + settings_set('risk_rate_n', (string)$clip($_POST['r_rate_n'] ?? null, 120)); + settings_set('risk_tip_main', trim((string)($_POST['r_tip_main'] ?? ''))); + settings_set('risk_tip_sub', trim((string)($_POST['r_tip_sub'] ?? ''))); + $msgKind = 'ok'; + $msgText = '风控规则已保存并即时生效。'; + break; + case 'risk_clear': + risk_clear_all(); + $msgKind = 'ok'; + $msgText = '全部风险计数已清空(黑名单本身不受影响)。'; + break; + case 'ban_batch': + $n = bl_delete_ids($pdo, (array)($_POST['bid'] ?? [])); + $banChanged = true; + $msgKind = $n > 0 ? 'ok' : 'err'; + $msgText = $n > 0 ? '已批量移除 ' . $n . ' 条黑名单规则(对应单 IP 计数已清除)。' : '未勾选任何需要移除的规则。'; + break; + case 'ban_clear_auto': + $rows = $pdo->query("SELECT rule FROM ip_blacklist WHERE source = 'auto'")->fetchAll(); + $cnt = count($rows); + $pdo->exec("DELETE FROM ip_blacklist WHERE source = 'auto'"); + foreach ($rows as $r) { + if (filter_var((string)$r['rule'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + risk_clear_for_ip((string)$r['rule']); + } + } + blacklist_rules(true); + $banChanged = true; + $msgKind = 'ok'; + $msgText = '已一键移除全部风控自动封禁(共 ' . $cnt . ' 条),相关 IP 风控计数已同步清除。'; + break; + case 'ban_clear_all': + $pdo->exec('DELETE FROM ip_blacklist'); + risk_clear_all(); + blacklist_rules(true); + $banChanged = true; + $msgKind = 'ok'; + $msgText = '已清空全部黑名单,风险计数一并清零。'; + break; + default: + $msgKind = 'err'; + $msgText = '未知操作。'; + } + } +} + +/* ---------- 概要统计(数据来源:data/vistors.log) ---------- */ +$entries = visitors_entries(); +$statTotal = count($entries); +$ipSet = []; +foreach ($entries as $e) { + $ipSet[$e['ip']] = 1; +} +$statIps = count($ipSet); +$statToday = 0; +$todayStart = date('Y-m-d 00:00:00'); +foreach ($entries as $e) { + if ($e['created_at'] >= $todayStart) { + $statToday++; + } +} +function vis_scalar(PDO $pdo, string $sql): int +{ + $v = $pdo->query($sql)->fetchColumn(); + return (int)$v; +} +$statBan = vis_scalar($pdo, 'SELECT COUNT(*) FROM ip_blacklist'); + +/* ---------- 筛选与分页 ---------- */ +$mode = (string)($_GET['mode'] ?? 'full'); +if (!in_array($mode, ['full', 'compact'], true)) { + $mode = 'full'; +} +$ipF = trim((string)($_GET['ip'] ?? '')); +$q = trim((string)($_GET['q'] ?? '')); +$ipF = substr($ipF, 0, 64); +$q = substr($q, 0, 100); +$page = max(1, (int)($_GET['page'] ?? 1)); +$perPage = 30; + +// entries 已按新→旧排列,内存中过滤 / 聚合后分页 +$logs = []; +$total = 0; +$totalPages = 1; +if ($mode === 'compact') { + // 精简模式:按 IP 聚合(最近访问时间 / IP / 访问次数),首次出现即该 IP 最新一条 + $grp = []; + $order = []; + foreach ($entries as $e) { + if ($ipF !== '' && stripos($e['ip'], $ipF) === false) continue; + if ($q !== '' && stripos($e['url'], $q) === false && stripos($e['method'], $q) === false) continue; + if (!isset($grp[$e['ip']])) { + $grp[$e['ip']] = ['ip' => $e['ip'], 'last_at' => $e['created_at'], 'cnt' => 0]; + $order[] = $e['ip']; + } + $grp[$e['ip']]['cnt']++; + } + $all = []; + foreach ($order as $ip) { + $all[] = $grp[$ip]; + } + $total = count($all); + $totalPages = max(1, (int)ceil($total / $perPage)); + if ($page > $totalPages) { + $page = $totalPages; + } + $off = ($page - 1) * $perPage; + $logs = array_slice($all, $off, $perPage); +} else { + // 完整模式:逐条明细 + $all = []; + foreach ($entries as $e) { + if ($ipF !== '' && stripos($e['ip'], $ipF) === false) continue; + if ($q !== '' && stripos($e['url'], $q) === false && stripos($e['method'], $q) === false) continue; + $all[] = $e; + } + $total = count($all); + $totalPages = max(1, (int)ceil($total / $perPage)); + if ($page > $totalPages) { + $page = $totalPages; + } + $off = ($page - 1) * $perPage; + $logs = array_slice($all, $off, $perPage); +} + +/* ---------- 链接工具(保留当前筛选参数) ---------- */ +function vis_url(array $ov): string +{ + $p = $_GET; + foreach ($ov as $k => $v) { + if ($v === null || $v === '') { + unset($p[$k]); + } else { + $p[$k] = $v; + } + } + if (!$p) { + return 'visits.php'; + } + return 'visits.php?' . http_build_query($p); +} + +/* ---------- 当前访问者信息(用于黑名单自查提示) ---------- */ +$curIp = client_ip(); +if ($curIp === '') { + $curIp = '未知'; +} +$curHit = null; +if ($curIp !== '未知') { + if ($banChanged) { + blacklist_rules(true); + } + $curHit = blacklist_ip_hit($curIp); +} + +/* ---------- 自动风控规则配置与当前 IP 计数 ---------- */ +$rcfg = risk_rule_cfg(); +$curRisk = ($curIp !== '未知') ? risk_row($curIp) : null; + +/* ---------- 黑名单规则(直接读表展示,便于核对备注与来源) ---------- */ +$banRows = $pdo->query('SELECT id, rule, note, created_at, source FROM ip_blacklist ORDER BY id DESC')->fetchAll(); +$banAutoCount = 0; +foreach ($banRows as $b) { + if (((string)($b['source'] ?? 'manual')) === 'auto') $banAutoCount++; +} + +layout_head('访问信息'); +admin_topbar('visits'); +?> +
+
+

访问信息

+

记录访客对本站的动态请求(含后台),完整模式逐条展示访问明细,精简模式按 IP 汇总次数;黑名单 / 自动风控对所有访问统一生效,命中即返回 403 风控提示页,后台与登录页不例外。

+ + + + + + +
+
访问概况
+
+
累计访问
+
独立 IP
+
今日访问
+
黑名单规则
+
+
+ + + 日志按行追加写入 data/vistors.log(制表符分隔:时间 / IP / 请求方式 / 响应码 / URL),完整保留不设上限;可自行归档或删除该文件。 +
+
+ + +
+
访问记录
+
+ + + + + 重置 +
+ + +
+ 共命中 ; + 当前第 / 页,每页 条。 +
+
+ + + + + + + + + + + + + + +
最近访问时间IP 地址访问次数
+ + + + + + + + + + + + + + + + + + +
访问时间IP 地址请求方式访问 URL响应码
= 200 && $code < 300) ? 'good' : (($code >= 300 && $code < 400) ? 'info' : (($code >= 400 && $code < 500) ? 'warn' : 'bad')); + ?> +
+ +
+ + 1): ?> +
+ + 1): ?>← 上一页 + 1) echo ''; + for ($p = $start; $p <= $end; $p++) { + if ($p === $page) { + echo '' . $p . ''; + } else { + echo '' . $p . ''; + } + } + if ($end < $totalPages) echo ''; + ?> + 下一页 → +
+ + +
+ +
+ + +
+
自动风控规则(达到阈值自动加入黑名单;命中前台与后台均返回 403 风控提示页,提示文案可在下方自定义)
+
+ +
+ + 失败次数 ≥ 次 → 永久封禁(建议 5-20) +
+
+ + 累计次数 ≥ 次 → 永久封禁(建议 50-300) +
+
+ + 60 秒内 ≥ 次 → 永久封禁(多人共用出口 IP 时建议关闭) +
+

计数方式:404 与登录失败只要发生即累计(规则关闭也照常计数,便于后台观察);规则启用且达到阈值时自动封禁,保存后即时生效。自动封禁在黑名单中标注“风控自动”,可随时移除解封(解封时同步清除该 IP 计数)。

+
+ 主提示(留空=默认) + +
+
+ 次行说明(留空=默认) + +
+ +
+
+ + + + 当前访问 IP()计数:登录失败 次 · 404 · 本分钟请求 + +
+
+ + +
+
IP 黑名单(全站拦截前台与后台,命中统一返回 403 风控提示页)
+
+ + + + +
+

+ 支持三种写法:单个 IP、CIDR 网段(如 203.0.113.0/24)、星号通配段(如 203.0.113.*192.168.*.*); + 目前仅按 IPv4 匹配。黑名单即时生效且不受「站长提示」开关影响,命中 IP 的前台与后台(含登录页)访问一律被拒绝。如需解除,请从未被封禁的网络进入后台移除该规则,或直接在 data/homepage.db 的 ip_blacklist 表中删除记录。 +

+
+ + 当前访问 IP: —— 命中规则「」(),全站(前台与后台)访问将被拦截 + + 当前访问 IP: —— 未命中任何黑名单规则 + +
+ + +
+
+ + + +
+ 0): ?> +
+ + +
+ +
+ + +
+
+ +
+ + + + 风控自动 + + 手动 + + (无备注)' ?> + +
+ + +
+
+ + +

尚未配置黑名单规则。启用上方「自动风控」后,异常 IP 会自动记录到这里;也可在下方手动添加规则。

+ +
+
+
+ + + + + + diff --git a/api/site.php b/api/site.php new file mode 100644 index 0000000..0b7345c --- /dev/null +++ b/api/site.php @@ -0,0 +1,12 @@ + setting_get('site_logo', '')], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); diff --git a/assets/css/common.css b/assets/css/common.css index 719a5f2..5365dc4 100644 --- a/assets/css/common.css +++ b/assets/css/common.css @@ -168,6 +168,52 @@ select { width: auto; min-width: 150px; } /* 第三方天气 iframe 内容为白底,无法跨域改色:深色主题下用反色滤镜融入当前背景 */ [data-theme="dark"] .weather-box iframe { filter: invert(0.9) hue-rotate(180deg) contrast(0.92) saturate(0.9); } +/* ---------- 首页顶部组件:访客信息模式(hero_mode=info) ---------- */ +.visitor-box { display: flex; justify-content: center; } +.vb-grid { + width: 100%; max-width: 800px; + display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; + background: rgba(255, 255, 255, .28); + border: 1px solid rgba(255, 255, 255, .22); + border-radius: 10px; overflow: hidden; +} +.vb-cell { background: rgba(10, 22, 44, .32); padding: 9px 16px; text-align: left; } +.vb-cell span { display: block; font-size: 11px; letter-spacing: .5px; opacity: .72; margin-bottom: 2px; } +.vb-cell b { font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; word-break: break-all; color: #fff; } + +/* ---------- 首页顶部组件:空白精简模式(hero_mode=blank) ---------- */ +.hero-slim { min-height: 0; padding: 16px 0 12px; } + +/* ============ 后台访问信息页 ============ */ +.vis-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; } +.vis-stat { background: var(--bg-2); border: 1px solid var(--line); border-radius: 12px; padding: 12px 16px; } +.vis-stat b { display: block; font-size: 22px; line-height: 1.3; font-variant-numeric: tabular-nums; } +.vis-stat span { font-size: 12px; color: var(--text-3); } +.v-filter { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; } +.v-filter input[type=text] { width: 200px; } +.v-filter input[type=text].w-s { width: 160px; } +.v-scroll { overflow-x: auto; margin-top: 4px; } +.v-tbl { width: 100%; border-collapse: collapse; font-size: 13px; } +.v-tbl th, .v-tbl td { padding: 8px 10px; text-align: left; vertical-align: top; } +.v-tbl th { color: var(--text-3); font-weight: 600; border-bottom: 1px solid var(--line-2); } +.v-tbl td { border-bottom: 1px solid var(--line); word-break: break-all; } +.v-tbl tr:last-child td { border-bottom: none; } +.v-nowrap { white-space: nowrap; } +.v-tag { display: inline-block; min-width: 44px; text-align: center; padding: 1px 8px; border-radius: 20px; font-size: 12px; border: 1px solid var(--line-2); color: var(--text-2); } +.v-tag.good { color: var(--ok); border-color: var(--ok); } +.v-tag.info { color: var(--brand); border-color: var(--brand); } +.v-tag.warn { color: var(--warn); border-color: var(--warn); } +.v-tag.bad { color: var(--danger); border-color: var(--danger); } +.pager { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 14px; } +.pager .pg-info { font-size: 13px; color: var(--text-3); margin-right: 4px; } +.bl-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 10px 0; border-top: 1px dashed var(--line); } +.bl-row:first-child { border-top: none; padding-top: 0; } +.bl-rule { font-family: Consolas, Menlo, monospace; background: var(--code-bg); color: var(--code-text); border-radius: 6px; padding: 2px 9px; font-size: 13px; } +.bl-note { color: var(--text-2); flex: 1 1 140px; min-width: 100px; font-size: 13px; } +.bl-time { font-size: 12px; color: var(--text-3); white-space: nowrap; } +.vis-ok-tip { background: rgba(22, 163, 74, .12); color: var(--ok); border-radius: 8px; padding: 7px 12px; font-size: 13px; display: inline-block; } +.vis-warn-tip { background: rgba(217, 119, 6, .12); color: var(--warn); border-radius: 8px; padding: 7px 12px; font-size: 13px; display: inline-block; } + /* ---------- 名言条 ---------- */ .quote-bar { background: var(--card); border-bottom: 1px solid var(--line); } .quote-inner { display: flex; align-items: center; justify-content: center; gap: 12px; padding: 12px 16px; text-align: center; } @@ -572,6 +618,9 @@ a.nav-btn[data-note]:hover::after { opacity: 1; transform: translateX(-50%) tran .hero-top { flex-direction: column; text-align: center; } .hero-meta { text-align: center; } .weather-box iframe { width: 100%; } + .visitor-box .vb-grid { grid-template-columns: repeat(2, 1fr); } + .vis-stats { grid-template-columns: repeat(2, 1fr); } + .v-filter input[type=text] { width: 100%; } .quote-inner { flex-wrap: wrap; } .boards { grid-template-columns: repeat(2, 1fr); gap: 12px; min-height: 0; } .board-panel { width: 100%; } @@ -761,3 +810,18 @@ a.nav-btn[data-note]:hover::after { opacity: 1; transform: translateX(-50%) tran .art-tg-txt { display: none; } .art-topbar-nav { gap: 4px; } } + +/* ---------- 右下角 Toast 通知(后台操作成功/失败即时提示,不影响页面布局) ---------- */ +.hp-toasts { position: fixed; right: 18px; bottom: 18px; z-index: 9990; display: flex; flex-direction: column; align-items: flex-end; gap: 10px; max-width: min(380px, calc(100vw - 36px)); pointer-events: none; } +.hp-toast { pointer-events: auto; display: flex; align-items: flex-start; gap: 10px; min-width: 220px; max-width: 100%; box-sizing: border-box; background: var(--card); color: var(--text-1); border: 1px solid var(--line-2); border-left: 4px solid var(--brand); border-radius: 12px; padding: 12px 14px; box-shadow: 0 10px 30px rgba(15,23,42,.18); font-size: 14px; line-height: 1.55; animation: hpToastIn .26s ease; } +.hp-toast.ok { border-left-color: var(--ok); } +.hp-toast.err { border-left-color: var(--danger); } +.hp-toast .ht-ic { flex: none; width: 18px; height: 18px; border-radius: 50%; color: #fff; font-size: 12px; font-weight: 800; display: inline-flex; align-items: center; justify-content: center; margin-top: 2px; } +.hp-toast.ok .ht-ic { background: var(--ok); } +.hp-toast.err .ht-ic { background: var(--danger); } +.hp-toast .ht-tx { flex: 1 1 auto; min-width: 0; word-break: break-word; } +.hp-toast .ht-x { flex: none; align-self: flex-start; margin-left: 2px; padding: 0 2px; border: 0; background: none; color: var(--text-3); font-size: 16px; line-height: 1; cursor: pointer; } +.hp-toast .ht-x:hover { color: var(--danger); } +.hp-toast.hide { opacity: 0; transform: translateY(8px); transition: opacity .22s ease, transform .22s ease; } +@keyframes hpToastIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } } +@media (max-width: 600px) { .hp-toasts { right: 12px; bottom: 12px; left: 12px; align-items: stretch; } .hp-toast { min-width: 0; } } diff --git a/assets/img/favicon.ico b/assets/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..edab23335eaaa20dd24680d1fb718bef94e5a7d5 GIT binary patch literal 38700 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 literal 0 HcmV?d00001 diff --git a/assets/js/codec.js b/assets/js/codec.js index 2ce13db..cd7995d 100644 --- a/assets/js/codec.js +++ b/assets/js/codec.js @@ -1,6 +1,6 @@ /* codec.js —— 编码/加解密纯前端算法库 * 提供:UTF-8、Base64、Base32、URL、Unicode、SHA 系列(Web Crypto 原生) - * MD5 / SHA-224 依赖 CryptoJS(由页面懒加载,双 CDN fallback) + * MD5 / SHA-224 依赖 CryptoJS(优先本地 assets/js/crypto-js.min.js,缺失则回退 CDN) */ (function (global) { 'use strict'; @@ -177,10 +177,11 @@ return cryptoJsDigest(algorithm, text); } - /* ---------- CryptoJS 懒加载(MD5 / SHA-224 等需要) ---------- */ + /* ---------- CryptoJS 懒加载(MD5 / SHA-224 等需要;优先本地,其次 CDN) ---------- */ var CDN_CRYPTO_JS = [ - 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js', - 'https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js' + '../assets/js/crypto-js.min.js', // 本地内置(离线可用) + 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js', // 回退 1 + 'https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js' // 回退 2 ]; var cryptoJSPromise = null; @@ -209,7 +210,7 @@ function cryptoJsDigest(algo, text) { return loadCryptoJS().then(function (C) { - if (!C) throw new Error('计算 ' + algo + ' 需要联网加载加密组件(crypto-js)失败,请检查网络'); + if (!C) throw new Error('计算 ' + algo + ' 的加密组件加载失败:请将 crypto-js.min.js 放入 assets/js/ 目录离线使用,或检查网络'); var method = CRYPTO_JS_METHODS[algo]; if (!method || typeof C[method] !== 'function') throw new Error('不支持的算法: ' + algo); return C[method](text).toString(C.enc.Hex); diff --git a/assets/js/common.js b/assets/js/common.js index bb786ce..012388c 100644 --- a/assets/js/common.js +++ b/assets/js/common.js @@ -342,10 +342,90 @@ var f = ev.target; if (!f || !f.getAttribute || !f.hasAttribute('data-confirm')) return; ev.preventDefault(); - siteConfirm(f.getAttribute('data-confirm')).then(function (ok) { if (ok) f.submit(); }); + siteConfirm(f.getAttribute('data-confirm')).then(function (ok) { + if (ok) { f.submit(); } else { hpClearScrollY(); } + }); }); } + /* ---------- 同页 POST 提交后保持滚动位置(整页刷新不再跳回顶部) ---------- */ + var HP_SCROLL_KEY = 'hp-scroll-pos'; + function hpSamePagePost(f) { + if (!f || !f.method) return false; + if (!/post/i.test(f.method)) return false; + if (f.target === '_blank') return false; + var a = (f.getAttribute('action') || '').trim(); + if (a === '') return true; // 无 action = 提交当前页 + try { + var cur = new URL(location.href.split('#')[0]); + var u = new URL(a, cur.href); + return u.origin === cur.origin && u.pathname === cur.pathname; + } catch (e) { return false; } + } + function hpSaveScrollY() { + try { + var y = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; + sessionStorage.setItem(HP_SCROLL_KEY, JSON.stringify({ p: location.pathname + location.search, y: y })); + } catch (e) { /* ignore */ } + } + function hpClearScrollY() { + try { sessionStorage.removeItem(HP_SCROLL_KEY); } catch (e) { /* ignore */ } + } + var hpScrollTarget = -1; + function hpRestoreScrollY() { + if (hpScrollTarget >= 0) { window.scrollTo(0, hpScrollTarget); return; } + try { + var raw = sessionStorage.getItem(HP_SCROLL_KEY); + if (!raw) return; + sessionStorage.removeItem(HP_SCROLL_KEY); + var d = JSON.parse(raw); + if (d && d.p === (location.pathname + location.search) && typeof d.y === 'number' && d.y > 0) { + hpScrollTarget = d.y; + window.scrollTo(0, hpScrollTarget); + } + } catch (e) { hpClearScrollY(); } + } + // 捕获阶段:同页 POST 提交前保存(按回车等也会触发 submit 事件) + document.addEventListener('submit', function (ev) { + if (hpSamePagePost(ev.target)) hpSaveScrollY(); + }, true); + // 捕获阶段:点击提交按钮时也保存(bindConfirmForms 确认后走 f.submit() 不触发 submit 事件) + document.addEventListener('click', function (ev) { + var t = ev.target; + var btn = (t && t.closest) ? t.closest('button[type="submit"], input[type="submit"], button:not([type])') : null; + if (!btn) return; + var f = btn.form || null; + if (hpSamePagePost(f)) hpSaveScrollY(); + }, true); + // 资源加载完成后再定位一次,防止图片等异步资源改变高度造成轻微偏移 + window.addEventListener('load', function () { + if (hpScrollTarget >= 0) { + window.scrollTo(0, hpScrollTarget); + hpScrollTarget = -1; + } + }); + + /* ---------- 首页访客信息(hero 信息模式:补齐屏幕分辨率 / 系统语言) ---------- */ + function initVisitorInfo() { + var box = $('.js-visitor-box'); + if (!box) return; + var langEl = $('.js-vlang', box); + if (langEl) { + var lang = ''; + try { lang = navigator.language || (navigator.languages && navigator.languages[0]) || ''; } catch (e) { /* ignore */ } + langEl.textContent = lang || '--'; + } + var scrEl = $('.js-vscreen', box); + if (scrEl) { + var w = 0, h = 0, dpr = 1; + try { w = window.screen.width || 0; h = window.screen.height || 0; dpr = window.devicePixelRatio || 1; } catch (e) { /* ignore */ } + if (w && h) { + var ratio = Math.round(dpr * 100); + scrEl.textContent = (ratio !== 100) ? (w + ' × ' + h + '(' + ratio + '%)') : (w + ' × ' + h); + } + } + } + /* ---------- 返回顶部(右下角按钮,滚动后出现;兼容内滚容器页面) ---------- */ function initToTop() { var btn = document.querySelector('.js-to-top'); @@ -380,8 +460,101 @@ measure(); } + /* ---------- 功能区静态工具页:库卡片 ?from=nav 进入时,返回按钮改为“返回上一页” ---------- */ + function initFuncBack() { + var el = document.querySelector('.js-fb-back'); + if (!el) return; + if (!/(?:^|[?&])from=nav(?:&|$)/.test(location.search)) return; + el.textContent = '← 返回上一页'; + el.title = '返回上一页(回到来源库首页)'; + el.addEventListener('click', function (ev) { + // 有浏览历史时返回来源页;否则保留默认 href(功能区首页) + if (window.history.length > 1) { + ev.preventDefault(); + window.history.back(); + } + }); + } + + /* ---------- 右下角 Toast 通知(后台整页刷新后的成功/失败提示使用,不影响页面布局) ---------- */ + function hpToast(kind, text, dur) { + if (text === '' || text === null || typeof text === 'undefined') return; + kind = (kind === 'ok' || kind === 'err') ? kind : 'ok'; + var wrap = document.querySelector('.hp-toasts'); + if (!wrap) { + wrap = document.createElement('div'); + wrap.className = 'hp-toasts'; + document.body.appendChild(wrap); + } + var box = document.createElement('div'); + box.className = 'hp-toast ' + kind; + var ic = document.createElement('span'); + ic.className = 'ht-ic'; + ic.textContent = kind === 'ok' ? '✓' : '!'; + var tx = document.createElement('div'); + tx.className = 'ht-tx'; + tx.textContent = String(text); + var x = document.createElement('button'); + x.type = 'button'; + x.className = 'ht-x'; + x.setAttribute('aria-label', '关闭提示'); + x.textContent = '×'; + function close() { + if (!box.parentNode) return; + box.classList.add('hide'); + setTimeout(function () { + if (box.parentNode) box.parentNode.removeChild(box); + if (wrap.parentNode && !wrap.children.length) wrap.parentNode.removeChild(wrap); + }, 240); + } + x.addEventListener('click', close); + box.appendChild(ic); + box.appendChild(tx); + box.appendChild(x); + wrap.appendChild(box); + var delay = (typeof dur === 'number' && dur > 0) ? dur : (kind === 'err' ? 6000 : 3600); + setTimeout(close, delay); + } + /* 由 PHP 注入的整页操作提示(window.hpToastMsg = {kind, text})自动弹出 */ + function hpShowPageToast() { + var m = window.hpToastMsg; + if (!m || typeof m !== 'object') return; + hpToast(m.kind, m.text); + } + + /* ---------- 静态工具页 favicon 跟随后台站点 logo(api/site.php 探测,异常静默回退默认) ---------- */ + function initSiteIcon() { + // 仅纯静态 .html 页需要(PHP 页面 favicon 已由 layout_head 服务端输出) + if (!/\.html$/i.test(location.pathname)) return; + var iconEl = document.querySelector('link[rel~="icon"]'); + if (!iconEl) return; + var abs = ''; + try { abs = new URL(iconEl.getAttribute('href') || '', location.href).href; } catch (e) { return; } + var marker = '/assets/img/logo.svg'; + var pos = abs.indexOf(marker); + if (pos < 0) return; // 非默认图标(已自定义)则无需处理 + var root = abs.slice(0, pos + 1); + var xhr = new XMLHttpRequest(); + try { + xhr.open('GET', root + 'api/site.php', true); + xhr.timeout = 4000; + xhr.onreadystatechange = function () { + if (xhr.readyState !== 4) return; + try { + var d = JSON.parse(xhr.responseText || '{}'); + if (d && typeof d.logo === 'string' && d.logo !== '') { + iconEl.href = root + d.logo; + } + } catch (e) { /* 接口异常:保持默认图标 */ } + }; + xhr.onerror = function () { /* file:// 或网络异常:保持默认图标 */ }; + xhr.send(); + } catch (e) { /* 同源限制等:保持默认图标 */ } + } + /* ---------- 自动初始化 ---------- */ document.addEventListener('DOMContentLoaded', function () { + initSiteIcon(); initTheme(); initClocks(); initQuoteRefresh(); @@ -390,11 +563,16 @@ initIconControls(); initCopyButtons(); bindConfirmForms(); + initVisitorInfo(); initToTop(); + initFuncBack(); + hpShowPageToast(); }); global.HomePage = { $: $, $$: $$, copyText: copyText, applyTheme: applyTheme, currentTheme: currentTheme, - confirm: siteConfirm, ask: askConfirm + confirm: siteConfirm, ask: askConfirm, toast: hpToast }; + // 底部脚本执行时 DOM 已解析完毕:在首帧绘制前后尽早恢复滚动位置,避免“先停在顶部再跳回”的大幅跳动 + hpRestoreScrollY(); })(window); diff --git a/assets/js/gmcrypto.js b/assets/js/gmcrypto.js index 53bb9c7..c534b0e 100644 --- a/assets/js/gmcrypto.js +++ b/assets/js/gmcrypto.js @@ -1,7 +1,7 @@ /* gmcrypto.js —— 国密 SM2 / SM3 / SM4 前端封装 * SM1 为国密硬件算法(不公开、依赖专用加密芯片),无法在浏览器纯软件实现, * 故本页提供可在网页中直接使用的 SM2(非对称)/ SM3(摘要)/ SM4(对称分组)。 - * 底层使用 sm-crypto@0.3.13,运行时按需双 CDN 懒加载(jsdelivr → unpkg)。 + * 底层使用 sm-crypto@0.3.13:优先本地 assets/js/sm-crypto/ 目录,缺失则回退 CDN(jsdelivr → unpkg)。 */ (function (global) { 'use strict'; @@ -11,22 +11,25 @@ sm2: { file: 'sm2.js', key: 'sm2', urls: [ - 'https://cdn.jsdelivr.net/npm/sm-crypto@' + VERSION + '/dist/sm2.js', - 'https://unpkg.com/sm-crypto@' + VERSION + '/dist/sm2.js' + '../assets/js/sm-crypto/sm2.js', // 本地内置(离线可用) + 'https://cdn.jsdelivr.net/npm/sm-crypto@' + VERSION + '/dist/sm2.js', // 回退 1 + 'https://unpkg.com/sm-crypto@' + VERSION + '/dist/sm2.js' // 回退 2 ] }, sm3: { file: 'sm3.js', key: 'sm3', urls: [ - 'https://cdn.jsdelivr.net/npm/sm-crypto@' + VERSION + '/dist/sm3.js', - 'https://unpkg.com/sm-crypto@' + VERSION + '/dist/sm3.js' + '../assets/js/sm-crypto/sm3.js', // 本地内置(离线可用) + 'https://cdn.jsdelivr.net/npm/sm-crypto@' + VERSION + '/dist/sm3.js', // 回退 1 + 'https://unpkg.com/sm-crypto@' + VERSION + '/dist/sm3.js' // 回退 2 ] }, sm4: { file: 'sm4.js', key: 'sm4', urls: [ - 'https://cdn.jsdelivr.net/npm/sm-crypto@' + VERSION + '/dist/sm4.js', - 'https://unpkg.com/sm-crypto@' + VERSION + '/dist/sm4.js' + '../assets/js/sm-crypto/sm4.js', // 本地内置(离线可用) + 'https://cdn.jsdelivr.net/npm/sm-crypto@' + VERSION + '/dist/sm4.js', // 回退 1 + 'https://unpkg.com/sm-crypto@' + VERSION + '/dist/sm4.js' // 回退 2 ] } }; @@ -137,7 +140,7 @@ function need(name) { return loadLib(name).then(function (lib) { - if (!lib) throw new Error('国密 ' + name.toUpperCase() + ' 组件加载失败,请检查网络后重试'); + if (!lib) throw new Error('国密 ' + name.toUpperCase() + ' 组件加载失败:请将 ' + name + '.js 放入 assets/js/sm-crypto/ 目录离线使用,或检查网络后重试'); return lib; }); } diff --git a/assets/js/ip.js b/assets/js/ip.js index 70556e8..56e59cb 100644 --- a/assets/js/ip.js +++ b/assets/js/ip.js @@ -6,7 +6,7 @@ * * 段按出现次数(段内全部 IP 原文出现次数之和)从高到低排列; * 「重点匹配 IP」:填写的 IP 命中后,其所在 /24 段优先置顶、对应 IP 红色标出。 - * CDN 段库由服务端通过 #ipCdnRaw 隐藏文本框注入,可在后台维护。 + * CDN 段库默认内置在页面 #ipCdnRaw 隐藏文本框中(func/ip.html),可直接用编辑器增补厂商。 */ (function () { 'use strict'; diff --git a/func/av.php b/func/av.html similarity index 77% rename from func/av.php rename to func/av.html index e0b515e..9d5b984 100644 --- a/func/av.php +++ b/func/av.html @@ -1,18 +1,25 @@ - + + + + + + +杀软识别 + + + + +
+ ← 返回功能区首页 + 杀软识别 + 首页 +
-layout_head('杀软识别'); -func_bar('杀软识别'); -?>
+

🔒 粘贴的 tasklist 输出仅在本机浏览器内解析匹配,不会上传服务器;如需离线独立使用,请将 data/avlist.js 一并拷贝。

使用说明

在目标机器执行 tasklist 复制全部输出,粘贴到下方输入框后点击“识别”。 系统会逐行解析进程名并与预置的安全软件进程表(data/avlist.js)进行大小写不敏感匹配,表格仅展示识别出的安全软件进程,常规进程不展示。

@@ -37,8 +44,8 @@ func_bar('杀软识别');
- - + + +编码 / 加解密 + + + + +
+ ← 返回功能区首页 + 编码 / 加解密 + 首页 +
-layout_head('编码 / 加解密'); -func_bar('编码 / 加解密'); -?>
+

🔒 本页全部运算均在浏览器本地完成,粘贴的文本不会上传到任何服务器,可放心处理大段内容。MD5 / SHA-224 在无内置加密库时需联网加载组件(仅下载代码,不含你的内容)。

+ + + + diff --git a/func/password.php b/func/password.html similarity index 78% rename from func/password.php rename to func/password.html index f20f2c7..4e17c8f 100644 --- a/func/password.php +++ b/func/password.html @@ -1,18 +1,25 @@ - + + + + + + +随机密码生成 + + + + +
+ ← 返回功能区首页 + 随机密码生成 + 首页 +
-layout_head('随机密码生成'); -func_bar('随机密码生成'); -?>
+

🔒 密码在本机浏览器内随机生成,不会上传服务器,生成后请妥善保存。

@@ -39,8 +46,8 @@ func_bar('随机密码生成');
- - + + +辅助维权 + + + + +
+ ← 返回功能区首页 + 辅助维权 + 首页 +
+
+

🔒 全部配置 / 命令均由本机浏览器本地生成,输入内容不会上传服务器。

辅助维权 · 教学辅助

本工具仅供内网教学 / 授权实验环境使用,请勿用于任何未经授权的真实目标。 三类输出全部在浏览器本地生成:① 反弹 Shell 构建(Linux / Windows 目标机,含普通、Python 动态与 Base64 编码版本); @@ -166,8 +168,8 @@ mimikatz.exe "kerberos::ptt 1-40a0000-xxx.kirbi" exit

- - + + diff --git a/func/qrcode.php b/func/qrcode.html similarity index 74% rename from func/qrcode.php rename to func/qrcode.html index 73c6c7c..0308d57 100644 --- a/func/qrcode.php +++ b/func/qrcode.html @@ -1,18 +1,25 @@ - + + + + + + +二维码生成器 + + + + +
+ ← 返回功能区首页 + 二维码生成器 + 首页 +
-layout_head('二维码生成器'); -func_bar('二维码生成器'); -?>
+

🔒 二维码内容仅在本机浏览器内编码,不会上传服务器。组件优先加载本地 assets/js/qrcode.min.js,未找到时自动回退公共 CDN(仅加载代码,不含你的内容)。

@@ -35,15 +42,16 @@ func_bar('二维码生成器');
- - + +