diff --git a/lib/config.ts b/lib/config.ts index c9914a2539e4b8..fb14dec4165a61 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -43,9 +43,6 @@ export type Config = { proxyStrategy: string; pacUri?: string; pacScript?: string; - denylist?: string[]; - allowlist?: string[]; - allowLocalhost: boolean; accessKey?: string; debugInfo: string; loggerLevel: string; @@ -373,9 +370,6 @@ const calculateValue = () => { pacUri: envs.PAC_URI, pacScript: envs.PAC_SCRIPT, // access control - denylist: envs.DENYLIST ? envs.DENYLIST.split(',') : undefined, - allowlist: envs.ALLOWLIST ? envs.ALLOWLIST.split(',') : undefined, - allowLocalhost: toBoolean(envs.ALLOW_LOCALHOST, false), accessKey: envs.ACCESS_KEY, // logging // 是否显示 Debug 信息,取值 'true' 'false' 'some_string' ,取值为 'true' 时永久显示,取值为 'false' 时永远隐藏,取值为 'some_string' 时请求带上 '?debug=some_string' 显示 diff --git a/lib/middleware/access-control.test.ts b/lib/middleware/access-control.test.ts index 639895a0ea769a..d4f51576ab3f11 100644 --- a/lib/middleware/access-control.test.ts +++ b/lib/middleware/access-control.test.ts @@ -10,190 +10,11 @@ async function checkBlock(response) { afterEach(() => { delete process.env.ACCESS_KEY; - delete process.env.DENYLIST; - delete process.env.ALLOWLIST; jest.resetModules(); }); describe('access-control', () => { - it(`denylist`, async () => { - const key = '1L0veRSSHub'; - const code = md5('/test/2' + key); - process.env.DENYLIST = 'est/1,233.233.233.,black'; - process.env.ACCESS_KEY = key; - const app = (await import('@/app')).default; - - const response11 = await app.request('/test/1'); - await checkBlock(response11); - - const response12 = await app.request('/test/1', { - headers: { - 'X-Mock-IP': '233.233.233.233', - }, - }); - await checkBlock(response12); - - const response13 = await app.request('/test/1', { - headers: { - 'user-agent': 'blackua', - }, - }); - await checkBlock(response13); - - const response21 = await app.request('/test/2'); - expect(response21.status).toBe(200); - - const response22 = await app.request('/test/2', { - headers: { - 'X-Mock-IP': '233.233.233.233', - }, - }); - await checkBlock(response22); - - const response23 = await app.request('/test/2', { - headers: { - 'user-agent': 'blackua', - }, - }); - await checkBlock(response23); - - // wrong key/code, not on denylist - const response311 = await app.request(`/test/2?key=wrong+${key}`); - expect(response311.status).toBe(200); - - const response312 = await app.request(`/test/2?code=wrong+${code}`); - expect(response312.status).toBe(200); - - // wrong key/code, on denylist - const response321 = await app.request(`/test/2?key=wrong+${key}`, { - headers: { - 'X-Mock-IP': '233.233.233.233', - }, - }); - await checkBlock(response321); - - const response322 = await app.request(`/test/2?code=wrong+${code}`, { - headers: { - 'X-Mock-IP': '233.233.233.233', - }, - }); - await checkBlock(response322); - - // right key/code, on denylist - const response331 = await app.request(`/test/2?key=${key}`, { - headers: { - 'X-Mock-IP': '233.233.233.233', - }, - }); - expect(response331.status).toBe(200); - - const response332 = await app.request(`/test/2?code=${code}`, { - headers: { - 'X-Mock-IP': '233.233.233.233', - }, - }); - expect(response332.status).toBe(200); - }); - - it(`allowlist`, async () => { - const key = '1L0veRSSHub'; - const code = md5('/test/2' + key); - process.env.ALLOWLIST = 'est/1,233.233.233.,103.31.4.0/22,white'; - process.env.ACCESS_KEY = key; - const app = (await import('@/app')).default; - - const response01 = await app.request('/'); - expect(response01.status).toBe(200); - - const response02 = await app.request('/robots.txt'); - expect(response02.status).toBe(404); - - const response11 = await app.request('/test/1'); - expect(response11.status).toBe(200); - - const response12 = await app.request('/test/1', { - headers: { - 'X-Mock-IP': '233.233.233.233', - }, - }); - expect(response12.status).toBe(200); - - const response13 = await app.request('/test/1', { - headers: { - 'user-agent': 'whiteua', - }, - }); - expect(response13.status).toBe(200); - - const response21 = await app.request('/test/2'); - await checkBlock(response21); - - const response22 = await app.request('/test/2', { - headers: { - 'X-Mock-IP': '233.233.233.233', - }, - }); - expect(response22.status).toBe(200); - - const response221 = await app.request('/test/2', { - headers: { - 'X-Mock-IP': '103.31.4.0', - }, - }); - expect(response221.status).toBe(200); - - const response222 = await app.request('/test/2', { - headers: { - 'X-Mock-IP': '103.31.7.255', - }, - }); - expect(response222.status).toBe(200); - - const response223 = await app.request('/test/2', { - headers: { - 'X-Mock-IP': '103.31.8.0', - }, - }); - await checkBlock(response223); - - const response23 = await app.request('/test/2', { - headers: { - 'user-agent': 'whiteua', - }, - }); - expect(response23.status).toBe(200); - - // wrong key/code, not on allowlist - const response311 = await app.request(`/test/2?code=wrong+${code}`); - await checkBlock(response311); - - const response312 = await app.request(`/test/2?key=wrong+${key}`); - await checkBlock(response312); - - // wrong key/code, on allowlist - const response321 = await app.request(`/test/2?code=wrong+${code}`, { - headers: { - 'X-Mock-IP': '233.233.233.233', - }, - }); - expect(response321.status).toBe(200); - - const response322 = await app.request(`/test/2?key=wrong+${key}`, { - headers: { - 'X-Mock-IP': '233.233.233.233', - }, - }); - expect(response322.status).toBe(200); - - // right key/code - const response331 = await app.request(`/test/2?code=${code}`); - expect(response331.status).toBe(200); - - const response332 = await app.request(`/test/2?key=${key}`); - expect(response332.status).toBe(200); - }); - - it(`no list`, async () => { + it(`access key`, async () => { const key = '1L0veRSSHub'; const code = md5('/test/2' + key); process.env.ACCESS_KEY = key; @@ -205,9 +26,7 @@ describe('access-control', () => { const response02 = await app.request('/robots.txt'); expect(response02.status).toBe(404); - const response11 = await app.request('/test/1'); - await checkBlock(response11); - + // no key/code const response21 = await app.request('/test/2'); await checkBlock(response21); diff --git a/lib/middleware/access-control.ts b/lib/middleware/access-control.ts index 11e4b92f5323c1..830c75b606670c 100644 --- a/lib/middleware/access-control.ts +++ b/lib/middleware/access-control.ts @@ -1,72 +1,24 @@ import type { MiddlewareHandler } from 'hono'; import { config } from '@/config'; import md5 from '@/utils/md5'; -import ipUtils from 'ip'; -import { getIp } from '@/utils/helpers'; import RejectError from '@/errors/reject'; const reject = () => { throw new RejectError('Authentication failed. Access denied.'); }; -const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/; -const cidrPattern = /((?:\d{1,3}\.){3}\d{1,3})\/(\d{1,2})/; - -const ipInCidr = (cidr: string, ip: string) => { - const cidrMatch = cidr.match(cidrPattern); - const ipMatch = ip.match(ipv4Pattern); - if (!cidrMatch || !ipMatch) { - return false; - } - const subnetMask = Number.parseInt(cidrMatch[2]); - const cidrIpBits = ipv4ToBitsring(cidrMatch[1]).substring(0, subnetMask); - const ipBits = ipv4ToBitsring(ip).substring(0, subnetMask); - return cidrIpBits === ipBits; -}; - -const ipv4ToBitsring = (ip: string) => - ip - .split('.') - .map((part) => ('00000000' + Number.parseInt(part).toString(2)).slice(-8)) - .join(''); - const middleware: MiddlewareHandler = async (ctx, next) => { - const ip = getIp(ctx); const requestPath = ctx.req.path; - const requestUA = ctx.req.header('user-agent'); const accessKey = ctx.req.query('key'); const accessCode = ctx.req.query('code'); - const isControlled = config.accessKey || config.allowlist || config.denylist; - - const allowLocalhost = config.allowLocalhost && ip && ipUtils.isPrivate(ip); - - const grant = async () => { - if (ctx.res.status !== 403) { - await next(); - } - }; - if (requestPath === '/' || requestPath === '/robots.txt') { await next(); } else { - if (!isControlled || allowLocalhost) { - return grant(); + if (config.accessKey && !(config.accessKey === accessKey || accessCode === md5(requestPath + config.accessKey))) { + return reject(); } - - if (config.accessKey && (config.accessKey === accessKey || accessCode === md5(requestPath + config.accessKey))) { - return grant(); - } - - if (config.allowlist && config.allowlist.some((item) => ip?.includes(item) || (ip && ipInCidr(item, ip)) || requestPath.includes(item) || requestUA?.includes(item))) { - return grant(); - } - - if (config.denylist && !config.denylist.some((item) => ip?.includes(item) || (ip && ipInCidr(item, ip)) || requestPath.includes(item) || requestUA?.includes(item))) { - return grant(); - } - - reject(); + await next(); } }; diff --git a/lib/middleware/logger.ts b/lib/middleware/logger.ts index 16431944d7285f..f806e7ee06170e 100644 --- a/lib/middleware/logger.ts +++ b/lib/middleware/logger.ts @@ -1,6 +1,6 @@ import { MiddlewareHandler } from 'hono'; import logger from '@/utils/logger'; -import { getIp, getPath, time } from '@/utils/helpers'; +import { getPath, time } from '@/utils/helpers'; enum LogPrefix { Outgoing = '-->', @@ -28,7 +28,7 @@ const middleware: MiddlewareHandler = async (ctx, next) => { const { method, raw } = ctx.req; const path = getPath(raw); - logger.info(`${LogPrefix.Incoming} ${method} ${path} from ${getIp(ctx)}`); + logger.info(`${LogPrefix.Incoming} ${method} ${path}`); const start = Date.now(); diff --git a/lib/utils/helpers.ts b/lib/utils/helpers.ts index 285e094ea7fea0..8368cca1d7a075 100644 --- a/lib/utils/helpers.ts +++ b/lib/utils/helpers.ts @@ -1,9 +1,3 @@ -import { Context } from 'hono'; -import { Ipware } from '@fullerstack/nax-ipware'; -import { config } from '@/config'; - -const ipware = new Ipware(); - export const getRouteNameFromPath = (path: string) => { const p = path.split('/').filter(Boolean); if (p.length > 0) { @@ -12,8 +6,6 @@ export const getRouteNameFromPath = (path: string) => { return null; }; -export const getIp = (ctx: Context) => (config.nodeName === 'mock' && ctx.req.header('X-Mock-IP') ? ctx.req.header('X-Mock-IP') : ipware.getClientIP(ctx.req.raw)?.ip); - export const getPath = (request: Request): string => { // Optimized: RegExp is faster than indexOf() + slice() const match = request.url.match(/^https?:\/\/[^/]+(\/[^?]*)/); diff --git a/package.json b/package.json index e8ab13d1958bf1..728680545d796c 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "outputDirectory": "coverage" }, "dependencies": { - "@fullerstack/nax-ipware": "0.10.0", "@hono/node-server": "1.8.2", "@koa/router": "12.0.1", "@notionhq/client": "2.2.14", @@ -108,7 +107,6 @@ "imapflow": "1.0.153", "instagram-private-api": "1.45.3", "ioredis": "5.3.2", - "ip": "2.0.1", "ip-regex": "5.0.0", "jsdom": "24.0.0", "json-bigint": "1.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0083ad85cbe45c..79231ca146de4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,9 +5,6 @@ settings: excludeLinksFromLockfile: false dependencies: - '@fullerstack/nax-ipware': - specifier: 0.10.0 - version: 0.10.0(typescript@5.3.3) '@hono/node-server': specifier: 1.8.2 version: 1.8.2 @@ -101,9 +98,6 @@ dependencies: ioredis: specifier: 5.3.2 version: 5.3.2 - ip: - specifier: 2.0.1 - version: 2.0.1 ip-regex: specifier: 5.0.0 version: 5.0.0 @@ -2189,16 +2183,6 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@fullerstack/nax-ipware@0.10.0(typescript@5.3.3): - resolution: {integrity: sha512-A2g9pqeRprTcaK9zQk4fQVKKoH0YFTVTFJkJNPuq6UIs2nWnaWDeoK8xWIQHJF13k9Tz84iDQlovs7YDw/iZDQ==} - dependencies: - lodash: 4.17.21 - ts-essentials: 7.0.3(typescript@5.3.3) - tslib: 2.6.2 - transitivePeerDependencies: - - typescript - dev: false - /@hono/node-server@1.8.2: resolution: {integrity: sha512-h8l2TBLCPHZBUrrkosZ6L5CpBLj6zdESyF4B+zngiCDF7aZFQJ0alVbLx7jn8PCVi9EyoFf8a4hOZFi1tD95EA==} engines: {node: '>=18.14.1'} @@ -6584,10 +6568,6 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: false - /ip@2.0.1: - resolution: {integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==} - dev: false - /is-alphabetical@1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} dev: true @@ -11029,14 +11009,6 @@ packages: engines: {node: '>=14.0.0'} dev: false - /ts-essentials@7.0.3(typescript@5.3.3): - resolution: {integrity: sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==} - peerDependencies: - typescript: '>=3.7.0' - dependencies: - typescript: 5.3.3 - dev: false - /ts-jest@29.1.2(@babel/core@7.24.0)(jest@29.7.0)(typescript@5.3.3): resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==} engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0} diff --git a/website/docs/install/config.md b/website/docs/install/config.md index aa620013052711..f1536442bb7c07 100644 --- a/website/docs/install/config.md +++ b/website/docs/install/config.md @@ -124,15 +124,11 @@ About PAC script, please refer to [Proxy Auto-Configuration (PAC) file](https:// ## Access Control Configurations -RSSHub supports access control via access key/code, allowlisting and denylisting, enabling any will activate access control for all routes. `ALLOW_LOCALHOST: true` will grant access to all localhost IP addresses. +RSSHub supports access control using access keys/codes. Enabling it will activate global access control, and lack of access permission will result in denied access. ### Allowlisting/denylisting -- `ALLOWLIST`: the allowlist. When set, values in `DENYLIST` are disregarded - -- `DENYLIST`: the denylist - -Allowlisting/denylisting support IP, route and UA as values, fuzzy matching. Use `,` as the delimiter to separate multiple values, eg: `ALLOWLIST=1.1.1.1,2.2.2.2,/qdaily/column/59` +This configuration has been removed. It is recommended to use a proxy server such as Nginx or Cloudflare for access control. ### Access Key/Code @@ -148,13 +144,6 @@ Access code is the md5 generated based on the access key + route, eg: - Or using `key` directly, eg: `https://rsshub.app/qdaily/column/59?key=ILoveRSSHub` -See the relation between access key/code and allowlist/denylisting. - -| | Allowlist | Denylist | Correct access key/code | Wrong access key/code | No access key/code | -| ----------- | ----------- | ----------- | ----------------------- | --------------------- | ------------------ | -| Allowlist | ✅ | ✅ | ✅ | ✅ | ✅ | -| Denylist | ✅ | ❌ | ✅ | ❌ | ❌ | - ## Logging Configurations `DEBUG_INFO`: display route information on the homepage for debugging purposes. When set to neither `true` nor `false`, use parameter `debug` to enable display, eg: `https://rsshub.app/?debug=value_of_DEBUG_INFO` . Default to `true` diff --git a/website/i18n/zh/docusaurus-plugin-content-docs/current/install/config.md b/website/i18n/zh/docusaurus-plugin-content-docs/current/install/config.md index 0a3647c4f9398b..a1e429473c702d 100644 --- a/website/i18n/zh/docusaurus-plugin-content-docs/current/install/config.md +++ b/website/i18n/zh/docusaurus-plugin-content-docs/current/install/config.md @@ -123,15 +123,10 @@ async function handleRequest(request) { ## 访问控制配置 -RSSHub 支持使用访问密钥 / 码,允许清单和拒绝清单三种方式进行访问控制。开启任意选项将会激活全局访问控制,没有访问权限将会导致访问被拒绝。同时可以通过 `ALLOW_LOCALHOST: true` 赋予所有本地 IP 访问权限。 - +RSSHub 支持使用访问密钥 / 码进行访问控制。开启将会激活全局访问控制,没有访问权限将会导致访问被拒绝。 ### 允许清单/拒绝清单 -- `ALLOWLIST`: 允许清单,设置允许清单后拒绝清单无效 - -- `DENYLIST`: 拒绝清单 - -允许清单/拒绝清单支持 IP、路由和 UA,模糊匹配,设置多项时用英文逗号 `,` 隔开,例如 `ALLOWLIST=1.1.1.1,2.2.2.2,/qdaily/column/59` +此配置已被移除,建议使用类似 Nginx 或 Cloudflare 的代理服务器进行访问控制。 ### 访问密钥 / 码 @@ -147,14 +142,6 @@ RSSHub 支持使用访问密钥 / 码,允许清单和拒绝清单三种方式 - 或使用访问密钥 `key` 直接访问所有路由,例如:`https://rsshub.app/qdaily/column/59?key=ILoveRSSHub` -访问密钥 / 码与允许清单/拒绝清单的访问控制关系如下: - -| | 正确访问密钥 / 码 | 错误访问密钥 / 码 | 无访问密钥 / 码 | -| ---------- | ----------------- | ----------------- | --------------- | -| 在允许清单中 | ✅ | ✅ | ✅ | -| 在拒绝清单中 | ✅ | ❌ | ❌ | -| 无允许清单/拒绝清单 | ✅ | ❌ | ❌ | - ## 日志配置 `DEBUG_INFO`: 是否在首页显示路由信息。值为非 `true` `false` 时,在请求中带上参数 `debug` 开启显示,例如:`https://rsshub.app/?debug=value_of_DEBUG_INFO` 。默认 `true`