add diaposine
This commit is contained in:
+2
-2
@@ -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
|
||||||
|
|||||||
Generated
+47
-15
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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))
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user