add diaposine

This commit is contained in:
Kirill
2026-06-02 09:48:39 +05:00
parent 29d240424b
commit 6c341045b8
5 changed files with 165 additions and 18 deletions
+2 -2
View File
@@ -17,8 +17,8 @@ JWT_SECRET=замените-на-секрет-jwt
# Разрешённый Origin фронта (через запятую при нескольких) # Разрешённый Origin фронта (через запятую при нескольких)
# CORS_ORIGIN=http://127.0.0.1:5173 # CORS_ORIGIN=http://127.0.0.1:5173
# Ограничение доступа по IP на время разработки (через запятую). Не задано — защита отключена. # Ограничение доступа по IP на время разработки (через запятую). Поддерживает точные IP и CIDR-диапазоны. Не задано — защита отключена.
# SITE_ACCESS_IPS=1.2.3.4,5.6.7.8 # SITE_ACCESS_IPS=1.2.3.4,5.6.7.8,192.168.1.0/24
# Публичные URL для OAuth redirect (локально обычно так): # Публичные URL для OAuth redirect (локально обычно так):
SERVER_PUBLIC_URL=http://127.0.0.1:3333 SERVER_PUBLIC_URL=http://127.0.0.1:3333
+47 -15
View File
@@ -15,6 +15,7 @@
"@fastify/multipart": "^10.0.0", "@fastify/multipart": "^10.0.0",
"@fastify/static": "^9.1.3", "@fastify/static": "^9.1.3",
"@prisma/client": "5.22.0", "@prisma/client": "5.22.0",
"@rollup/rollup-linux-x64-gnu": "^4.61.0",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"fastify": "^5.8.5", "fastify": "^5.8.5",
@@ -191,7 +192,6 @@
"resolved": "https://registry.npmjs.org/@dicebear/core/-/core-9.4.2.tgz", "resolved": "https://registry.npmjs.org/@dicebear/core/-/core-9.4.2.tgz",
"integrity": "sha512-MF0042+Z3s8PGZKZLySfhft28bUa3B1iq0e5NSjCvY8gfMi5aIH/iRJGRJa1N9Jz1BNkxYb4yvJ/N9KO8Z6Y+w==", "integrity": "sha512-MF0042+Z3s8PGZKZLySfhft28bUa3B1iq0e5NSjCvY8gfMi5aIH/iRJGRJa1N9Jz1BNkxYb4yvJ/N9KO8Z6Y+w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/json-schema": "^7.0.15" "@types/json-schema": "^7.0.15"
}, },
@@ -463,6 +463,29 @@
"@dicebear/core": "^9.0.0" "@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": { "node_modules/@emnapi/wasi-threads": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "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": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.60.4", "version": "4.61.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.0.tgz",
"integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", "integrity": "sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true,
"os": [ "os": [
"linux" "linux"
] ]
@@ -2376,7 +2400,6 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -3027,7 +3050,6 @@
"integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2", "@eslint-community/regexpp": "^4.12.2",
@@ -3084,7 +3106,6 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"eslint-config-prettier": "bin/cli.js" "eslint-config-prettier": "bin/cli.js"
}, },
@@ -3632,7 +3653,6 @@
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -4286,7 +4306,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -4431,7 +4450,6 @@
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@@ -4462,7 +4480,6 @@
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@prisma/engines": "5.22.0" "@prisma/engines": "5.22.0"
}, },
@@ -4645,6 +4662,23 @@
"fsevents": "~2.3.2" "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": { "node_modules/rollup/node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -5175,7 +5209,6 @@
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"napi-postinstall": "^0.3.4" "napi-postinstall": "^0.3.4"
}, },
@@ -5229,7 +5262,6 @@
"integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
+1
View File
@@ -25,6 +25,7 @@
"@fastify/multipart": "^10.0.0", "@fastify/multipart": "^10.0.0",
"@fastify/static": "^9.1.3", "@fastify/static": "^9.1.3",
"@prisma/client": "5.22.0", "@prisma/client": "5.22.0",
"@rollup/rollup-linux-x64-gnu": "^4.61.0",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"fastify": "^5.8.5", "fastify": "^5.8.5",
@@ -172,4 +172,88 @@ describe('registerIpGate', () => {
}) })
expect(res.statusCode).toBe(200) 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)
})
}) })
+31 -1
View File
@@ -12,6 +12,32 @@ function normalizeIp(ip) {
return 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) { export function build403Html(ip) {
const safeIp = ip || 'не определён' const safeIp = ip || 'не определён'
return `<!DOCTYPE html> return `<!DOCTYPE html>
@@ -95,7 +121,11 @@ export async function registerIpGate(fastify) {
if (EXCLUDED_PATHS.includes(urlPath)) return 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)) return reply.code(403).type('text/html').send(build403Html(request.ip))
}) })