feat: 管理员白名单与个性化配置权限控制
系统配置仅允许 admin_list.txt 白名单 IP 编辑;localhost 访问时解析为 LAN IP;个性化配置支持按 IP 权限隔离,新增时自动填充当前 IP。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2b8cb3ca9a
commit
9bec3208f8
120
admin.html
120
admin.html
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>API Proxy Mock 配置管理</title>
|
||||
<title>企业级数据Mock管理系统 - by tony</title>
|
||||
<link rel="stylesheet" href="/assets/element-ui/index.css" />
|
||||
<style>
|
||||
body {
|
||||
@ -56,6 +56,10 @@
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 120px);
|
||||
}
|
||||
.basic-config-readonly {
|
||||
pointer-events: none;
|
||||
opacity: 0.65;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -227,11 +231,21 @@
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="基础配置" name="basic">
|
||||
<el-tab-pane label="系统配置" name="basic">
|
||||
<el-card class="section-card">
|
||||
<div style="display:flex;justify-content:flex-end;align-items:center;margin-bottom:12px;">
|
||||
<el-button size="mini" icon="el-icon-refresh" :loading="configRefreshing" @click="refreshConfig">刷新</el-button>
|
||||
</div>
|
||||
<el-alert
|
||||
v-if="!canEditBasicConfig"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-bottom:16px;"
|
||||
:title="'当前 IP(' + (clientIp || '未知') + ')不在管理员白名单内,无法修改系统配置'"
|
||||
description="系统配置仅允许 admin_list.txt 中的 IP 访问与编辑。"
|
||||
></el-alert>
|
||||
<div :class="{ 'basic-config-readonly': !canEditBasicConfig }">
|
||||
<el-form :model="form.config" label-width="180px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
@ -294,24 +308,30 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="IP管理" name="ip">
|
||||
<el-tab-pane label="个性化配置" name="ip">
|
||||
<el-card class="section-card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||
<div>
|
||||
<strong>IP 访问策略</strong>
|
||||
<strong>个性化配置</strong>
|
||||
<div class="small-text" style="margin-top:4px;">
|
||||
按客户端 IP 拉黑或配置独立目标环境(不含本地代理端口),便于开发/测试分流。
|
||||
通过 localhost 访问时会自动匹配本机唯一 LAN IP 规则。
|
||||
<a href="/__client-ip" target="_blank" style="color:#409EFF;">查看当前请求识别到的 IP</a>
|
||||
为当前 IP 配置独立目标环境(不含本地代理端口),覆盖全局系统配置。
|
||||
普通用户仅可管理 IP 为 <strong>{{ clientIp || "未知" }}</strong> 的配置;管理员可管理全部。
|
||||
<a href="/__client-ip" target="_blank" style="color:#409EFF;">查看当前识别 IP</a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<el-input size="small" v-model="ipRuleSearch" placeholder="搜索 IP 或备注" clearable style="width:220px;" prefix-icon="el-icon-search"></el-input>
|
||||
<el-button size="mini" icon="el-icon-refresh" :loading="ipRulesRefreshing" @click="refreshIpRules">刷新</el-button>
|
||||
<el-button size="mini" type="primary" @click="openIpRuleDialogForCreate">新增 IP 规则</el-button>
|
||||
<el-button
|
||||
v-if="canCreateIpRule"
|
||||
size="mini"
|
||||
type="primary"
|
||||
@click="openIpRuleDialogForCreate"
|
||||
>{{ isAdmin ? "新增配置" : "配置我的环境" }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="pagedIpRules" border style="width: 100%;">
|
||||
@ -346,9 +366,12 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="canManageIpRule(scope.row)">
|
||||
<el-button size="mini" type="primary" plain @click="openIpRuleDialogForEdit(scope.row)">修改</el-button>
|
||||
<el-button size="mini" type="danger" @click="removeIpRule(scope.row)">删除</el-button>
|
||||
</template>
|
||||
<span v-else class="small-text">无权限</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
@ -619,7 +642,7 @@
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="ipRuleDialog.mode === 'edit' ? '修改 IP 规则' : '新增 IP 规则'"
|
||||
:title="ipRuleDialog.mode === 'edit' ? '修改个性化配置' : (isAdmin ? '新增个性化配置' : '配置我的环境')"
|
||||
:visible.sync="ipRuleDialog.visible"
|
||||
width="760px"
|
||||
>
|
||||
@ -627,21 +650,24 @@
|
||||
<el-form-item label="IP 地址">
|
||||
<el-input
|
||||
v-model="ipRuleDialog.form.ip"
|
||||
placeholder="如 192.168.1.100"
|
||||
:disabled="ipRuleDialog.mode === 'edit'"
|
||||
placeholder="自动识别当前 IP"
|
||||
disabled
|
||||
></el-input>
|
||||
<div class="small-text" style="margin-top:4px;">
|
||||
{{ ipRuleDialog.mode === 'create' ? '自动填充当前 IP,不可修改' : 'IP 地址不可修改' }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="ipRuleDialog.form.remark" placeholder="如:测试组、开发组"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="拉黑访问">
|
||||
<el-form-item v-if="isAdmin" label="拉黑访问">
|
||||
<el-switch v-model="ipRuleDialog.form.blocked" @change="onIpRuleBlockedChange"></el-switch>
|
||||
<div class="small-text" style="margin-top:4px;">开启后该 IP 的所有请求将被拒绝(403)</div>
|
||||
</el-form-item>
|
||||
<template v-if="!ipRuleDialog.form.blocked">
|
||||
<el-form-item label="个性化配置">
|
||||
<el-switch v-model="ipRuleDialog.form.useCustomConfig"></el-switch>
|
||||
<div class="small-text" style="margin-top:4px;">开启后可为此 IP 单独配置目标环境,覆盖全局基础配置</div>
|
||||
<div class="small-text" style="margin-top:4px;">开启后可为此 IP 单独配置目标环境,覆盖全局系统配置</div>
|
||||
</el-form-item>
|
||||
<template v-if="ipRuleDialog.form.useCustomConfig">
|
||||
<el-divider content-position="left">独立环境配置</el-divider>
|
||||
@ -714,6 +740,9 @@
|
||||
mocksRefreshing: false,
|
||||
apiRefreshing: false,
|
||||
configRefreshing: false,
|
||||
canEditBasicConfig: false,
|
||||
clientIp: "",
|
||||
isAdmin: false,
|
||||
ipRulesRefreshing: false,
|
||||
ipRules: [],
|
||||
ipRuleSearch: "",
|
||||
@ -907,7 +936,7 @@
|
||||
this.configRefreshing = true;
|
||||
try {
|
||||
await this.loadConfig();
|
||||
this.$message.success("基础配置已刷新");
|
||||
this.$message.success("系统配置已刷新");
|
||||
} finally {
|
||||
this.configRefreshing = false;
|
||||
}
|
||||
@ -916,7 +945,7 @@
|
||||
this.ipRulesRefreshing = true;
|
||||
try {
|
||||
await this.loadIpRules();
|
||||
this.$message.success("IP 规则已刷新");
|
||||
this.$message.success("个性化配置已刷新");
|
||||
} finally {
|
||||
this.ipRulesRefreshing = false;
|
||||
}
|
||||
@ -929,16 +958,31 @@
|
||||
throw new Error(data.error || "加载 IP 规则失败");
|
||||
}
|
||||
this.ipRules = Array.isArray(data.list) ? data.list : [];
|
||||
if (data.clientIp) {
|
||||
this.clientIp = data.clientIp;
|
||||
}
|
||||
if (data.isAdmin === true) {
|
||||
this.isAdmin = true;
|
||||
} else if (data.isAdmin === false) {
|
||||
this.isAdmin = false;
|
||||
}
|
||||
this.ipRulePagination.currentPage = 1;
|
||||
} catch (err) {
|
||||
this.ipRules = [];
|
||||
this.$message.error("加载 IP 规则失败: " + err.message);
|
||||
this.$message.error("加载个性化配置失败: " + err.message);
|
||||
}
|
||||
},
|
||||
canManageIpRule: function (rule) {
|
||||
if (!rule) return false;
|
||||
if (this.isAdmin) return true;
|
||||
var ruleIp = (rule.ip || "").trim();
|
||||
var mine = (this.clientIp || "").trim();
|
||||
return ruleIp && mine && ruleIp === mine;
|
||||
},
|
||||
getDefaultIpRuleForm: function () {
|
||||
var global = this.form.config || {};
|
||||
return {
|
||||
ip: "",
|
||||
ip: this.clientIp || "",
|
||||
remark: "",
|
||||
blocked: false,
|
||||
useCustomConfig: true,
|
||||
@ -967,12 +1011,23 @@
|
||||
this.ipRuleDialog.form.useCustomConfig = false;
|
||||
}
|
||||
},
|
||||
openIpRuleDialogForCreate: function () {
|
||||
openIpRuleDialogForCreate: async function () {
|
||||
if (!this.canCreateIpRule) {
|
||||
this.$message.warning("您已有个性化配置,请直接修改");
|
||||
return;
|
||||
}
|
||||
if (!this.clientIp) {
|
||||
await this.loadIpRules();
|
||||
}
|
||||
this.ipRuleDialog.mode = "create";
|
||||
this.ipRuleDialog.form = this.getDefaultIpRuleForm();
|
||||
this.ipRuleDialog.visible = true;
|
||||
},
|
||||
openIpRuleDialogForEdit: function (rule) {
|
||||
if (!this.canManageIpRule(rule)) {
|
||||
this.$message.error("只能修改与当前 IP 匹配的个性化配置");
|
||||
return;
|
||||
}
|
||||
var cfg = rule.config || {};
|
||||
this.ipRuleDialog.mode = "edit";
|
||||
this.ipRuleDialog.form = {
|
||||
@ -993,19 +1048,28 @@
|
||||
submitIpRuleDialog: async function () {
|
||||
var form = this.ipRuleDialog.form;
|
||||
var ip = (form.ip || "").trim();
|
||||
if (!this.isAdmin || this.ipRuleDialog.mode === "create") {
|
||||
ip = (this.clientIp || "").trim();
|
||||
form.blocked = false;
|
||||
}
|
||||
if (!ip) {
|
||||
this.$message.error("IP 地址不能为空");
|
||||
return;
|
||||
}
|
||||
if (!form.blocked && !form.useCustomConfig) {
|
||||
if (this.isAdmin && !form.blocked && !form.useCustomConfig) {
|
||||
this.$message.error("请至少启用「拉黑访问」或「个性化配置」");
|
||||
return;
|
||||
}
|
||||
if (!this.isAdmin && !form.useCustomConfig) {
|
||||
this.$message.error("请启用个性化配置");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var resp = await fetch("/__ip-rules", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
mode: this.ipRuleDialog.mode,
|
||||
ip: ip,
|
||||
remark: form.remark,
|
||||
blocked: !!form.blocked,
|
||||
@ -1025,6 +1089,10 @@
|
||||
}
|
||||
},
|
||||
removeIpRule: async function (rule) {
|
||||
if (!this.canManageIpRule(rule)) {
|
||||
this.$message.error("只能删除与当前 IP 匹配的个性化配置");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.$confirm("确定删除 IP「" + rule.ip + "」的规则吗?", "提示", {
|
||||
type: "warning",
|
||||
@ -1353,6 +1421,11 @@
|
||||
var resp = await fetch("/__config");
|
||||
var data = await resp.json();
|
||||
this.form.config = Object.assign({}, this.form.config, data.config || {});
|
||||
this.canEditBasicConfig = data.canEditBasicConfig === true;
|
||||
this.clientIp = data.clientIp || "";
|
||||
if (data.canEditBasicConfig === true) {
|
||||
this.isAdmin = true;
|
||||
}
|
||||
} catch (err) {
|
||||
this.$message.error("加载配置失败: " + err.message);
|
||||
}
|
||||
@ -1379,6 +1452,9 @@
|
||||
}
|
||||
},
|
||||
saveServerConfig: async function () {
|
||||
if (!this.canEditBasicConfig) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var resp = await fetch("/__config", {
|
||||
method: "POST",
|
||||
@ -1770,6 +1846,12 @@
|
||||
pagedIpRules: function () {
|
||||
return this.getPagedData(this.filteredIpRules, this.ipRulePagination);
|
||||
},
|
||||
canCreateIpRule: function () {
|
||||
if (this.isAdmin) return true;
|
||||
return !(this.ipRules || []).some(function (rule) {
|
||||
return rule && rule.ip;
|
||||
});
|
||||
},
|
||||
routeDialogMockFiles: function () {
|
||||
var groupFilter = this.routeDialog.form.groupFilter;
|
||||
if (groupFilter === null || groupFilter === undefined) {
|
||||
|
||||
2
admin_list.txt
Normal file
2
admin_list.txt
Normal file
@ -0,0 +1,2 @@
|
||||
192.168.3.9
|
||||
192.168.3.12
|
||||
@ -20,7 +20,8 @@ import {
|
||||
upsertIpRuleToDb,
|
||||
deleteIpRuleFromDb,
|
||||
} from "./db";
|
||||
import { isValidIpAddress, getClientIp, resolveEffectiveConfig, getMachineLanIpv4Addresses, formatTargetSummary } from "./ip-rules";
|
||||
import { isAdminClient, resolveAdminClientIp, canManageIpRule } from "./admin-list";
|
||||
import { isValidIpAddress, normalizeIp, resolveEffectiveConfig, getMachineLanIpv4Addresses, formatTargetSummary } from "./ip-rules";
|
||||
import {
|
||||
loadMockFiles,
|
||||
normalizeMockFilePath,
|
||||
@ -100,8 +101,18 @@ async function handleConfig(
|
||||
readBody(clientReq)
|
||||
.then(async (bodyText) => {
|
||||
const body = bodyText ? JSON.parse(bodyText) : {};
|
||||
// 保存服务器配置
|
||||
// 保存服务器配置(仅 admin_list.txt 白名单 IP 可修改)
|
||||
if (body.config) {
|
||||
if (!isAdminClient(clientReq)) {
|
||||
clientRes.writeHead(403, { "Content-Type": "application/json" });
|
||||
clientRes.end(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: "当前 IP 不在管理员白名单内,无法修改基础配置",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await saveConfig(body.config);
|
||||
}
|
||||
// 保存路由配置
|
||||
@ -141,6 +152,8 @@ async function handleConfig(
|
||||
JSON.stringify(
|
||||
{
|
||||
config: state.config,
|
||||
canEditBasicConfig: isAdminClient(clientReq),
|
||||
clientIp: resolveAdminClientIp(clientReq),
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
@ -628,11 +641,20 @@ async function handleIpRules(
|
||||
}
|
||||
|
||||
if (clientReq.method === "GET") {
|
||||
const clientIp = resolveAdminClientIp(clientReq);
|
||||
const isAdmin = isAdminClient(clientReq);
|
||||
const list = isAdmin
|
||||
? state.ipRules
|
||||
: state.ipRules.filter(
|
||||
(rule) => normalizeIp(rule.ip) === normalizeIp(clientIp),
|
||||
);
|
||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||
clientRes.end(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
list: state.ipRules,
|
||||
list,
|
||||
clientIp,
|
||||
isAdmin,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
@ -647,6 +669,16 @@ async function handleIpRules(
|
||||
if (!ip) {
|
||||
throw new Error("ip 不能为空");
|
||||
}
|
||||
if (!canManageIpRule(clientReq, ip)) {
|
||||
clientRes.writeHead(403, { "Content-Type": "application/json" });
|
||||
clientRes.end(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: "只能删除与当前 IP 匹配的个性化配置",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await deleteIpRuleFromDb(ip);
|
||||
state.ipRules = await loadIpRulesFromDb();
|
||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||
@ -655,6 +687,20 @@ async function handleIpRules(
|
||||
}
|
||||
|
||||
const rule = parseIpRuleBody(body);
|
||||
const isCreate = body.mode === "create";
|
||||
if (!isAdminClient(clientReq) || isCreate) {
|
||||
rule.ip = resolveAdminClientIp(clientReq);
|
||||
}
|
||||
if (!canManageIpRule(clientReq, rule.ip)) {
|
||||
clientRes.writeHead(403, { "Content-Type": "application/json" });
|
||||
clientRes.end(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: "只能修改与当前 IP 匹配的个性化配置",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await upsertIpRuleToDb(rule);
|
||||
state.ipRules = await loadIpRulesFromDb();
|
||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||
|
||||
45
src/admin-list.ts
Normal file
45
src/admin-list.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import * as fs from "fs";
|
||||
import * as http from "http";
|
||||
import { ADMIN_LIST_FILE } from "./constants";
|
||||
import { resolveClientIp, normalizeIp } from "./ip-rules";
|
||||
import { state } from "./state";
|
||||
|
||||
export function loadAdminList(): string[] {
|
||||
if (!fs.existsSync(ADMIN_LIST_FILE)) {
|
||||
return [];
|
||||
}
|
||||
const content = fs.readFileSync(ADMIN_LIST_FILE, "utf-8");
|
||||
return content
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && !line.startsWith("#"))
|
||||
.map(normalizeIp);
|
||||
}
|
||||
|
||||
/** 客户端 IP 是否在 admin_list.txt 白名单内(localhost 会映射为本机 LAN IP) */
|
||||
export function isAdminClient(req: http.IncomingMessage): boolean {
|
||||
const adminList = loadAdminList();
|
||||
if (adminList.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const whitelist = new Set(adminList.map(normalizeIp));
|
||||
const clientIp = resolveClientIp(req, adminList);
|
||||
return whitelist.has(clientIp);
|
||||
}
|
||||
|
||||
export function resolveAdminClientIp(req: http.IncomingMessage): string {
|
||||
const adminList = loadAdminList();
|
||||
const hintIps = [...adminList, ...state.ipRules.map((rule) => rule.ip)];
|
||||
return resolveClientIp(req, hintIps);
|
||||
}
|
||||
|
||||
/** 是否可管理指定 IP 的规则(管理员可操作全部,普通用户仅可操作自身 IP) */
|
||||
export function canManageIpRule(
|
||||
req: http.IncomingMessage,
|
||||
ruleIp: string,
|
||||
): boolean {
|
||||
if (isAdminClient(req)) {
|
||||
return true;
|
||||
}
|
||||
return normalizeIp(ruleIp) === normalizeIp(resolveAdminClientIp(req));
|
||||
}
|
||||
@ -5,3 +5,4 @@ export const MOCK_DIR = path.join(__dirname, "..", "mock");
|
||||
export const DATA_DIR = path.join(__dirname, "..", "data");
|
||||
export const DB_FILE = path.join(DATA_DIR, "mock-mappings.sqlite3");
|
||||
export const LEGACY_DB_FILE = path.join(MOCK_DIR, "mock-mappings.sqlite3");
|
||||
export const ADMIN_LIST_FILE = path.join(__dirname, "..", "admin_list.txt");
|
||||
|
||||
103
src/ip-rules.ts
103
src/ip-rules.ts
@ -3,7 +3,7 @@ import * as os from "os";
|
||||
import { state } from "./state";
|
||||
import type { AppConfig, IpRule, IpRuleConfig } from "./types";
|
||||
|
||||
function normalizeIp(raw: string): string {
|
||||
export function normalizeIp(raw: string): string {
|
||||
let ip = raw.trim();
|
||||
if (ip.startsWith("::ffff:")) {
|
||||
ip = ip.slice(7);
|
||||
@ -37,6 +37,14 @@ export function getClientIp(req: http.IncomingMessage): string {
|
||||
return normalizeIp(remote);
|
||||
}
|
||||
|
||||
function getIpv4Subnet24(ip: string): string | undefined {
|
||||
const parts = ip.split(".");
|
||||
if (parts.length !== 4) {
|
||||
return undefined;
|
||||
}
|
||||
return `${parts[0]}.${parts[1]}.${parts[2]}`;
|
||||
}
|
||||
|
||||
/** 本机非回环 IPv4 地址,用于 localhost 访问时的规则回退 */
|
||||
export function getMachineLanIpv4Addresses(): string[] {
|
||||
const ips = new Set<string>();
|
||||
@ -53,27 +61,80 @@ export function getMachineLanIpv4Addresses(): string[] {
|
||||
return Array.from(ips).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* localhost 访问时将 127.0.0.1 映射为本机 LAN IPv4。
|
||||
* hints 用于在多个网卡时优先选择(如白名单、IP 规则中已配置的地址)。
|
||||
*/
|
||||
export function resolveLocalhostToLanIp(hints: string[] = []): string | undefined {
|
||||
const lanIps = getMachineLanIpv4Addresses();
|
||||
if (lanIps.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (lanIps.length === 1) {
|
||||
return lanIps[0];
|
||||
}
|
||||
|
||||
const hintSet = new Set(hints.map(normalizeIp));
|
||||
if (hintSet.size > 0) {
|
||||
const matched = lanIps.filter((ip) => hintSet.has(normalizeIp(ip)));
|
||||
if (matched.length === 1) {
|
||||
return matched[0];
|
||||
}
|
||||
if (matched.length > 1) {
|
||||
return matched.sort()[0];
|
||||
}
|
||||
|
||||
const hintSubnets = new Set(
|
||||
[...hintSet]
|
||||
.map(getIpv4Subnet24)
|
||||
.filter((subnet): subnet is string => !!subnet),
|
||||
);
|
||||
if (hintSubnets.size > 0) {
|
||||
const sameSubnet = lanIps.filter((ip) => {
|
||||
const subnet = getIpv4Subnet24(ip);
|
||||
return subnet ? hintSubnets.has(subnet) : false;
|
||||
});
|
||||
if (sameSubnet.length === 1) {
|
||||
return sameSubnet[0];
|
||||
}
|
||||
if (sameSubnet.length > 1) {
|
||||
return sameSubnet.sort()[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ruleLanIps = state.ipRules
|
||||
.map((rule) => normalizeIp(rule.ip))
|
||||
.filter((ip) => lanIps.includes(ip));
|
||||
if (ruleLanIps.length === 1) {
|
||||
return ruleLanIps[0];
|
||||
}
|
||||
|
||||
const private192 = lanIps.filter((ip) => ip.startsWith("192.168."));
|
||||
if (private192.length >= 1) {
|
||||
return private192.sort()[0];
|
||||
}
|
||||
|
||||
return lanIps[0];
|
||||
}
|
||||
|
||||
/** 解析用于展示与策略匹配的客户端 IP(localhost → LAN IPv4) */
|
||||
export function resolveClientIp(
|
||||
req: http.IncomingMessage,
|
||||
hints: string[] = [],
|
||||
): string {
|
||||
const raw = normalizeIp(getClientIp(req));
|
||||
if (raw !== "127.0.0.1") {
|
||||
return raw;
|
||||
}
|
||||
return resolveLocalhostToLanIp(hints) ?? raw;
|
||||
}
|
||||
|
||||
export function findIpRule(clientIp: string): IpRule | undefined {
|
||||
const normalized = normalizeIp(clientIp);
|
||||
const direct = state.ipRules.find(
|
||||
return state.ipRules.find(
|
||||
(rule) => normalizeIp(rule.ip) === normalized,
|
||||
);
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
// 本机通过 localhost 访问时,remoteAddress 为 127.0.0.1,与 LAN IP 规则不匹配
|
||||
if (normalized === "127.0.0.1") {
|
||||
const lanIps = new Set(getMachineLanIpv4Addresses());
|
||||
const localRules = state.ipRules.filter((rule) =>
|
||||
lanIps.has(normalizeIp(rule.ip)),
|
||||
);
|
||||
if (localRules.length === 1) {
|
||||
return localRules[0];
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function mergeConfigWithIpRule(
|
||||
@ -113,10 +174,12 @@ export type EffectiveConfigResult =
|
||||
export function resolveEffectiveConfig(
|
||||
req: http.IncomingMessage,
|
||||
): EffectiveConfigResult {
|
||||
const ip = getClientIp(req);
|
||||
const rawIp = normalizeIp(getClientIp(req));
|
||||
const hintIps = state.ipRules.map((rule) => rule.ip);
|
||||
const ip = resolveClientIp(req, hintIps);
|
||||
const rule = findIpRule(ip);
|
||||
const ruleMatchedViaLocalhost =
|
||||
ip === "127.0.0.1" && !!rule && normalizeIp(rule.ip) !== ip;
|
||||
rawIp === "127.0.0.1" && !!rule && normalizeIp(rule.ip) !== rawIp;
|
||||
|
||||
if (rule?.blocked) {
|
||||
return { blocked: true, ip, matchedRule: rule };
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user