更新完善网页逻辑,工具迁移至html逻辑
This commit is contained in:
parent
c3bfce7afb
commit
646754c04d
@ -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' : '';
|
||||
|
||||
@ -33,7 +33,8 @@ function renumber_sorts(PDO $pdo): void
|
||||
|
||||
function msg_block(string $kind, string $text): string
|
||||
{
|
||||
return '<div class="' . ($kind === 'ok' ? 'ok-msg' : 'err-msg') . '">' . he($text) . '</div>';
|
||||
// 改为右下角 Toast:注入 hpToastMsg,由 common.js 在页面加载后自动弹出
|
||||
return '<script>window.hpToastMsg = ' . json_encode(['kind' => $kind === 'ok' ? 'ok' : 'err', 'text' => $text], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) . ';</script>';
|
||||
}
|
||||
|
||||
$msgKind = '';
|
||||
@ -236,7 +237,7 @@ admin_topbar('articles');
|
||||
<p class="tip">文章库(第五大板块)的文章维护:正文为标准 Markdown,图片统一上传到 data/articles/img 目录。</p>
|
||||
|
||||
<?php if ($savedFlag): ?>
|
||||
<div class="ok-msg">保存成功,文章库前台已同步更新。</div>
|
||||
<script>window.hpToastMsg = <?= json_encode(['kind' => 'ok', 'text' => '保存成功,文章库前台已同步更新。'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>
|
||||
<?php endif; ?>
|
||||
<?php if ($msgText !== ''): echo msg_block($msgKind, $msgText); endif; ?>
|
||||
|
||||
|
||||
@ -31,7 +31,8 @@ function remove_old_logo(): void
|
||||
|
||||
function alert_html(string $kind, string $msg): void
|
||||
{
|
||||
echo '<div class="' . ($kind === 'ok' ? 'ok-msg' : 'err-msg') . '">' . he($msg) . '</div>';
|
||||
// 改为右下角 Toast:注入 hpToastMsg,由 common.js 在页面加载后自动弹出
|
||||
echo '<script>window.hpToastMsg = ' . json_encode(['kind' => $kind === 'ok' ? 'ok' : 'err', 'text' => $msg], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) . ';</script>';
|
||||
}
|
||||
|
||||
$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');
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 首页顶部组件模式(Hero 下方横幅:天气 / 访客信息 / 空白) -->
|
||||
<div class="fieldset-card">
|
||||
<div class="fs-title">首页顶部组件(Hero 下方横幅区)</div>
|
||||
<?php $hm = (string)($all['hero_mode'] ?? 'weather'); ?>
|
||||
<form method="post">
|
||||
<?= csrf_field() ?><input type="hidden" name="act" value="hero_mode">
|
||||
<div class="field-row" style="align-items:center">
|
||||
<label class="seg-check"><input type="radio" name="hero_mode" value="weather"<?= $hm === 'weather' ? ' checked' : '' ?>> 🌤️ 天气模式(默认)</label>
|
||||
<label class="seg-check"><input type="radio" name="hero_mode" value="info"<?= $hm === 'info' ? ' checked' : '' ?>> 🖥️ 访客信息模式</label>
|
||||
<label class="seg-check"><input type="radio" name="hero_mode" value="blank"<?= $hm === 'blank' ? ' checked' : '' ?>> ⬜ 空白模式</label>
|
||||
</div>
|
||||
<div style="margin-top:12px"><button type="submit" class="btn btn-primary">保存顶部组件模式</button></div>
|
||||
</form>
|
||||
<p class="tip" style="margin:10px 0 0">
|
||||
天气模式(默认):横幅展示第三方天气条;访客信息模式:横幅显示当前访问者 IP、浏览器、操作系统、内核、屏幕分辨率与系统语言;空白模式:隐藏横幅,仅保留站点信息与时间信息,顶部高度自动缩减。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<div class="fieldset-card">
|
||||
<div class="fs-title">底部信息</div>
|
||||
|
||||
@ -135,10 +135,10 @@ admin_topbar('exttools');
|
||||
</p>
|
||||
|
||||
<?php if ($saved): ?>
|
||||
<div class="ok-msg">保存成功,库首页外部工具区已同步更新。</div>
|
||||
<script>window.hpToastMsg = <?= json_encode(['kind' => 'ok', 'text' => '保存成功,库首页外部工具区已同步更新。'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>
|
||||
<?php endif; ?>
|
||||
<?php if ($errorMsg !== ''): ?>
|
||||
<div class="err-msg"><?= he($errorMsg) ?></div>
|
||||
<script>window.hpToastMsg = <?= json_encode(['kind' => 'err', 'text' => $errorMsg], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 筛选工具条 -->
|
||||
|
||||
@ -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('后台登录');
|
||||
<button type="submit" class="btn btn-primary" style="width:100%">登录</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="tip" style="margin-top:12px">初始账号:admin / admin123(首次登录后请在“内容与顺序”中修改)</p>
|
||||
</div>
|
||||
</main>
|
||||
<?php layout_theme_fab(); ?>
|
||||
|
||||
@ -162,10 +162,10 @@ admin_topbar('nav');
|
||||
</div>
|
||||
|
||||
<?php if ($saved): ?>
|
||||
<div class="ok-msg">保存成功,前台页面已同步更新。</div>
|
||||
<script>window.hpToastMsg = <?= json_encode(['kind' => 'ok', 'text' => '保存成功,前台页面已同步更新。'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>
|
||||
<?php endif; ?>
|
||||
<?php if ($errorMsg !== ''): ?>
|
||||
<div class="err-msg"><?= he($errorMsg) ?></div>
|
||||
<script>window.hpToastMsg = <?= json_encode(['kind' => 'err', 'text' => $errorMsg], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="fieldset-card" style="margin-bottom:16px">
|
||||
|
||||
@ -148,10 +148,10 @@ admin_topbar('tools');
|
||||
</p>
|
||||
|
||||
<?php if ($saved): ?>
|
||||
<div class="ok-msg">保存成功,功能区首页已同步更新。</div>
|
||||
<script>window.hpToastMsg = <?= json_encode(['kind' => 'ok', 'text' => '保存成功,功能区首页已同步更新。'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>
|
||||
<?php endif; ?>
|
||||
<?php if ($errorMsg !== ''): ?>
|
||||
<div class="err-msg"><?= he($errorMsg) ?></div>
|
||||
<script>window.hpToastMsg = <?= json_encode(['kind' => 'err', 'text' => $errorMsg], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="fieldset-card">
|
||||
|
||||
502
admin/visits.php
Normal file
502
admin/visits.php
Normal file
@ -0,0 +1,502 @@
|
||||
<?php
|
||||
/**
|
||||
* admin/visits.php —— 访问信息
|
||||
* 访问日志查询(精简 / 完整两种模式 + IP / 关键词筛选 + 分页)与 IP 黑名单管理。
|
||||
* 黑名单命中规则后前台与后台(含登录页)请求均被直接拒绝(与前台同一风控提示页)。
|
||||
*/
|
||||
$P = '../';
|
||||
require_once __DIR__ . '/_guard.php';
|
||||
|
||||
$pdo = db();
|
||||
|
||||
function settings_set(string $key, string $value): void
|
||||
{
|
||||
$st = db()->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');
|
||||
?>
|
||||
<main class="page-main">
|
||||
<div class="wrap page-body">
|
||||
<h2 style="margin-bottom:6px">访问信息</h2>
|
||||
<p class="tip">记录访客对本站的动态请求(含后台),完整模式逐条展示访问明细,精简模式按 IP 汇总次数;黑名单 / 自动风控对所有访问统一生效,命中即返回 403 风控提示页,后台与登录页不例外。</p>
|
||||
|
||||
<?php if ($msgText !== ''): ?>
|
||||
<script>window.hpToastMsg = <?= json_encode(['kind' => $msgKind === 'ok' ? 'ok' : 'err', 'text' => $msgText], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 概要统计 -->
|
||||
<div class="fieldset-card">
|
||||
<div class="fs-title">访问概况</div>
|
||||
<div class="vis-stats">
|
||||
<div class="vis-stat"><b><?= $statTotal ?></b><span>累计访问</span></div>
|
||||
<div class="vis-stat"><b><?= $statIps ?></b><span>独立 IP</span></div>
|
||||
<div class="vis-stat"><b><?= $statToday ?></b><span>今日访问</span></div>
|
||||
<div class="vis-stat"><b><?= $statBan ?></b><span>黑名单规则</span></div>
|
||||
</div>
|
||||
<form method="post" data-confirm="确认清空全部访问日志?此操作不可恢复。" style="margin-top:14px">
|
||||
<?= csrf_field() ?><input type="hidden" name="act" value="log_clear">
|
||||
<button type="submit" class="btn btn-sm btn-danger">清空访问日志</button>
|
||||
<span class="tip" style="margin-left:8px">日志按行追加写入 <code>data/vistors.log</code>(制表符分隔:时间 / IP / 请求方式 / 响应码 / URL),完整保留不设上限;可自行归档或删除该文件。</span>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 筛选 + 日志列表 -->
|
||||
<div class="fieldset-card">
|
||||
<div class="fs-title">访问记录<?= $mode === 'compact' ? '(精简模式:按 IP 汇总)' : '(完整模式:逐条明细)' ?></div>
|
||||
<form method="get" class="v-filter">
|
||||
<select name="mode" title="展示模式" onchange="this.form.submit()">
|
||||
<option value="full"<?= $mode === 'full' ? ' selected' : '' ?>>完整模式(逐条明细)</option>
|
||||
<option value="compact"<?= $mode === 'compact' ? ' selected' : '' ?>>精简模式(仅时间 / IP / 次数)</option>
|
||||
</select>
|
||||
<input type="text" name="ip" value="<?= he($ipF) ?>" placeholder="按 IP 筛选,支持模糊(如 203.0)">
|
||||
<input type="text" name="q" class="w-s" value="<?= he($q) ?>" placeholder="关键词:地址 / 请求方式">
|
||||
<button type="submit" class="btn btn-primary">筛选</button>
|
||||
<a class="btn" href="visits.php">重置</a>
|
||||
</form>
|
||||
|
||||
<?php if ($logs): ?>
|
||||
<div class="tip" style="margin:10px 0 6px">
|
||||
共命中 <?= $total ?> <?= $mode === 'compact' ? '个 IP' : '条记录' ?>;
|
||||
当前第 <?= $page ?> / <?= $totalPages ?> 页,每页 <?= $perPage ?> 条。
|
||||
</div>
|
||||
<div class="v-scroll">
|
||||
<?php if ($mode === 'compact'): ?>
|
||||
<table class="v-tbl">
|
||||
<thead>
|
||||
<tr><th>最近访问时间</th><th>IP 地址</th><th>访问次数</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($logs as $r): ?>
|
||||
<tr>
|
||||
<td class="v-nowrap"><?= he((string)$r['last_at']) ?></td>
|
||||
<td class="v-nowrap"><?= he((string)$r['ip']) ?></td>
|
||||
<td><b><?= (int)$r['cnt'] ?></b> 次</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<table class="v-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>访问时间</th><th>IP 地址</th><th>请求方式</th><th>访问 URL</th><th>响应码</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($logs as $r): ?>
|
||||
<tr>
|
||||
<td class="v-nowrap"><?= he((string)$r['created_at']) ?></td>
|
||||
<td class="v-nowrap"><?= he((string)$r['ip']) ?></td>
|
||||
<td><span class="v-tag"><?= he((string)$r['method']) ?></span></td>
|
||||
<td><?= he((string)$r['url']) ?></td>
|
||||
<td class="v-nowrap"><?php
|
||||
$code = (int)$r['status'];
|
||||
$cls = ($code >= 200 && $code < 300) ? 'good' : (($code >= 300 && $code < 400) ? 'info' : (($code >= 400 && $code < 500) ? 'warn' : 'bad'));
|
||||
?><span class="v-tag <?= $cls ?>"><?= $code ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div class="pager">
|
||||
<span class="pg-info">共 <?= $total ?> 条</span>
|
||||
<?php if ($page > 1): ?><a class="btn btn-sm" href="<?= he(vis_url(['page' => $page - 1])) ?>">← 上一页</a><?php endif; ?>
|
||||
<?php
|
||||
$win = 2;
|
||||
$start = max(1, $page - $win);
|
||||
$end = min($totalPages, $page + $win);
|
||||
if ($start > 1) echo '<span class="tip">…</span>';
|
||||
for ($p = $start; $p <= $end; $p++) {
|
||||
if ($p === $page) {
|
||||
echo '<span class="btn btn-sm btn-primary">' . $p . '</span>';
|
||||
} else {
|
||||
echo '<a class="btn btn-sm" href="' . he(vis_url(['page' => $p])) . '">' . $p . '</a>';
|
||||
}
|
||||
}
|
||||
if ($end < $totalPages) echo '<span class="tip">…</span>';
|
||||
?>
|
||||
<?php if ($page < $totalPages): ?><a class="btn btn-sm" href="<?= he(vis_url(['page' => $page + 1])) ?>">下一页 →</a><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<div class="err-msg" style="margin-top:12px"><?= ($ipF !== '' || $q !== '') ? '该筛选条件下暂无访问记录,可点击「重置」查看全部。' : '还没有访问记录,访问站点前台页面后将在这里实时展示。' ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- 自动风控规则 -->
|
||||
<div class="fieldset-card">
|
||||
<div class="fs-title">自动风控规则<span class="tip" style="font-weight:400">(达到阈值自动加入黑名单;命中前台与后台均返回 403 风控提示页,提示文案可在下方自定义)</span></div>
|
||||
<form method="post">
|
||||
<?= csrf_field() ?><input type="hidden" name="act" value="risk_save">
|
||||
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:9px 0">
|
||||
<label class="seg-check" style="min-width:148px"><input type="checkbox" name="r_login_on" value="1"<?= $rcfg['login_fail']['on'] ? ' checked' : '' ?>> 后台登录失败封禁</label>
|
||||
<span class="tip">失败次数 ≥ <input type="number" name="r_login_n" min="1" max="1000000" value="<?= (int)$rcfg['login_fail']['n'] ?>" style="width:96px;padding:4px 8px;border:1px solid var(--line-2);border-radius:8px;background:var(--card);color:inherit"> 次 → 永久封禁(建议 5-20)</span>
|
||||
</div>
|
||||
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:9px 0">
|
||||
<label class="seg-check" style="min-width:148px"><input type="checkbox" name="r_404_on" value="1"<?= $rcfg['not_found']['on'] ? ' checked' : '' ?>> 前台 404 封禁</label>
|
||||
<span class="tip">累计次数 ≥ <input type="number" name="r_404_n" min="1" max="1000000" value="<?= (int)$rcfg['not_found']['n'] ?>" style="width:96px;padding:4px 8px;border:1px solid var(--line-2);border-radius:8px;background:var(--card);color:inherit"> 次 → 永久封禁(建议 50-300)</span>
|
||||
</div>
|
||||
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:9px 0">
|
||||
<label class="seg-check" style="min-width:148px"><input type="checkbox" name="r_rate_on" value="1"<?= $rcfg['rate']['on'] ? ' checked' : '' ?>> 请求频率封禁</label>
|
||||
<span class="tip">60 秒内 ≥ <input type="number" name="r_rate_n" min="1" max="1000000" value="<?= (int)$rcfg['rate']['n'] ?>" style="width:96px;padding:4px 8px;border:1px solid var(--line-2);border-radius:8px;background:var(--card);color:inherit"> 次 → 永久封禁(多人共用出口 IP 时建议关闭)</span>
|
||||
</div>
|
||||
<p class="tip" style="margin:8px 0 6px">计数方式:404 与登录失败只要发生即累计(规则关闭也照常计数,便于后台观察);规则启用且达到阈值时自动封禁,保存后即时生效。自动封禁在黑名单中标注“风控自动”,可随时移除解封(解封时同步清除该 IP 计数)。</p>
|
||||
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:0 0 8px">
|
||||
<span class="tip" style="min-width:148px">主提示(留空=默认)</span>
|
||||
<input type="text" name="r_tip_main" maxlength="100" value="<?= he((string)$rcfg['_tip']['raw_main']) ?>" placeholder="例:您触发了本站风控,请稍后再访问" title="黑名单命中时提示页的大号主文案" style="flex:1 1 300px;min-width:260px;padding:6px 10px;border:1px solid var(--line-2);border-radius:8px;background:var(--card);color:inherit">
|
||||
</div>
|
||||
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:0 0 12px">
|
||||
<span class="tip" style="min-width:148px">次行说明(留空=默认)</span>
|
||||
<input type="text" name="r_tip_sub" maxlength="200" value="<?= he((string)$rcfg['_tip']['raw_sub']) ?>" placeholder="例:如为正常访问,请稍后重试;若频繁误判,请联系站点管理员处理。" title="主提示下方的次要说明文案" style="flex:1 1 300px;min-width:260px;padding:6px 10px;border:1px solid var(--line-2);border-radius:8px;background:var(--card);color:inherit">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">保存风控规则</button>
|
||||
</form>
|
||||
<form method="post" data-confirm="确认清空全部风险计数?各 IP 将从 0 重新累计,不影响已封禁的黑名单。" style="margin-top:14px">
|
||||
<?= csrf_field() ?><input type="hidden" name="act" value="risk_clear">
|
||||
<button type="submit" class="btn btn-sm">清空全部风险计数</button>
|
||||
<?php if ($curRisk): ?>
|
||||
<span class="tip" style="margin-left:8px">当前访问 IP(<?= he($curIp) ?>)计数:登录失败 <?= (int)$curRisk['login_fail'] ?> 次 · 404 <?= (int)$curRisk['not_found'] ?> 次<?php if ($rcfg['rate']['on']): ?> · 本分钟请求 <?= (int)$curRisk['rate_count'] ?> 次<?php endif; ?></span>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- IP 黑名单 -->
|
||||
<div class="fieldset-card">
|
||||
<div class="fs-title">IP 黑名单(全站拦截前台与后台,命中统一返回 403 风控提示页)</div>
|
||||
<form method="post" class="v-filter">
|
||||
<?= csrf_field() ?><input type="hidden" name="act" value="ban_add">
|
||||
<input type="text" name="rule" required placeholder="如:203.0.113.7 / 203.0.113.0/24 / 203.0.113.*" style="flex:1 1 260px;max-width:420px">
|
||||
<input type="text" name="note" placeholder="备注(可选,如:恶意扫描器)" style="flex:1 1 220px;max-width:340px">
|
||||
<button type="submit" class="btn btn-primary">加入黑名单</button>
|
||||
</form>
|
||||
<p class="tip" style="margin:10px 0 2px">
|
||||
支持三种写法:单个 IP、CIDR 网段(如 <code>203.0.113.0/24</code>)、星号通配段(如 <code>203.0.113.*</code> 或 <code>192.168.*.*</code>);
|
||||
目前仅按 IPv4 匹配。黑名单即时生效且不受「站长提示」开关影响,命中 IP 的<strong>前台与后台(含登录页)访问一律被拒绝</strong>。如需解除,请从未被封禁的网络进入后台移除该规则,或直接在 data/homepage.db 的 ip_blacklist 表中删除记录。
|
||||
</p>
|
||||
<div style="margin:6px 0 10px">
|
||||
<?php if ($curHit): ?>
|
||||
<span class="vis-warn-tip">当前访问 IP:<?= he($curIp) ?> —— 命中规则「<?= he($curHit['rule']) ?>」(<?= ($curHit['note'] !== '') ? he((string)$curHit['note']) : '无备注' ?>),全站(前台与后台)访问将被拦截</span>
|
||||
<?php else: ?>
|
||||
<span class="vis-ok-tip">当前访问 IP:<?= he($curIp) ?> —— 未命中任何黑名单规则</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($banRows): ?>
|
||||
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:8px 0 4px">
|
||||
<form method="post" id="blBatchForm" data-confirm="确认移除选中的黑名单规则?对应单 IP 风控计数将同步清除。">
|
||||
<?= csrf_field() ?><input type="hidden" name="act" value="ban_batch">
|
||||
<label class="tip" style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;margin:0 4px 0 0"><input type="checkbox" id="blCheckAll"> 全选</label>
|
||||
<button type="submit" class="btn btn-sm btn-danger" id="blBatchBtn" disabled>移除选中(0)</button>
|
||||
</form>
|
||||
<?php if ($banAutoCount > 0): ?>
|
||||
<form method="post" data-confirm="确认一键移除全部风控自动封禁(共 <?= $banAutoCount ?> 条)?相关 IP 风控计数将同步清除。">
|
||||
<?= csrf_field() ?><input type="hidden" name="act" value="ban_clear_auto">
|
||||
<button type="submit" class="btn btn-sm">一键移除风控自动(<?= $banAutoCount ?>)</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<form method="post" data-confirm="确认清空全部黑名单?全部风险计数将一并清零。">
|
||||
<?= csrf_field() ?><input type="hidden" name="act" value="ban_clear_all">
|
||||
<button type="submit" class="btn btn-sm btn-danger">清空全部</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php foreach ($banRows as $b): ?>
|
||||
<div class="bl-row">
|
||||
<label class="tip" style="display:inline-flex;align-items:center;margin:0" title="勾选后可在上方批量移除"><input type="checkbox" form="blBatchForm" class="js-bl-check" name="bid[]" value="<?= (int)$b['id'] ?>"></label>
|
||||
<code class="bl-rule"><?= he((string)$b['rule']) ?></code>
|
||||
<?php if (((string)($b['source'] ?? 'manual')) === 'auto'): ?>
|
||||
<span class="v-tag warn" style="min-width:0">风控自动</span>
|
||||
<?php else: ?>
|
||||
<span class="v-tag info" style="min-width:0">手动</span>
|
||||
<?php endif; ?>
|
||||
<span class="bl-note"><?= $b['note'] !== '' ? he((string)$b['note']) : '<span class="tip">(无备注)</span>' ?></span>
|
||||
<span class="bl-time"><?= he((string)$b['created_at']) ?></span>
|
||||
<form method="post" data-confirm="确认移除黑名单规则「<?= he((string)$b['rule']) ?>」?">
|
||||
<?= csrf_field() ?><input type="hidden" name="act" value="ban_del"><input type="hidden" name="id" value="<?= (int)$b['id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger">移除</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<p class="tip" style="margin:6px 0 0">尚未配置黑名单规则。启用上方「自动风控」后,异常 IP 会自动记录到这里;也可在下方手动添加规则。</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php layout_theme_fab(); ?>
|
||||
<script src="../assets/js/common.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var all = document.getElementById('blCheckAll');
|
||||
var btn = document.getElementById('blBatchBtn');
|
||||
var boxes = Array.prototype.slice.call(document.querySelectorAll('.js-bl-check'));
|
||||
if (!all || !btn || !boxes.length) return;
|
||||
function refresh() {
|
||||
var n = boxes.filter(function (c) { return c.checked; }).length;
|
||||
btn.disabled = n === 0;
|
||||
btn.textContent = '移除选中(' + n + ')';
|
||||
all.checked = n > 0 && n === boxes.length;
|
||||
}
|
||||
all.addEventListener('change', function () {
|
||||
boxes.forEach(function (c) { c.checked = all.checked; });
|
||||
refresh();
|
||||
});
|
||||
boxes.forEach(function (c) { c.addEventListener('change', refresh); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
12
api/site.php
Normal file
12
api/site.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* api/site.php —— 站点公开元信息 JSON 接口
|
||||
* 供纯静态 HTML 页(如 func/*.html)读取当前站点 logo,使工具页 favicon / 标识与后台设置保持同步。
|
||||
* 返回:{ "logo": "data/logo/site_logo_xxx.svg" 或 ""(空 = 使用默认 assets/img/logo.svg) }
|
||||
*/
|
||||
require_once dirname(__DIR__) . '/includes/db.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
|
||||
echo json_encode(['logo' => setting_get('site_logo', '')], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
@ -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; } }
|
||||
|
||||
BIN
assets/img/favicon.ico
Normal file
BIN
assets/img/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
*
|
||||
* 段按出现次数(段内全部 IP 原文出现次数之和)从高到低排列;
|
||||
* 「重点匹配 IP」:填写的 IP 命中后,其所在 /24 段优先置顶、对应 IP 红色标出。
|
||||
* CDN 段库由服务端通过 #ipCdnRaw 隐藏文本框注入,可在后台维护。
|
||||
* CDN 段库默认内置在页面 #ipCdnRaw 隐藏文本框中(func/ip.html),可直接用编辑器增补厂商。
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
@ -1,18 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* func/av.php —— 杀软识别
|
||||
* 粘贴 tasklist 输出 → 匹配 data/avlist.js 进程表 → 表格输出 + 统计
|
||||
*/
|
||||
$P = '../';
|
||||
require_once dirname(__DIR__) . '/includes/layout.php';
|
||||
require_once __DIR__ . '/_bar.php';
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="renderer" content="webkit">
|
||||
<script>(function(){var t;try{t=localStorage.getItem("hp-theme")}catch(e){}if(!t){t=(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)?"dark":"light"}document.documentElement.setAttribute("data-theme",t);})();</script>
|
||||
<title>杀软识别</title>
|
||||
<link rel="icon" href="../assets/img/logo.svg">
|
||||
<link rel="stylesheet" href="../assets/css/common.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="func-bar"><div class="wrap func-bar-inner">
|
||||
<a class="btn btn-sm js-fb-back" href="index.php" title="返回功能区首页">← 返回功能区首页</a>
|
||||
<span class="func-bar-title">杀软识别</span>
|
||||
<a class="btn btn-sm" href="../index.php">首页</a>
|
||||
</div></div>
|
||||
|
||||
layout_head('杀软识别');
|
||||
func_bar('杀软识别');
|
||||
?>
|
||||
<main class="page-main">
|
||||
<div class="wrap page-body">
|
||||
<div class="panel-card">
|
||||
<p class="tip" style="margin-top:0">🔒 粘贴的 tasklist 输出仅在本机浏览器内解析匹配,不会上传服务器;如需离线独立使用,请将 <code>data/avlist.js</code> 一并拷贝。</p>
|
||||
<h2>使用说明</h2>
|
||||
<p class="tip">在目标机器执行 <code>tasklist</code> 复制全部输出,粘贴到下方输入框后点击“识别”。
|
||||
系统会逐行解析进程名并与预置的安全软件进程表(data/avlist.js)进行大小写不敏感匹配,<strong>表格仅展示识别出的安全软件进程</strong>,常规进程不展示。</p>
|
||||
@ -37,8 +44,8 @@ func_bar('杀软识别');
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php layout_theme_fab(); ?>
|
||||
<?php layout_footer(); ?>
|
||||
<button type="button" class="theme-fab js-theme-btn" title="切换昼夜模式" aria-label="切换昼夜模式"><span class="js-theme-icon"></span></button>
|
||||
<button type="button" class="theme-fab to-top js-to-top" title="返回顶部" aria-label="返回顶部">↑</button>
|
||||
<script src="../data/avlist.js"></script>
|
||||
<script src="../assets/js/common.js"></script>
|
||||
<script>
|
||||
@ -52,7 +59,6 @@ func_bar('杀软识别');
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
// 跳过表头与分隔行
|
||||
if (/映像名称|Image Name|进程名|PID|===/i.test(line) && !/\d$/.test(line.trim())) continue;
|
||||
var name = line.split(/[\s,]+/)[0];
|
||||
if (!name) continue;
|
||||
@ -99,7 +105,6 @@ func_bar('杀软识别');
|
||||
}
|
||||
var body = $('avBody');
|
||||
body.innerHTML = '';
|
||||
// 仅保留识别(命中安全软件进程表)的行
|
||||
var matched = [];
|
||||
rows.forEach(function (r) {
|
||||
var info = lookup(r.name);
|
||||
@ -132,7 +137,6 @@ func_bar('杀软识别');
|
||||
$('avMsg').innerHTML = '';
|
||||
$('taskInput').focus();
|
||||
});
|
||||
// 输入内容后可用 Ctrl/Cmd + Enter 快速识别
|
||||
$('taskInput').addEventListener('keydown', function (ev) {
|
||||
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) run();
|
||||
});
|
||||
@ -1,19 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* func/codec.php —— 编码 / 加解密工具
|
||||
* Base64 / Base32 / URL / Unicode(支持中文与 emoji)双向;MD5 / SHA-1/224/256/384/512 哈希。
|
||||
* 输入输出文本区均可自由编辑、可竖向拉伸。
|
||||
*/
|
||||
$P = '../';
|
||||
require_once dirname(__DIR__) . '/includes/layout.php';
|
||||
require_once __DIR__ . '/_bar.php';
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="renderer" content="webkit">
|
||||
<script>(function(){var t;try{t=localStorage.getItem("hp-theme")}catch(e){}if(!t){t=(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)?"dark":"light"}document.documentElement.setAttribute("data-theme",t);})();</script>
|
||||
<title>编码 / 加解密</title>
|
||||
<link rel="icon" href="../assets/img/logo.svg">
|
||||
<link rel="stylesheet" href="../assets/css/common.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="func-bar"><div class="wrap func-bar-inner">
|
||||
<a class="btn btn-sm js-fb-back" href="index.php" title="返回功能区首页">← 返回功能区首页</a>
|
||||
<span class="func-bar-title">编码 / 加解密</span>
|
||||
<a class="btn btn-sm" href="../index.php">首页</a>
|
||||
</div></div>
|
||||
|
||||
layout_head('编码 / 加解密');
|
||||
func_bar('编码 / 加解密');
|
||||
?>
|
||||
<main class="page-main">
|
||||
<div class="wrap page-body">
|
||||
<div class="panel-card">
|
||||
<p class="tip" style="margin-top:0">🔒 本页全部运算均在浏览器本地完成,粘贴的文本不会上传到任何服务器,可放心处理大段内容。MD5 / SHA-224 在无内置加密库时需联网加载组件(仅下载代码,不含你的内容)。</p>
|
||||
<div class="field-row" style="align-items:center">
|
||||
<label class="fl" style="margin:0" for="selAlgo">算法</label>
|
||||
<select id="selAlgo">
|
||||
@ -58,8 +64,8 @@ func_bar('编码 / 加解密');
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php layout_theme_fab(); ?>
|
||||
<?php layout_footer(); ?>
|
||||
<button type="button" class="theme-fab js-theme-btn" title="切换昼夜模式" aria-label="切换昼夜模式"><span class="js-theme-icon"></span></button>
|
||||
<button type="button" class="theme-fab to-top js-to-top" title="返回顶部" aria-label="返回顶部">↑</button>
|
||||
<script src="../assets/js/common.js"></script>
|
||||
<script src="../assets/js/codec.js"></script>
|
||||
<script>
|
||||
@ -1,21 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* func/gmcodec.php —— 国密加解密
|
||||
* SM2 非对称加解密 / SM3 摘要 / SM4 对称加解密。
|
||||
* 说明:SM1 为国密硬件算法(不公开、依赖专用芯片),无法在浏览器纯软件实现,故未提供。
|
||||
*/
|
||||
$P = '../';
|
||||
require_once dirname(__DIR__) . '/includes/layout.php';
|
||||
require_once __DIR__ . '/_bar.php';
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="renderer" content="webkit">
|
||||
<script>(function(){var t;try{t=localStorage.getItem("hp-theme")}catch(e){}if(!t){t=(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)?"dark":"light"}document.documentElement.setAttribute("data-theme",t);})();</script>
|
||||
<title>国密加解密</title>
|
||||
<link rel="icon" href="../assets/img/logo.svg">
|
||||
<link rel="stylesheet" href="../assets/css/common.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="func-bar"><div class="wrap func-bar-inner">
|
||||
<a class="btn btn-sm js-fb-back" href="index.php" title="返回功能区首页">← 返回功能区首页</a>
|
||||
<span class="func-bar-title">国密加解密</span>
|
||||
<a class="btn btn-sm" href="../index.php">首页</a>
|
||||
</div></div>
|
||||
|
||||
layout_head('国密加解密');
|
||||
func_bar('国密加解密');
|
||||
?>
|
||||
<main class="page-main">
|
||||
<div class="wrap page-body">
|
||||
<div class="panel-card">
|
||||
<p class="tip" style="margin-top:0">支持 SM2 / SM3 / SM4 三种国密算法;SM1 为不公开的硬件算法(需国密芯片),无法纯软件实现。
|
||||
SM2/SM4 运算依赖在线加载 <code>sm-crypto</code> 组件(双 CDN),加载失败会明确提示。</p>
|
||||
<p class="tip" style="margin-top:0">🔒 SM2 / SM3 / SM4 运算全部在本机浏览器完成,明文 / 密钥 / 密文不会上传服务器。算法组件优先加载本地 <code>assets/js/sm-crypto/</code> 下的 <code>sm2.js / sm3.js / sm4.js</code>,未找到时自动回退公共 CDN(仅加载代码,不含你的内容)。</p>
|
||||
<p class="tip" style="margin-top:0">支持 SM2 / SM3 / SM4 三种国密算法;SM1 为不公开的硬件算法(需国密芯片),无法纯软件实现。</p>
|
||||
|
||||
<div class="field-row" style="align-items:center;margin-top:12px">
|
||||
<label class="fl" style="margin:0" for="gmAlgo">算法</label>
|
||||
@ -69,7 +74,7 @@ func_bar('国密加解密');
|
||||
</div>
|
||||
|
||||
<!-- SM3 提示区 -->
|
||||
<div id="gmSm3Tip" class="tip" hidden style="margin-top:8px">SM3 输出固定 64 位十六进制摘要(HMAC 场景暂不提供,如需请使用后台维护的其它方案)。</div>
|
||||
<div id="gmSm3Tip" class="tip" hidden style="margin-top:8px">SM3 输出固定 64 位十六进制摘要(HMAC 场景暂不提供)。</div>
|
||||
|
||||
<div id="gmMsg"></div>
|
||||
</div>
|
||||
@ -93,8 +98,8 @@ func_bar('国密加解密');
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php layout_theme_fab(); ?>
|
||||
<?php layout_footer(); ?>
|
||||
<button type="button" class="theme-fab js-theme-btn" title="切换昼夜模式" aria-label="切换昼夜模式"><span class="js-theme-icon"></span></button>
|
||||
<button type="button" class="theme-fab to-top js-to-top" title="返回顶部" aria-label="返回顶部">↑</button>
|
||||
<script src="../assets/js/common.js"></script>
|
||||
<script src="../assets/js/gmcrypto.js"></script>
|
||||
<script>
|
||||
@ -1,27 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* func/ip.php —— IP 地址处理
|
||||
* 提取输入文本 / 上传文本文件中的全部 IPv4,按 /24 段聚合输出纯文本结果:
|
||||
* IP段 发现数量(段内去重IP数) IP段性质 出现次数
|
||||
* 1.2.3.0/24 8 公网 56
|
||||
* 1.2.3.4 出现次数
|
||||
*
|
||||
* 出现次数:具体 IP 为其在原文中的出现总次数;段行出现次数为段内全部 IP 次数之和。
|
||||
* 段默认按出现次数从高到低排列,命中「重点匹配 IP」的段优先置顶。
|
||||
* 段性质区分:内网(含保留)/ CDN / 公网;IPv6 暂不参与。
|
||||
* 支持上传常见文本格式文件(txt / json / log / md / csv / conf / ini)自动载入,
|
||||
* 支持将结果导出为 UTF-8 TXT 文件。
|
||||
*/
|
||||
$P = '../';
|
||||
require_once dirname(__DIR__) . '/includes/layout.php';
|
||||
require_once __DIR__ . '/_bar.php';
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="renderer" content="webkit">
|
||||
<script>(function(){var t;try{t=localStorage.getItem("hp-theme")}catch(e){}if(!t){t=(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)?"dark":"light"}document.documentElement.setAttribute("data-theme",t);})();</script>
|
||||
<title>IP 地址处理</title>
|
||||
<link rel="icon" href="../assets/img/logo.svg">
|
||||
<link rel="stylesheet" href="../assets/css/common.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="func-bar"><div class="wrap func-bar-inner">
|
||||
<a class="btn btn-sm js-fb-back" href="index.php" title="返回功能区首页">← 返回功能区首页</a>
|
||||
<span class="func-bar-title">IP 地址处理</span>
|
||||
<a class="btn btn-sm" href="../index.php">首页</a>
|
||||
</div></div>
|
||||
|
||||
layout_head('IP 地址处理');
|
||||
func_bar('IP 地址处理');
|
||||
?>
|
||||
<main class="page-main">
|
||||
<div class="wrap page-body">
|
||||
<div class="panel-card">
|
||||
<p class="tip" style="margin-top:0">🔒 粘贴的日志 / 上传的文本文件<b>全部在本机浏览器内解析</b>,不会上传到服务器,可放心处理大文件(≤ 20MB 由浏览器本地读取)。</p>
|
||||
<h2>使用说明</h2>
|
||||
<p class="tip">粘贴任意日志 / 文本,或直接<b>上传文本文件</b>(txt / json / log / md / csv / conf / ini,≤ 20MB),
|
||||
系统提取其中全部 <strong>IPv4</strong>(IPv6 暂不参与),按 <strong>/24 段</strong>聚合:
|
||||
@ -56,15 +54,31 @@ func_bar('IP 地址处理');
|
||||
</div>
|
||||
<div id="ipPreview" class="ip-pre"></div>
|
||||
<p class="tip" id="ipStat" style="margin:8px 0"></p>
|
||||
<p class="tip">段行为 <code>IP段 / 发现数量 / 性质 / 出现次数</code>(出现次数为段内全部 IP 原文出现次数之和);具体 IP 行为 <code>IP / 出现次数</code>。段按出现次数降序排列,含重点 IP 的段置顶;复制或导出的纯文本不含颜色。若 /24 段内混有不同性质,按数量多者标注“xx为主(混合)”。</p>
|
||||
<p class="tip">段行为 <code>IP段 / 发现数量 / 性质 / 出现次数</code>(出现次数为段内全部 IP 原文出现次数之和);具体 IP 行为 <code>IP / 出现次数</code>。段按出现次数降序排列,含重点 IP 的段置顶;复制或导出的纯文本不含颜色。若 /24 段内混有不同性质,按数量多者标注“xx为主(混合)”。CDN 段库为页面内置默认,可用文本编辑器在本 HTML 文件的 <code>ipCdnRaw</code> 中增补厂商。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php layout_theme_fab(); ?>
|
||||
<?php layout_footer(); ?>
|
||||
<textarea id="ipCdnRaw" hidden><?= he(cdn_ranges_text()) ?></textarea>
|
||||
<textarea id="ipCdnRaw" hidden># 内置默认:Cloudflare 官方 IPv4 段(可在本 HTML 的 ipCdnRaw 内补充其它厂商,每行:CIDR + 空格 + 厂商名;# 开头为注释)
|
||||
103.21.244.0/22 Cloudflare
|
||||
103.22.200.0/22 Cloudflare
|
||||
103.31.4.0/22 Cloudflare
|
||||
104.16.0.0/13 Cloudflare
|
||||
104.24.0.0/14 Cloudflare
|
||||
108.162.192.0/18 Cloudflare
|
||||
131.0.72.0/22 Cloudflare
|
||||
141.101.64.0/18 Cloudflare
|
||||
162.158.0.0/15 Cloudflare
|
||||
172.64.0.0/13 Cloudflare
|
||||
173.245.48.0/20 Cloudflare
|
||||
188.114.96.0/20 Cloudflare
|
||||
190.93.240.0/20 Cloudflare
|
||||
197.234.240.0/22 Cloudflare
|
||||
198.41.128.0/17 Cloudflare</textarea>
|
||||
|
||||
<button type="button" class="theme-fab js-theme-btn" title="切换昼夜模式" aria-label="切换昼夜模式"><span class="js-theme-icon"></span></button>
|
||||
<button type="button" class="theme-fab to-top js-to-top" title="返回顶部" aria-label="返回顶部">↑</button>
|
||||
<script src="../assets/js/common.js"></script>
|
||||
<script src="../assets/js/ip.js"></script>
|
||||
</body>
|
||||
@ -1,18 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* func/password.php —— 随机密码生成
|
||||
* 字符集多选 + 自定义字符集 + 长度 + 一次生成多组 + 复制
|
||||
*/
|
||||
$P = '../';
|
||||
require_once dirname(__DIR__) . '/includes/layout.php';
|
||||
require_once __DIR__ . '/_bar.php';
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="renderer" content="webkit">
|
||||
<script>(function(){var t;try{t=localStorage.getItem("hp-theme")}catch(e){}if(!t){t=(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)?"dark":"light"}document.documentElement.setAttribute("data-theme",t);})();</script>
|
||||
<title>随机密码生成</title>
|
||||
<link rel="icon" href="../assets/img/logo.svg">
|
||||
<link rel="stylesheet" href="../assets/css/common.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="func-bar"><div class="wrap func-bar-inner">
|
||||
<a class="btn btn-sm js-fb-back" href="index.php" title="返回功能区首页">← 返回功能区首页</a>
|
||||
<span class="func-bar-title">随机密码生成</span>
|
||||
<a class="btn btn-sm" href="../index.php">首页</a>
|
||||
</div></div>
|
||||
|
||||
layout_head('随机密码生成');
|
||||
func_bar('随机密码生成');
|
||||
?>
|
||||
<main class="page-main">
|
||||
<div class="wrap page-body">
|
||||
<div class="panel-card">
|
||||
<p class="tip" style="margin-top:0">🔒 密码在本机浏览器内随机生成,不会上传服务器,生成后请妥善保存。</p>
|
||||
<div class="field-row">
|
||||
<label class="seg-check"><input type="checkbox" id="ckUpper" checked> 大写 A-Z</label>
|
||||
<label class="seg-check"><input type="checkbox" id="ckLower" checked> 小写 a-z</label>
|
||||
@ -39,8 +46,8 @@ func_bar('随机密码生成');
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php layout_theme_fab(); ?>
|
||||
<?php layout_footer(); ?>
|
||||
<button type="button" class="theme-fab js-theme-btn" title="切换昼夜模式" aria-label="切换昼夜模式"><span class="js-theme-icon"></span></button>
|
||||
<button type="button" class="theme-fab to-top js-to-top" title="返回顶部" aria-label="返回顶部">↑</button>
|
||||
<script src="../assets/js/common.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
@ -78,7 +85,6 @@ func_bar('随机密码生成');
|
||||
|
||||
function genOne(chars, len) {
|
||||
var out = '';
|
||||
// 确保自定义字符集大小时每个字符都可能出现
|
||||
for (var i = 0; i < len; i++) {
|
||||
out += chars.charAt(randInt(chars.length));
|
||||
}
|
||||
@ -108,7 +114,7 @@ func_bar('随机密码生成');
|
||||
var line = document.createElement('div');
|
||||
line.className = 'pwd-line';
|
||||
var code = document.createElement('code');
|
||||
code.textContent = pwd; // 用 textContent 写入,避免 HTML 注入
|
||||
code.textContent = pwd;
|
||||
var copyBtn = document.createElement('button');
|
||||
copyBtn.type = 'button';
|
||||
copyBtn.className = 'btn btn-sm js-copy';
|
||||
@ -1,20 +1,13 @@
|
||||
<?php
|
||||
/**
|
||||
* func/persist.php —— 辅助维权(教学辅助)
|
||||
* 仅面向内网教学 / 授权实验环境。三个模块:
|
||||
* 1) 反弹 Shell 构建:输入监听 IP / 端口,按 Linux / Windows 目标机分别生成三类语句
|
||||
* (普通反弹 / Python 动态 Shell / Base64 编码版本);
|
||||
* 2) Windows 辅助:SAM、SYSTEM 注册表配置单元提取、mimikatz 常用命令与票据提取参考;
|
||||
* 3) Linux 自启服务:输入执行文件绝对路径,生成 systemd(高版本)或 init.d(低版本)
|
||||
* 可编辑配置,默认不产生 / 不落盘日志且崩溃自动重启。
|
||||
*/
|
||||
$P = '../';
|
||||
require_once dirname(__DIR__) . '/includes/layout.php';
|
||||
require_once __DIR__ . '/_bar.php';
|
||||
|
||||
layout_head('辅助维权');
|
||||
func_bar('辅助维权');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="renderer" content="webkit">
|
||||
<script>(function(){var t;try{t=localStorage.getItem("hp-theme")}catch(e){}if(!t){t=(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)?"dark":"light"}document.documentElement.setAttribute("data-theme",t);})();</script>
|
||||
<title>辅助维权</title>
|
||||
<link rel="icon" href="../assets/img/logo.svg">
|
||||
<link rel="stylesheet" href="../assets/css/common.css">
|
||||
<style>
|
||||
.p-sec { margin-top: 16px; }
|
||||
.p-sec-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
|
||||
@ -24,9 +17,18 @@ func_bar('辅助维权');
|
||||
.b64-note { font-size: 12px; color: var(--text-3); margin: 6px 0 4px; }
|
||||
.p-sec code, .tip code { font-family: Consolas, Menlo, monospace; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="func-bar"><div class="wrap func-bar-inner">
|
||||
<a class="btn btn-sm js-fb-back" href="index.php" title="返回功能区首页">← 返回功能区首页</a>
|
||||
<span class="func-bar-title">辅助维权</span>
|
||||
<a class="btn btn-sm" href="../index.php">首页</a>
|
||||
</div></div>
|
||||
|
||||
<main class="page-main">
|
||||
<div class="wrap page-body">
|
||||
<div class="panel-card">
|
||||
<p class="tip" style="margin-top:0">🔒 全部配置 / 命令均由本机浏览器本地生成,输入内容不会上传服务器。</p>
|
||||
<h2>辅助维权 · 教学辅助</h2>
|
||||
<p class="tip">本工具<b>仅供内网教学 / 授权实验环境使用</b>,请勿用于任何未经授权的真实目标。
|
||||
三类输出全部在浏览器本地生成:① <b>反弹 Shell 构建</b>(Linux / Windows 目标机,含普通、Python 动态与 Base64 编码版本);
|
||||
@ -166,8 +168,8 @@ mimikatz.exe "kerberos::ptt 1-40a0000-xxx.kirbi" exit
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php layout_theme_fab(); ?>
|
||||
<?php layout_footer(); ?>
|
||||
<button type="button" class="theme-fab js-theme-btn" title="切换昼夜模式" aria-label="切换昼夜模式"><span class="js-theme-icon"></span></button>
|
||||
<button type="button" class="theme-fab to-top js-to-top" title="返回顶部" aria-label="返回顶部">↑</button>
|
||||
<script src="../assets/js/common.js"></script>
|
||||
<script src="../assets/js/persist.js"></script>
|
||||
</body>
|
||||
@ -1,18 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* func/qrcode.php —— 二维码生成器
|
||||
* 使用 qrcodejs(CDN 双源加载);支持尺寸调节、放大预览与 PNG 下载。
|
||||
*/
|
||||
$P = '../';
|
||||
require_once dirname(__DIR__) . '/includes/layout.php';
|
||||
require_once __DIR__ . '/_bar.php';
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="renderer" content="webkit">
|
||||
<script>(function(){var t;try{t=localStorage.getItem("hp-theme")}catch(e){}if(!t){t=(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)?"dark":"light"}document.documentElement.setAttribute("data-theme",t);})();</script>
|
||||
<title>二维码生成器</title>
|
||||
<link rel="icon" href="../assets/img/logo.svg">
|
||||
<link rel="stylesheet" href="../assets/css/common.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="func-bar"><div class="wrap func-bar-inner">
|
||||
<a class="btn btn-sm js-fb-back" href="index.php" title="返回功能区首页">← 返回功能区首页</a>
|
||||
<span class="func-bar-title">二维码生成器</span>
|
||||
<a class="btn btn-sm" href="../index.php">首页</a>
|
||||
</div></div>
|
||||
|
||||
layout_head('二维码生成器');
|
||||
func_bar('二维码生成器');
|
||||
?>
|
||||
<main class="page-main">
|
||||
<div class="wrap page-body">
|
||||
<div class="panel-card">
|
||||
<p class="tip" style="margin-top:0">🔒 二维码内容仅在本机浏览器内编码,不会上传服务器。组件优先加载本地 <code>assets/js/qrcode.min.js</code>,未找到时自动回退公共 CDN(仅加载代码,不含你的内容)。</p>
|
||||
<div class="field-row">
|
||||
<input type="text" id="qrText" placeholder="输入需要生成二维码的内容,如网址 / 文本…" style="flex:1;min-width:260px">
|
||||
<button type="button" class="btn btn-primary" id="qrGen">生成二维码</button>
|
||||
@ -35,15 +42,16 @@ func_bar('二维码生成器');
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php layout_theme_fab(); ?>
|
||||
<?php layout_footer(); ?>
|
||||
<button type="button" class="theme-fab js-theme-btn" title="切换昼夜模式" aria-label="切换昼夜模式"><span class="js-theme-icon"></span></button>
|
||||
<button type="button" class="theme-fab to-top js-to-top" title="返回顶部" aria-label="返回顶部">↑</button>
|
||||
<script src="../assets/js/common.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
var CDNS = [
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js'
|
||||
'../assets/js/qrcode.min.js', // 本地内置(离线可用)
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js', // 回退 1
|
||||
'https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js' // 回退 2
|
||||
];
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
@ -52,7 +60,7 @@ func_bar('二维码生成器');
|
||||
var idx = 0;
|
||||
(function next() {
|
||||
if (window.QRCode) { cb(null); return; }
|
||||
if (idx >= CDNS.length) { cb(new Error('二维码组件加载失败,请检查网络后重试')); return; }
|
||||
if (idx >= CDNS.length) { cb(new Error('二维码组件加载失败,请检查网络后重试,或将 qrcode.min.js 放入 assets/js/ 目录离线使用')); return; }
|
||||
var s = document.createElement('script');
|
||||
s.src = CDNS[idx++];
|
||||
s.onload = function () { cb(window.QRCode ? null : new Error('二维码组件加载失败')); };
|
||||
@ -69,7 +77,7 @@ func_bar('二维码生成器');
|
||||
if (!value) { msg.innerHTML = '<div class="err-msg">请输入内容。</div>'; $('qrText').focus(); return; }
|
||||
loadQR(function (err) {
|
||||
if (err) { msg.innerHTML = '<div class="err-msg">' + err.message + '</div>'; return; }
|
||||
if ($('qrText').value.trim() !== value) return; // 期间内容已变
|
||||
if ($('qrText').value.trim() !== value) return;
|
||||
var size = parseInt($('qrSize').value, 10);
|
||||
var box = $('qrBox');
|
||||
box.innerHTML = '';
|
||||
519
includes/db.php
519
includes/db.php
@ -119,8 +119,27 @@ function db_init(PDO $pdo): void
|
||||
created_at TEXT DEFAULT \'\',
|
||||
updated_at TEXT DEFAULT \'\'
|
||||
)');
|
||||
// ---------- IP 黑名单 ----------
|
||||
$pdo->exec('CREATE TABLE IF NOT EXISTS ip_blacklist (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rule TEXT NOT NULL,
|
||||
note TEXT DEFAULT \'\',
|
||||
created_at TEXT NOT NULL DEFAULT \'\',
|
||||
source TEXT NOT NULL DEFAULT \'manual\'
|
||||
)');
|
||||
|
||||
// 兼容旧库:categories 补充 icon 列
|
||||
// ---------- 自动风控计数(登录失败 / 404 次数 / 每分钟请求频率) ----------
|
||||
$pdo->exec('CREATE TABLE IF NOT EXISTS ip_risk (
|
||||
ip TEXT PRIMARY KEY,
|
||||
login_fail INTEGER NOT NULL DEFAULT 0,
|
||||
not_found INTEGER NOT NULL DEFAULT 0,
|
||||
rate_ts INTEGER NOT NULL DEFAULT 0,
|
||||
rate_count INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT \'\'
|
||||
)');
|
||||
|
||||
// 兼容旧库:ip_blacklist 补 source 列;categories 补充 icon 列
|
||||
db_ensure_column($pdo, 'ip_blacklist', 'source', "TEXT NOT NULL DEFAULT 'manual'");
|
||||
db_ensure_column($pdo, 'categories', 'icon', "TEXT DEFAULT ''");
|
||||
// 兼容旧库:quick_links / func_tools 若缺列一并补齐
|
||||
db_ensure_column($pdo, 'quick_links', 'note', "TEXT DEFAULT ''");
|
||||
@ -168,6 +187,7 @@ function db_init(PDO $pdo): void
|
||||
'search_engines'=> '', // 搜索引擎列表 JSON;空 = 使用默认 4 个
|
||||
'last_updated' => '',
|
||||
'hero_bg' => '', // 首页顶部标识栏背景(纯色 / CSS 渐变),空 = 主题默认
|
||||
'hero_mode' => 'weather', // 首页顶部组件模式:weather=天气(默认) / info=访客信息 / blank=空白精简
|
||||
'cdn_ranges' => '', // CDN 段库文本;空 = 使用默认内置列表
|
||||
'article_note' => '', // 文章区首页顶部注要(Markdown 风格,纯文本)
|
||||
];
|
||||
@ -186,12 +206,13 @@ function db_init(PDO $pdo): void
|
||||
}
|
||||
|
||||
// ---------- 功能区工具默认种子 ----------
|
||||
$toolCount = (int)$pdo->query('SELECT COUNT(*) FROM func_tools')->fetchColumn(); if ($toolCount === 0) {
|
||||
$toolCount = (int)$pdo->query('SELECT COUNT(*) FROM func_tools')->fetchColumn();
|
||||
if ($toolCount === 0) {
|
||||
$seedTools = [
|
||||
['编码 / 加解密', '🔐', 'Base64 / Base32 / URL / Unicode / MD5 / SHA 系列', 'codec.php', 0],
|
||||
['随机密码生成', '🔑', '自定义字符集与长度,一次生成多组', 'password.php', 0],
|
||||
['二维码生成器', '▦', '任意内容生成二维码,可放大并下载 PNG', 'qrcode.php', 0],
|
||||
['杀软识别', '🛡', '粘贴 tasklist 输出,匹配进程对应的安全软件', 'av.php', 0],
|
||||
['编码 / 加解密', '🔐', 'Base64 / Base32 / URL / Unicode / MD5 / SHA 系列', 'codec.html', 0],
|
||||
['随机密码生成', '🔑', '自定义字符集与长度,一次生成多组', 'password.html', 0],
|
||||
['二维码生成器', '▦', '任意内容生成二维码,可放大并下载 PNG', 'qrcode.html', 0],
|
||||
['杀软识别', '🛡', '粘贴 tasklist 输出,匹配进程对应的安全软件', 'av.html', 0],
|
||||
];
|
||||
$st = $pdo->prepare('INSERT INTO func_tools (name, icon, description, url, is_external, enabled, sort) VALUES (?,?,?,?,?,1,?)');
|
||||
$sort = 0;
|
||||
@ -202,9 +223,9 @@ function db_init(PDO $pdo): void
|
||||
|
||||
// ---------- 后期新增功能页默认登记(按 url 去重,允许后台再停用/删除) ----------
|
||||
$extraTools = [
|
||||
['国密加解密', '🔏', 'SM2 非对称加解密 / SM3 摘要 / SM4 对称加解密(SM1 为不公开硬件算法)', 'gmcodec.php'],
|
||||
['IP 地址处理', '🌐', '提取日志中的全部 IPv4 并统计出现次数,再按内网 / CDN / 公网区分展示', 'ip.php'],
|
||||
['辅助维权', '🛠️', '反弹 Shell 构建 / Windows SAM·mimikatz·票据提取 / Linux 自启服务(systemd 与 init.d)', 'persist.php'],
|
||||
['国密加解密', '🔏', 'SM2 非对称加解密 / SM3 摘要 / SM4 对称加解密(SM1 为不公开硬件算法)', 'gmcodec.html'],
|
||||
['IP 地址处理', '🌐', '提取日志中的全部 IPv4 并统计出现次数,再按内网 / CDN / 公网区分展示', 'ip.html'],
|
||||
['辅助维权', '🛠️', '反弹 Shell 构建 / Windows SAM·mimikatz·票据提取 / Linux 自启服务(systemd 与 init.d)', 'persist.html'],
|
||||
];
|
||||
$chkUrl = $pdo->prepare('SELECT COUNT(*) FROM func_tools WHERE url = ?');
|
||||
$insTool = $pdo->prepare('INSERT INTO func_tools (name, icon, description, url, is_external, enabled, sort) VALUES (?,?,?,?,0,1,?)');
|
||||
@ -265,6 +286,21 @@ MD;
|
||||
$stA = $pdo->prepare('INSERT INTO articles (part, title, summary, markdown, pinned, enabled, sort, created_at, updated_at) VALUES (?,?,?,?,1,1,1,?,?)');
|
||||
$stA->execute(['使用说明', '文章库使用说明', '了解分区、三栏阅读与 Markdown 图片引用的基本用法。', $sampleMd, $now, $now]);
|
||||
}
|
||||
|
||||
// ---------- 功能区工具登记:旧 PHP 外壳 → 静态 HTML(.php 已改为 302 重定向,入口自动同步迁移) ----------
|
||||
$toolHtmlMap = [
|
||||
'codec.php' => 'codec.html',
|
||||
'password.php' => 'password.html',
|
||||
'qrcode.php' => 'qrcode.html',
|
||||
'av.php' => 'av.html',
|
||||
'ip.php' => 'ip.html',
|
||||
'persist.php' => 'persist.html',
|
||||
'gmcodec.php' => 'gmcodec.html',
|
||||
];
|
||||
$updUrl = $pdo->prepare('UPDATE func_tools SET url = ? WHERE url = ?');
|
||||
foreach ($toolHtmlMap as $oldUrl => $newUrl) {
|
||||
$updUrl->execute([$newUrl, $oldUrl]);
|
||||
}
|
||||
}
|
||||
|
||||
/** 标记内容已更新(首页展示"上次更新时间") */
|
||||
@ -291,3 +327,468 @@ function setting_get(string $key, string $default = ''): string
|
||||
}
|
||||
return array_key_exists($key, $cache) ? $cache[$key] : $default;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 访问日志 + IP 黑名单(自动拦截 / 自动记录)
|
||||
// ============================================================
|
||||
|
||||
/** 访问日志文件路径:data/vistors.log(追加写入、完整保留,可自行归档 / 删除) */
|
||||
const ACCESS_LOG_FILE = DATA_DIR . '/vistors.log';
|
||||
|
||||
/** 当前客户端 IP(REMOTE_ADDR,忽略转发头以免被伪造) */
|
||||
function client_ip(): string
|
||||
{
|
||||
static $cached = null;
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
$ip = trim((string)($_SERVER['REMOTE_ADDR'] ?? ''));
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
$ip = '';
|
||||
}
|
||||
$cached = $ip;
|
||||
return $ip;
|
||||
}
|
||||
|
||||
/** IPv4 转无符号整数;失败返回 -1 */
|
||||
function ip4_int(string $ip): int
|
||||
{
|
||||
$n = ip2long($ip);
|
||||
if ($n === false) {
|
||||
return -1;
|
||||
}
|
||||
return $n < 0 ? $n + 4294967296 : $n;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并解析黑名单规则,返回 [ok, net(掩码后网络号), prefix, 错误说明]
|
||||
* 支持:单 IP(203.0.113.7)、CIDR(203.0.113.0/24)、星号通配(203.0.113.* / 192.168.*.*)
|
||||
*/
|
||||
function blacklist_parse_rule(string $raw): array
|
||||
{
|
||||
$rule = trim($raw);
|
||||
if ($rule === '') {
|
||||
return [false, 0, 0, '规则不能为空。'];
|
||||
}
|
||||
// 单 IPv4
|
||||
if (filter_var($rule, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return [true, ip4_int($rule), 32, ''];
|
||||
}
|
||||
// CIDR 网段
|
||||
if (preg_match('#^(\d{1,3}(?:\.\d{1,3}){3})/(\d{1,2})$#', $rule, $m)) {
|
||||
$prefix = (int)$m[2];
|
||||
if ($prefix > 32 || !filter_var($m[1], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return [false, 0, 0, '网段格式不正确,掩码需在 0-32 之间。'];
|
||||
}
|
||||
$mask = $prefix === 0 ? 0 : ((0xFFFFFFFF << (32 - $prefix)) & 0xFFFFFFFF);
|
||||
return [true, ip4_int($m[1]) & $mask, $prefix, ''];
|
||||
}
|
||||
// 星号通配段:* 只能连续出现在末尾
|
||||
if (strpos($rule, '*') !== false) {
|
||||
$parts = explode('.', $rule);
|
||||
if (count($parts) !== 4) {
|
||||
return [false, 0, 0, '仅支持标准 IPv4 / CIDR 网段 / 星号通配段。'];
|
||||
}
|
||||
$bytes = [0, 0, 0, 0];
|
||||
$prefix = 0;
|
||||
$seen = false;
|
||||
foreach ($parts as $i => $p) {
|
||||
if ($p === '*') {
|
||||
if (!$seen) {
|
||||
$seen = true;
|
||||
$prefix = $i * 8;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($seen || $p === '' || !ctype_digit($p) || (int)$p > 255) {
|
||||
return [false, 0, 0, '通配段格式不正确,* 只能连续出现在末尾。'];
|
||||
}
|
||||
$bytes[$i] = (int)$p;
|
||||
}
|
||||
if (!$seen) {
|
||||
return [false, 0, 0, '规则格式不正确。'];
|
||||
}
|
||||
$net = (($bytes[0] << 24) | ($bytes[1] << 16) | ($bytes[2] << 8) | $bytes[3]) & 0xFFFFFFFF;
|
||||
return [true, $net, $prefix, ''];
|
||||
}
|
||||
return [false, 0, 0, '仅支持标准 IPv4、CIDR 网段或星号通配段。'];
|
||||
}
|
||||
|
||||
/** 读取全部黑名单规则(仅返回解析成功的记录,静态缓存避免单次请求重复查询) */
|
||||
function blacklist_rules(bool $refresh = false): array
|
||||
{
|
||||
static $cache = null;
|
||||
if ($cache !== null && !$refresh) {
|
||||
return $cache;
|
||||
}
|
||||
$cache = [];
|
||||
try {
|
||||
foreach (db()->query('SELECT id, rule, note, created_at FROM ip_blacklist ORDER BY id ASC') as $row) {
|
||||
[$ok, $net, $prefix] = blacklist_parse_rule((string)$row['rule']);
|
||||
if (!$ok) {
|
||||
continue;
|
||||
}
|
||||
$cache[] = [
|
||||
'id' => (int)$row['id'],
|
||||
'rule' => (string)$row['rule'],
|
||||
'note' => (string)$row['note'],
|
||||
'created_at' => (string)$row['created_at'],
|
||||
'net' => $net,
|
||||
'prefix' => $prefix,
|
||||
];
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$cache = [];
|
||||
}
|
||||
return $cache;
|
||||
}
|
||||
|
||||
/** 当前 IPv4 是否命中黑名单;未命中或非 IPv4 返回 null,命中返回规则记录 */
|
||||
function blacklist_ip_hit(string $ip): ?array
|
||||
{
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return null;
|
||||
}
|
||||
$int = ip4_int($ip);
|
||||
foreach (blacklist_rules() as $r) {
|
||||
if ($r['prefix'] === 32) {
|
||||
if ($r['net'] === $int) {
|
||||
return $r;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$mask = $r['prefix'] === 0 ? 0 : ((0xFFFFFFFF << (32 - $r['prefix'])) & 0xFFFFFFFF);
|
||||
if (($int & $mask) === $r['net']) {
|
||||
return $r;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 请求结束统一追加写入一条访问日志(含被黑名单拦截的 403) */
|
||||
function access_log_flush(): void
|
||||
{
|
||||
try {
|
||||
$ip = client_ip();
|
||||
if ($ip === '') {
|
||||
return;
|
||||
}
|
||||
$method = (string)($_SERVER['REQUEST_METHOD'] ?? '');
|
||||
$method = $method !== '' ? strtoupper($method) : 'GET';
|
||||
$uri = (string)($_SERVER['REQUEST_URI'] ?? '/');
|
||||
// 登录口令、CSRF 等敏感 query 参数脱敏后再写日志
|
||||
$uri = preg_replace('/([?&](?:password|passwd|old_pwd|new_pwd|new_pwd2|csrf|token|secret)[^=]*=)[^&]*/i', '$1***', $uri);
|
||||
$uri = substr((string)$uri, 0, 600);
|
||||
$code = http_response_code();
|
||||
if (!is_int($code) || $code < 100 || $code > 599) {
|
||||
$code = 200;
|
||||
}
|
||||
// 每行:时间 / IP / 请求方式 / 响应码 / 访问 URL(制表符分隔)
|
||||
$line = date('Y-m-d H:i:s') . "\t" . $ip . "\t" . $method . "\t" . $code . "\t" . $uri . "\n";
|
||||
@file_put_contents(ACCESS_LOG_FILE, $line, FILE_APPEND | LOCK_EX);
|
||||
// 自动风控:前台 404 响应计入累计(是否达到阈值触发封禁见 risk_count_not_found / access_boot 自检)
|
||||
$isAdmin = strpos((string)($_SERVER['SCRIPT_NAME'] ?? ''), '/admin/') === 0;
|
||||
if (!$isAdmin && $code === 404) {
|
||||
risk_count_not_found($ip);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// 日志写入失败不影响页面主流程
|
||||
}
|
||||
}
|
||||
|
||||
/** 读取访问日志文件,解析为记录数组(新→旧排序),供后台“访问信息”页使用 */
|
||||
function visitors_entries(): array
|
||||
{
|
||||
$out = [];
|
||||
if (!is_file(ACCESS_LOG_FILE)) {
|
||||
return $out;
|
||||
}
|
||||
$fh = @fopen(ACCESS_LOG_FILE, 'rb');
|
||||
if (!$fh) {
|
||||
return $out;
|
||||
}
|
||||
while (($line = fgets($fh)) !== false) {
|
||||
$line = rtrim($line, "\r\n");
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
$p = explode("\t", $line);
|
||||
if (count($p) < 5) {
|
||||
continue;
|
||||
}
|
||||
$out[] = [
|
||||
'created_at' => (string)$p[0],
|
||||
'ip' => (string)$p[1],
|
||||
'method' => (string)$p[2],
|
||||
'status' => (int)$p[3],
|
||||
'url' => (string)$p[4],
|
||||
];
|
||||
}
|
||||
fclose($fh);
|
||||
return array_reverse($out); // 新→旧
|
||||
}
|
||||
|
||||
/** 自动启动:注册访问日志;黑名单对所有请求(前台与后台登录页)统一拦截;自动风控自检仅针对前台 */
|
||||
function access_boot(): void
|
||||
{
|
||||
if (PHP_SAPI === 'cli') {
|
||||
return;
|
||||
}
|
||||
register_shutdown_function('access_log_flush');
|
||||
$ip = client_ip();
|
||||
if ($ip === '') {
|
||||
return;
|
||||
}
|
||||
$isAdmin = strpos((string)($_SERVER['SCRIPT_NAME'] ?? ''), '/admin/') === 0;
|
||||
// 1) 已在黑名单(手动 / 自动):前台与后台一致拦截,统一返回风控提示页
|
||||
if (blacklist_ip_hit($ip) !== null) {
|
||||
risk_block_page();
|
||||
}
|
||||
// 2) 自动风控自检仅针对前台(后台正常维护不计入 404 / 频率,避免管理员被自身操作误伤)
|
||||
if (!$isAdmin) {
|
||||
$cfg = risk_rule_cfg();
|
||||
// 2a) 请求频率:先累计本请求,超过阈值即封禁
|
||||
if ($cfg['rate']['on'] && risk_bump_rate($ip, (int)$cfg['rate']['n'])) {
|
||||
risk_ban($ip, '请求频率超过阈值(60 秒内 ' . (int)$cfg['rate']['n'] . ' 次)');
|
||||
risk_block_page();
|
||||
}
|
||||
// 2b) 404 累计已达标(若上一请求刚触发封禁,这里会因命中黑名单直接拦截,此分支为兑底)
|
||||
if ($cfg['not_found']['on']) {
|
||||
$row = risk_row($ip);
|
||||
if ((int)$row['not_found'] >= (int)$cfg['not_found']['n']) {
|
||||
risk_ban($ip, '404 累计次数达阈值(' . (int)$cfg['not_found']['n'] . ' 次)');
|
||||
risk_block_page();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 自动风控:登录失败 / 404 次数 / 每分钟请求频率(可启停、可设阈值)
|
||||
// ============================================================
|
||||
|
||||
/** 读取 settings 键值全表(规避 setting_get 的静态缓存;供风控配置与拦截提示使用) */
|
||||
function risk_settings_map(): array
|
||||
{
|
||||
$map = [];
|
||||
try {
|
||||
foreach (db()->query('SELECT key, value FROM settings') as $r) {
|
||||
$map[(string)$r['key']] = (string)$r['value'];
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$map = [];
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 站长自定义拦截提示:main=主提示 sub=次行说明(留空回落默认文案);
|
||||
* 另附 raw_main / raw_sub 原始保存值(空串表示未自定义)。
|
||||
*/
|
||||
function risk_block_tip(): array
|
||||
{
|
||||
$map = risk_settings_map();
|
||||
$rawMain = trim((string)($map['risk_tip_main'] ?? ''));
|
||||
$rawSub = trim((string)($map['risk_tip_sub'] ?? ''));
|
||||
$main = $rawMain !== '' ? $rawMain : '您触发了本站风控,请稍后再访问';
|
||||
$sub = $rawSub !== '' ? $rawSub : '如为正常访问,请稍后重试;若频繁误判,请联系站点管理员处理。';
|
||||
return ['main' => $main, 'sub' => $sub, 'raw_main' => $rawMain, 'raw_sub' => $rawSub];
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取风控规则配置(始终直查 settings 表,避免缓存旧值)。
|
||||
* 返回:login_fail / not_found / rate,每项含 on(启用) n(阈值) tip(说明)。
|
||||
*/
|
||||
function risk_rule_cfg(): array
|
||||
{
|
||||
$defs = [
|
||||
'login_fail' => [1, 10, '后台登录失败累计达到阈值后,该 IP 被自动加入黑名单(永久封禁)。'],
|
||||
'not_found' => [1, 100, '前台访问返回 404 累计达到阈值后,该 IP 被自动加入黑名单(永久封禁)。'],
|
||||
'rate' => [0, 120, '60 秒内请求次数超过阈值后,该 IP 被自动加入黑名单(永久封禁);多人共享同一出口 IP 时建议关闭。'],
|
||||
];
|
||||
$map = risk_settings_map();
|
||||
$out = [];
|
||||
// 存储键别名:后台保存使用 risk_login_* / risk_404_* / risk_rate_*
|
||||
$alias = ['login_fail' => 'login', 'not_found' => '404', 'rate' => 'rate'];
|
||||
foreach ($defs as $k => $d) {
|
||||
$pre = $alias[$k];
|
||||
$out[$k] = [
|
||||
'on' => (($map['risk_' . $pre . '_on'] ?? (string)$d[0]) === '1'),
|
||||
'n' => max(1, min(1000000, (int)($map['risk_' . $pre . '_n'] ?? (string)$d[1]))),
|
||||
'tip' => $d[2],
|
||||
];
|
||||
}
|
||||
// 拦截提示(含原始保存值,供后台编辑回显与占位提示)
|
||||
$tip = risk_block_tip();
|
||||
$out['_tip'] = [
|
||||
'main' => $tip['main'],
|
||||
'sub' => $tip['sub'],
|
||||
'raw_main' => $tip['raw_main'],
|
||||
'raw_sub' => $tip['raw_sub'],
|
||||
];
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** 读取某 IP 的风控计数行(不存在时返回全 0) */
|
||||
function risk_row(string $ip): array
|
||||
{
|
||||
$empty = ['ip' => $ip, 'login_fail' => 0, 'not_found' => 0, 'rate_ts' => 0, 'rate_count' => 0, 'updated_at' => ''];
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return $empty;
|
||||
}
|
||||
try {
|
||||
$st = db()->prepare('SELECT ip, login_fail, not_found, rate_ts, rate_count, updated_at FROM ip_risk WHERE ip = ? LIMIT 1');
|
||||
$st->execute([$ip]);
|
||||
$r = $st->fetch();
|
||||
return $r ? $r : $empty;
|
||||
} catch (Throwable $e) {
|
||||
return $empty;
|
||||
}
|
||||
}
|
||||
|
||||
/** 写入某 IP 的风控计数 */
|
||||
function risk_upsert(string $ip, int $loginFail, int $notFound, int $rateTs, int $rateCount): void
|
||||
{
|
||||
try {
|
||||
$st = db()->prepare('INSERT INTO ip_risk (ip, login_fail, not_found, rate_ts, rate_count, updated_at) VALUES (?,?,?,?,?,?)
|
||||
ON CONFLICT(ip) DO UPDATE SET login_fail=excluded.login_fail, not_found=excluded.not_found,
|
||||
rate_ts=excluded.rate_ts, rate_count=excluded.rate_count, updated_at=excluded.updated_at');
|
||||
$st->execute([$ip, $loginFail, $notFound, $rateTs, $rateCount, date('Y-m-d H:i:s')]);
|
||||
} catch (Throwable $e) {
|
||||
// 风控写入失败不影响页面主流程
|
||||
}
|
||||
}
|
||||
|
||||
/** 将某 IPv4 永久封禁(幂等:已存在该单 IP 规则则不再重复添加),并刷新黑名单缓存 */
|
||||
function risk_ban(string $ip, string $reason): void
|
||||
{
|
||||
$ip = trim($ip);
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return;
|
||||
}
|
||||
$int = ip4_int($ip);
|
||||
foreach (blacklist_rules() as $r) {
|
||||
if ($r['prefix'] === 32 && $r['net'] === $int) {
|
||||
return; // 已存在
|
||||
}
|
||||
}
|
||||
try {
|
||||
$st = db()->prepare('INSERT INTO ip_blacklist (rule, note, created_at, source) VALUES (?,?,?,?)');
|
||||
$st->execute([$ip, '风控自动封禁:' . $reason, date('Y-m-d H:i:s'), 'auto']);
|
||||
blacklist_rules(true);
|
||||
} catch (Throwable $e) {
|
||||
// 忽略:避免风控本身拖垮请求
|
||||
}
|
||||
}
|
||||
|
||||
/** 后台登录失败累计;规则启用且达到阈值时自动封禁。返回 [当前失败次数, 本次是否触发封禁, 阈值] */
|
||||
function risk_login_fail(string $ip): array
|
||||
{
|
||||
$empty = [0, false, 0];
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return $empty;
|
||||
}
|
||||
$cfg = risk_rule_cfg();
|
||||
$row = risk_row($ip);
|
||||
$lf = (int)$row['login_fail'] + 1;
|
||||
$n = (int)$cfg['login_fail']['n'];
|
||||
risk_upsert($ip, $lf, (int)$row['not_found'], (int)$row['rate_ts'], (int)$row['rate_count']);
|
||||
$banned = $cfg['login_fail']['on'] && $lf >= $n;
|
||||
if ($banned) {
|
||||
risk_ban($ip, '后台登录失败累计达阈值(' . $n . ' 次)');
|
||||
}
|
||||
return [$lf, $banned, $n];
|
||||
}
|
||||
|
||||
/** 请求频率(每分钟窗口)累计;返回本次累计后是否达到阈值 */
|
||||
function risk_bump_rate(string $ip, int $threshold): bool
|
||||
{
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return false;
|
||||
}
|
||||
$win = (int)floor(time() / 60);
|
||||
$row = risk_row($ip);
|
||||
$cnt = ((int)$row['rate_ts'] === $win) ? ((int)$row['rate_count'] + 1) : 1;
|
||||
risk_upsert($ip, (int)$row['login_fail'], (int)$row['not_found'], $win, $cnt);
|
||||
return $cnt >= $threshold;
|
||||
}
|
||||
|
||||
/** 前台 404 响应累计;规则启用且达到阈值时自动封禁(本响应结束后生效,下一请求即被拦截) */
|
||||
function risk_count_not_found(string $ip): void
|
||||
{
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return;
|
||||
}
|
||||
$cfg = risk_rule_cfg();
|
||||
$row = risk_row($ip);
|
||||
$nf = (int)$row['not_found'] + 1;
|
||||
risk_upsert($ip, (int)$row['login_fail'], $nf, (int)$row['rate_ts'], (int)$row['rate_count']);
|
||||
if ($cfg['not_found']['on'] && $nf >= (int)$cfg['not_found']['n']) {
|
||||
risk_ban($ip, '404 累计次数达阈值(' . (int)$cfg['not_found']['n'] . ' 次)');
|
||||
}
|
||||
}
|
||||
|
||||
/** 清除某 IP 的风控计数(解封单 IP 黑名单时调用) */
|
||||
function risk_clear_for_ip(string $ip): void
|
||||
{
|
||||
$ip = trim($ip);
|
||||
if ($ip === '') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$st = db()->prepare('DELETE FROM ip_risk WHERE ip = ?');
|
||||
$st->execute([$ip]);
|
||||
} catch (Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/** 清空全部风控计数(不影响黑名单本身) */
|
||||
function risk_clear_all(): void
|
||||
{
|
||||
try {
|
||||
db()->exec('DELETE FROM ip_risk');
|
||||
} catch (Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/** 命中黑名单 / 触发风控时的统一提示页(403) */
|
||||
function risk_block_page(): void
|
||||
{
|
||||
if (!headers_sent()) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
}
|
||||
$site = '本站';
|
||||
try {
|
||||
$site = setting_get('site_name', '知识导航站');
|
||||
} catch (Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
$tip = risk_block_tip();
|
||||
$s = htmlspecialchars($site, ENT_QUOTES, 'UTF-8');
|
||||
$m = htmlspecialchars($tip['main'], ENT_QUOTES, 'UTF-8');
|
||||
$sb = htmlspecialchars($tip['sub'], ENT_QUOTES, 'UTF-8');
|
||||
echo '<!DOCTYPE html>' . "\n"
|
||||
. '<html lang="zh-CN"><head><meta charset="utf-8">'
|
||||
. '<meta name="viewport" content="width=device-width, initial-scale=1">'
|
||||
. '<meta name="robots" content="noindex,nofollow"><title>访问受限 - ' . $s . '</title>'
|
||||
. '<style>html,body{margin:0;height:100%}body{display:flex;align-items:center;justify-content:center;'
|
||||
. 'background:#f4f6f9;font-family:-apple-system,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif}'
|
||||
. '.card{max-width:420px;margin:20px;padding:38px 30px;background:#fff;border-radius:16px;'
|
||||
. 'box-shadow:0 10px 34px rgba(15,23,42,.10);text-align:center}.code{font-size:52px;font-weight:800;color:#e11d48;line-height:1}'
|
||||
. '.t1{margin:14px 0 6px;font-size:17px;color:#0f172a}.t2{margin:0;font-size:13px;color:#64748b;line-height:1.7}'
|
||||
. '.st{display:inline-block;margin-top:18px;font-size:12px;color:#94a3b8}'
|
||||
. '@media (prefers-color-scheme:dark){body{background:#0b1220}.card{background:#111a2c;'
|
||||
. 'box-shadow:0 10px 34px rgba(0,0,0,.5)}.t1{color:#e6ecf7}.t2{color:#8ea0bd}.st{color:#526180}}'
|
||||
. '</style></head><body>'
|
||||
. '<div class="card"><div class="code">403</div>'
|
||||
. '<div class="t1">' . $m . '</div>'
|
||||
. '<div class="t2">' . $sb . '</div>'
|
||||
. '<span class="st">' . $s . '</span>'
|
||||
. '</div></body></html>';
|
||||
exit;
|
||||
}
|
||||
|
||||
access_boot();
|
||||
|
||||
@ -115,6 +115,75 @@ function home_quote(): string
|
||||
return $lines[array_rand($lines)];
|
||||
}
|
||||
|
||||
/** 首页访客信息(hero 信息模式):IP 与浏览器/系统/内核由服务端解析 UA 提供,屏幕/语言由前端补齐 */
|
||||
function visitor_brief(): array
|
||||
{
|
||||
$ip = trim((string)($_SERVER['REMOTE_ADDR'] ?? ''));
|
||||
if ($ip === '' || !filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
$ip = '未知';
|
||||
}
|
||||
$ua = (string)($_SERVER['HTTP_USER_AGENT'] ?? '');
|
||||
$browser = '未知';
|
||||
$kernel = '未知';
|
||||
$os = '未知';
|
||||
if ($ua !== '') {
|
||||
// 浏览器 / 内核
|
||||
if (strpos($ua, 'MicroMessenger') !== false) {
|
||||
$browser = '微信内置浏览器';
|
||||
$kernel = 'WebKit';
|
||||
} elseif (preg_match('/Edg\/([\d.]+)/', $ua, $m)) {
|
||||
$browser = 'Edge ' . $m[1];
|
||||
$kernel = 'Blink';
|
||||
} elseif (preg_match('/OPR\/([\d.]+)/', $ua, $m)) {
|
||||
$browser = 'Opera ' . $m[1];
|
||||
$kernel = 'Blink';
|
||||
} elseif (strpos($ua, 'Firefox/') !== false) {
|
||||
$browser = 'Firefox';
|
||||
$kernel = 'Gecko';
|
||||
if (preg_match('/Firefox\/([\d.]+)/', $ua, $m)) $browser .= ' ' . $m[1];
|
||||
} elseif (preg_match('/(?:Chrome|CriOS)\/([\d.]+)/', $ua, $m)) {
|
||||
$browser = 'Chrome ' . $m[1];
|
||||
$kernel = 'Blink';
|
||||
} elseif (strpos($ua, 'Safari/') !== false) {
|
||||
$browser = 'Safari';
|
||||
$kernel = 'WebKit';
|
||||
if (preg_match('/Version\/([\d.]+)/', $ua, $m)) $browser .= ' ' . $m[1];
|
||||
}
|
||||
if ($kernel === '未知') {
|
||||
if (strpos($ua, 'Gecko/') !== false) $kernel = 'Gecko';
|
||||
elseif (strpos($ua, 'AppleWebKit/') !== false) $kernel = 'WebKit';
|
||||
}
|
||||
// 操作系统
|
||||
if (strpos($ua, 'Windows NT 10.0') !== false) {
|
||||
$os = 'Windows 10 / 11';
|
||||
} elseif (strpos($ua, 'Windows NT 6.3') !== false) {
|
||||
$os = 'Windows 8.1';
|
||||
} elseif (strpos($ua, 'Windows NT 6.2') !== false) {
|
||||
$os = 'Windows 8';
|
||||
} elseif (strpos($ua, 'Windows NT 6.1') !== false) {
|
||||
$os = 'Windows 7';
|
||||
} elseif (strpos($ua, 'Windows NT 5.1') !== false) {
|
||||
$os = 'Windows XP';
|
||||
} elseif (strpos($ua, 'Windows Phone') !== false) {
|
||||
$os = 'Windows Phone';
|
||||
} elseif (strpos($ua, 'Android') !== false) {
|
||||
$os = 'Android';
|
||||
if (preg_match('/Android ([\d.]+)/', $ua, $m)) $os .= ' ' . $m[1];
|
||||
} elseif (strpos($ua, 'iPad') !== false || strpos($ua, 'iPhone') !== false || strpos($ua, 'iPod') !== false) {
|
||||
$os = 'iOS';
|
||||
if (preg_match('/OS ([\d_]+)/', $ua, $m)) $os .= ' ' . str_replace('_', '.', $m[1]);
|
||||
} elseif (strpos($ua, 'Mac OS X') !== false) {
|
||||
$os = 'macOS';
|
||||
if (preg_match('/Mac OS X ([\d_]+)/', $ua, $m)) $os .= ' ' . str_replace('_', '.', $m[1]);
|
||||
} elseif (strpos($ua, 'CrOS') !== false) {
|
||||
$os = 'ChromeOS';
|
||||
} elseif (strpos($ua, 'Linux') !== false) {
|
||||
$os = 'Linux';
|
||||
}
|
||||
}
|
||||
return ['ip' => $ip, 'browser' => $browser, 'os' => $os, 'kernel' => $kernel];
|
||||
}
|
||||
|
||||
/** 图标值是否属于图片(dataURL / http / 常见图片扩展 / 相对路径) */
|
||||
function icon_is_image(string $raw): bool
|
||||
{
|
||||
|
||||
32
index.php
32
index.php
@ -9,6 +9,15 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
$siteName = setting_get('site_name', '知识导航站');
|
||||
$siteSlogan = setting_get('site_slogan', '');
|
||||
$heroBg = setting_get('hero_bg', ''); // 后台自定义顶部背景(纯色 / CSS 渐变)
|
||||
// 首页顶部组件模式:weather=天气(默认) / info=访客信息 / blank=空白精简
|
||||
$heroMode = setting_get('hero_mode', 'weather');
|
||||
if (!in_array($heroMode, ['weather', 'info', 'blank'], true)) {
|
||||
$heroMode = 'weather';
|
||||
}
|
||||
$visitor = [];
|
||||
if ($heroMode === 'info') {
|
||||
$visitor = visitor_brief();
|
||||
}
|
||||
$lastUpdated = setting_get('last_updated', '');
|
||||
if ($lastUpdated === '') {
|
||||
$lastUpdated = '暂无更新记录';
|
||||
@ -60,7 +69,7 @@ $catHint = [
|
||||
layout_head('');
|
||||
?>
|
||||
<!-- Hero 顶部展示栏 -->
|
||||
<header class="hero"<?php if ($heroBg !== ''): ?> style="background:<?= he($heroBg) ?>"<?php endif; ?>>
|
||||
<header class="hero<?= $heroMode === 'blank' ? ' hero-slim' : '' ?>"<?php if ($heroBg !== ''): ?> style="background:<?= he($heroBg) ?>"<?php endif; ?>>
|
||||
<div class="wrap hero-inner">
|
||||
<div class="hero-top">
|
||||
<div class="brand">
|
||||
@ -77,9 +86,24 @@ layout_head('');
|
||||
<div class="hm-row"><span>上次更新 </span><span class="val"><?= he($lastUpdated) ?></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="weather-box">
|
||||
<iframe width="800" height="150" src="https://i.tianqi.com/?c=code&a=getcode&id=48&num=6&icon=1" frameborder="0"></iframe>
|
||||
</div>
|
||||
<?php if ($heroMode === 'weather'): ?>
|
||||
<!-- 天气模式(默认):第三方天气条 -->
|
||||
<div class="weather-box">
|
||||
<iframe width="800" height="150" src="https://i.tianqi.com/?c=code&a=getcode&id=48&num=6&icon=1" frameborder="0"></iframe>
|
||||
</div>
|
||||
<?php elseif ($heroMode === 'info'): ?>
|
||||
<!-- 访客信息模式:IP / 浏览器 / 系统 / 内核 / 屏幕 / 语言 -->
|
||||
<div class="visitor-box js-visitor-box">
|
||||
<div class="vb-grid">
|
||||
<div class="vb-cell"><span>访问者 IP</span><b><?= he((string)($visitor['ip'] ?? '未知')) ?></b></div>
|
||||
<div class="vb-cell"><span>浏览器</span><b><?= he((string)($visitor['browser'] ?? '未知')) ?></b></div>
|
||||
<div class="vb-cell"><span>操作系统</span><b><?= he((string)($visitor['os'] ?? '未知')) ?></b></div>
|
||||
<div class="vb-cell"><span>内核 / 渲染引擎</span><b><?= he((string)($visitor['kernel'] ?? '未知')) ?></b></div>
|
||||
<div class="vb-cell"><span>屏幕分辨率</span><b class="js-vscreen">--</b></div>
|
||||
<div class="vb-cell"><span>系统语言</span><b class="js-vlang">--</b></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
8
nav.php
8
nav.php
@ -227,10 +227,10 @@ layout_head($catName);
|
||||
<div class="wrap">
|
||||
<!-- 主导航:分区文本居中,点击下滑线滑动指示;库切换请从首页板块入口进入 -->
|
||||
<nav class="lnx-nav" id="lnxNav">
|
||||
<button type="button" class="lnx-tab active" data-t="nav">🧭 导航区</button>
|
||||
<button type="button" class="lnx-tab" data-t="tools">🧰 内部功能区</button>
|
||||
<button type="button" class="lnx-tab" data-t="art">📚 文章区</button>
|
||||
<button type="button" class="lnx-tab" data-t="ext">🔗 外部工具区</button>
|
||||
<button type="button" class="lnx-tab active" data-t="nav">导航区</button>
|
||||
<button type="button" class="lnx-tab" data-t="tools">功能区</button>
|
||||
<button type="button" class="lnx-tab" data-t="art">文章区</button>
|
||||
<button type="button" class="lnx-tab" data-t="ext">工具区</button>
|
||||
<span class="lnx-ink" id="lnxInk" aria-hidden="true"></span>
|
||||
</nav>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user