Commit 6e875b88 authored by nanahira's avatar nanahira

add ALLOWED_NETWORK

parent 79a6c84e
Pipeline #37645 passed with stages
in 2 minutes and 34 seconds
...@@ -7,3 +7,4 @@ S3_ACCESS_KEY_ID: '' ...@@ -7,3 +7,4 @@ S3_ACCESS_KEY_ID: ''
S3_SECRET_ACCESS_KEY: '' S3_SECRET_ACCESS_KEY: ''
S3_PATH_STYLE: '' S3_PATH_STYLE: ''
HTTP_TIMEOUT: 30000 HTTP_TIMEOUT: 30000
ALLOWED_NETWORK: ''
\ No newline at end of file
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
"@nestjs/swagger": "^11.2.0", "@nestjs/swagger": "^11.2.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"ipaddr.js": "^2.2.0",
"nesties": "^1.1.2", "nesties": "^1.1.2",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
...@@ -9149,12 +9150,12 @@ ...@@ -9149,12 +9150,12 @@
} }
}, },
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.10" "node": ">= 10"
} }
}, },
"node_modules/is-arrayish": { "node_modules/is-arrayish": {
...@@ -11312,6 +11313,15 @@ ...@@ -11312,6 +11313,15 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-addr/node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
......
...@@ -10,10 +10,11 @@ import { ...@@ -10,10 +10,11 @@ import {
PutObjectCommand, PutObjectCommand,
S3Client, S3Client,
} from '@aws-sdk/client-s3'; } from '@aws-sdk/client-s3';
import { BlankReturnMessageDto } from 'nesties'; import { BlankReturnMessageDto, GenericReturnMessageDto } from 'nesties';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { teex } from './utility/tee-stream'; import { teex } from './utility/tee-stream';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import ipaddr from 'ipaddr.js';
@Injectable() @Injectable()
export class AppService extends ConsoleLogger { export class AppService extends ConsoleLogger {
...@@ -27,6 +28,20 @@ export class AppService extends ConsoleLogger { ...@@ -27,6 +28,20 @@ export class AppService extends ConsoleLogger {
bucket = this.config.get<string>('S3_BUCKET'); bucket = this.config.get<string>('S3_BUCKET');
endpoint = this.config.get<string>('S3_ENDPOINT'); endpoint = this.config.get<string>('S3_ENDPOINT');
pathPrefix = this.config.get<string>('S3_PATH_PREFIX') || ''; pathPrefix = this.config.get<string>('S3_PATH_PREFIX') || '';
allowedNetwork = this.config.get<string>('ALLOWED_NETWORK') || '';
allowedNetworks: [ipaddr.IPv4 | ipaddr.IPv6, number][] =
this.allowedNetwork === ''
? []
: this.allowedNetwork.split(',').map((_n) => {
const n = _n.trim();
if (n.includes('/')) {
return ipaddr.parseCIDR(n);
} else {
const parsed = ipaddr.parse(n);
const prefixLength = parsed.kind() === 'ipv6' ? 128 : 32;
return [parsed, prefixLength];
}
});
s3 = new S3Client({ s3 = new S3Client({
credentials: { credentials: {
...@@ -41,13 +56,36 @@ export class AppService extends ConsoleLogger { ...@@ -41,13 +56,36 @@ export class AppService extends ConsoleLogger {
const url = req.originalUrl.startsWith('/') const url = req.originalUrl.startsWith('/')
? req.originalUrl.slice(1) ? req.originalUrl.slice(1)
: req.originalUrl; : req.originalUrl;
const clientIp = req.ip.startsWith('::ffff:') ? req.ip.slice(7) : req.ip;
if (
this.allowedNetworks.length &&
!this.allowedNetworks.some((network) => {
const [addr, prefixLength] = network;
const ip = ipaddr.parse(clientIp);
return ip.kind() === addr.kind() && ip.match(addr, prefixLength);
})
) {
this.log(
`Client ${clientIp} is not allowed to access this service, redirecting to upstream ${url}`,
);
res.header('location', url);
throw new BlankReturnMessageDto(
301,
'Redirecting to upstream',
).toException();
}
const urlObj = new URL(url); const urlObj = new URL(url);
const filename = urlObj.pathname.split('/').pop(); const filename = urlObj.pathname.split('/').pop();
if (!filename) { if (!filename) {
throw new BlankReturnMessageDto(400, 'Bad filename').toException(); throw new BlankReturnMessageDto(400, 'Bad filename').toException();
} }
this.log(`Client ${req.ip} requested file ${filename} from ${url}`); this.log(
`Client ${clientIp} (${req.headers['user-agent'] || 'Unknown'}) requested file ${filename} from ${url}`,
);
const s3Key = `${this.pathPrefix}${filename}`; const s3Key = `${this.pathPrefix}${filename}`;
const runExisting = async () => { const runExisting = async () => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment