add diaposine
This commit is contained in:
+2
-2
@@ -17,8 +17,8 @@ JWT_SECRET=замените-на-секрет-jwt
|
||||
# Разрешённый Origin фронта (через запятую при нескольких)
|
||||
# CORS_ORIGIN=http://127.0.0.1:5173
|
||||
|
||||
# Ограничение доступа по IP на время разработки (через запятую). Не задано — защита отключена.
|
||||
# SITE_ACCESS_IPS=1.2.3.4,5.6.7.8
|
||||
# Ограничение доступа по IP на время разработки (через запятую). Поддерживает точные IP и CIDR-диапазоны. Не задано — защита отключена.
|
||||
# SITE_ACCESS_IPS=1.2.3.4,5.6.7.8,192.168.1.0/24
|
||||
|
||||
# Публичные URL для OAuth redirect (локально обычно так):
|
||||
SERVER_PUBLIC_URL=http://127.0.0.1:3333
|
||||
|
||||
Generated
+47
-15
@@ -15,6 +15,7 @@
|
||||
"@fastify/multipart": "^10.0.0",
|
||||
"@fastify/static": "^9.1.3",
|
||||
"@prisma/client": "5.22.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.61.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"fastify": "^5.8.5",
|
||||
@@ -191,7 +192,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/core/-/core-9.4.2.tgz",
|
||||
"integrity": "sha512-MF0042+Z3s8PGZKZLySfhft28bUa3B1iq0e5NSjCvY8gfMi5aIH/iRJGRJa1N9Jz1BNkxYb4yvJ/N9KO8Z6Y+w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
},
|
||||
@@ -463,6 +463,29 @@
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
@@ -1762,15 +1785,16 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
|
||||
"integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
|
||||
"version": "4.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.0.tgz",
|
||||
"integrity": "sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
@@ -2376,7 +2400,6 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3027,7 +3050,6 @@
|
||||
"integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
@@ -3084,7 +3106,6 @@
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
@@ -3632,7 +3653,6 @@
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -4286,7 +4306,6 @@
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4431,7 +4450,6 @@
|
||||
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -4462,7 +4480,6 @@
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@prisma/engines": "5.22.0"
|
||||
},
|
||||
@@ -4645,6 +4662,23 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
|
||||
"integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/rollup/node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -5175,7 +5209,6 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"napi-postinstall": "^0.3.4"
|
||||
},
|
||||
@@ -5229,7 +5262,6 @@
|
||||
"integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"@fastify/multipart": "^10.0.0",
|
||||
"@fastify/static": "^9.1.3",
|
||||
"@prisma/client": "5.22.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.61.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"fastify": "^5.8.5",
|
||||
|
||||
@@ -172,4 +172,88 @@ describe('registerIpGate', () => {
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
})
|
||||
|
||||
it('пропускает запрос с IP в CIDR-диапазоне /24', async () => {
|
||||
process.env.SITE_ACCESS_IPS = '192.168.1.0/24'
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/test',
|
||||
remoteAddress: '192.168.1.100',
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
})
|
||||
|
||||
it('блокирует запрос с IP вне CIDR-диапазона', async () => {
|
||||
process.env.SITE_ACCESS_IPS = '192.168.1.0/24'
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/test',
|
||||
remoteAddress: '10.0.0.1',
|
||||
})
|
||||
expect(res.statusCode).toBe(403)
|
||||
})
|
||||
|
||||
it('пропускает IP в CIDR /32 (эквивалент одного IP)', async () => {
|
||||
process.env.SITE_ACCESS_IPS = '10.0.0.5/32'
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/test',
|
||||
remoteAddress: '10.0.0.5',
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
})
|
||||
|
||||
it('блокирует IP рядом с CIDR /32', async () => {
|
||||
process.env.SITE_ACCESS_IPS = '10.0.0.5/32'
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/test',
|
||||
remoteAddress: '10.0.0.6',
|
||||
})
|
||||
expect(res.statusCode).toBe(403)
|
||||
})
|
||||
|
||||
it('пропускает любой IP в CIDR /0', async () => {
|
||||
process.env.SITE_ACCESS_IPS = '0.0.0.0/0'
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/test',
|
||||
remoteAddress: '1.2.3.4',
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
})
|
||||
|
||||
it('поддерживает микс точных IP и CIDR-диапазонов', async () => {
|
||||
process.env.SITE_ACCESS_IPS = '1.2.3.4,10.0.0.0/24'
|
||||
const res1 = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/test',
|
||||
remoteAddress: '1.2.3.4',
|
||||
})
|
||||
expect(res1.statusCode).toBe(200)
|
||||
|
||||
const res2 = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/test',
|
||||
remoteAddress: '10.0.0.50',
|
||||
})
|
||||
expect(res2.statusCode).toBe(200)
|
||||
|
||||
const res3 = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/test',
|
||||
remoteAddress: '9.9.9.9',
|
||||
})
|
||||
expect(res3.statusCode).toBe(403)
|
||||
})
|
||||
|
||||
it('IPv6-mapped адрес в CIDR-диапазоне', async () => {
|
||||
process.env.SITE_ACCESS_IPS = '192.168.1.0/24'
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/test',
|
||||
remoteAddress: '::ffff:192.168.1.50',
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,6 +12,32 @@ function normalizeIp(ip) {
|
||||
return ip
|
||||
}
|
||||
|
||||
function ipToInt(ip) {
|
||||
const parts = ip.split('.')
|
||||
if (parts.length !== 4) return null
|
||||
return parts.reduce((acc, octet) => {
|
||||
const num = parseInt(octet, 10)
|
||||
if (isNaN(num) || num < 0 || num > 255) return null
|
||||
return acc !== null ? (acc << 8) + num : null
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function cidrMatch(ip, cidr) {
|
||||
const slashIdx = cidr.indexOf('/')
|
||||
if (slashIdx === -1) return false
|
||||
|
||||
const baseIp = cidr.slice(0, slashIdx)
|
||||
const prefix = parseInt(cidr.slice(slashIdx + 1), 10)
|
||||
if (isNaN(prefix) || prefix < 0 || prefix > 32) return false
|
||||
|
||||
const ipInt = ipToInt(normalizeIp(ip))
|
||||
const baseInt = ipToInt(normalizeIp(baseIp))
|
||||
if (ipInt === null || baseInt === null) return false
|
||||
|
||||
const mask = prefix === 0 ? 0 : ~(2 ** (32 - prefix) - 1) >>> 0
|
||||
return (ipInt & mask) === (baseInt & mask)
|
||||
}
|
||||
|
||||
export function build403Html(ip) {
|
||||
const safeIp = ip || 'не определён'
|
||||
return `<!DOCTYPE html>
|
||||
@@ -95,7 +121,11 @@ export async function registerIpGate(fastify) {
|
||||
|
||||
if (EXCLUDED_PATHS.includes(urlPath)) return
|
||||
|
||||
if (allowedIps.includes(normalizeIp(request.ip))) return
|
||||
const normalizedIp = normalizeIp(request.ip)
|
||||
if (allowedIps.includes(normalizedIp)) return
|
||||
|
||||
const isInCidr = allowedIps.some((entry) => cidrMatch(normalizedIp, entry))
|
||||
if (isInCidr) return
|
||||
|
||||
return reply.code(403).type('text/html').send(build403Html(request.ip))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user