mirror of
https://codeberg.org/yeentown/barkey
synced 2024-11-27 08:03:01 +00:00
Compare commits
17 commits
0954b098a4
...
3d8c3aa081
Author | SHA1 | Date | |
---|---|---|---|
|
3d8c3aa081 | ||
|
b47ebf162f | ||
8ac6e62184 | |||
0670359c4f | |||
73ae0cf039 | |||
3054cd4936 | |||
783cf3ed4a | |||
bccd6b2dd8 | |||
f9dbe135e9 | |||
390d99e531 | |||
|
da42cd8a4d | ||
196cb6bb8b | |||
|
9e809aa3b6 | ||
|
27339e03c2 | ||
|
680c2a0718 | ||
|
f258888408 | ||
|
d150e92f41 |
2 changed files with 90 additions and 1 deletions
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "sharkey",
|
"name": "sharkey",
|
||||||
"version": "2024.9.1",
|
"version": "2024.9.2",
|
||||||
"codename": "shonk",
|
"codename": "shonk",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -28,7 +28,11 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||||
import { correctFilename } from '@/misc/correct-filename.js';
|
import { correctFilename } from '@/misc/correct-filename.js';
|
||||||
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
|
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
|
||||||
|
import { RateLimiterService } from '@/server/api/RateLimiterService.js';
|
||||||
|
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||||
|
import { AuthenticateService } from '@/server/api/AuthenticateService.js';
|
||||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||||
|
import type Limiter from 'ratelimiter';
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
const _dirname = dirname(_filename);
|
const _dirname = dirname(_filename);
|
||||||
|
@ -52,6 +56,8 @@ export class FileServerService {
|
||||||
private videoProcessingService: VideoProcessingService,
|
private videoProcessingService: VideoProcessingService,
|
||||||
private internalStorageService: InternalStorageService,
|
private internalStorageService: InternalStorageService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
|
private authenticateService: AuthenticateService,
|
||||||
|
private rateLimiterService: RateLimiterService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('server', 'gray');
|
this.logger = this.loggerService.getLogger('server', 'gray');
|
||||||
|
|
||||||
|
@ -76,6 +82,8 @@ export class FileServerService {
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => {
|
fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => {
|
||||||
|
if (!await this.checkRateLimit(request, reply, `/files/${request.params.key}`)) return;
|
||||||
|
|
||||||
return await this.sendDriveFile(request, reply)
|
return await this.sendDriveFile(request, reply)
|
||||||
.catch(err => this.errorHandler(request, reply, err));
|
.catch(err => this.errorHandler(request, reply, err));
|
||||||
});
|
});
|
||||||
|
@ -89,6 +97,20 @@ export class FileServerService {
|
||||||
Params: { url: string; };
|
Params: { url: string; };
|
||||||
Querystring: { url?: string; };
|
Querystring: { url?: string; };
|
||||||
}>('/proxy/:url*', async (request, reply) => {
|
}>('/proxy/:url*', async (request, reply) => {
|
||||||
|
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
|
||||||
|
if (!url || !URL.canParse(url)) {
|
||||||
|
reply.code(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyUrl = new URL(url);
|
||||||
|
keyUrl.searchParams.forEach(k => keyUrl.searchParams.delete(k));
|
||||||
|
keyUrl.hash = '';
|
||||||
|
keyUrl.username = '';
|
||||||
|
keyUrl.password = '';
|
||||||
|
|
||||||
|
if (!await this.checkRateLimit(request, reply, `/proxy/${keyUrl}`)) return;
|
||||||
|
|
||||||
return await this.proxyHandler(request, reply)
|
return await this.proxyHandler(request, reply)
|
||||||
.catch(err => this.errorHandler(request, reply, err));
|
.catch(err => this.errorHandler(request, reply, err));
|
||||||
});
|
});
|
||||||
|
@ -572,4 +594,71 @@ export class FileServerService {
|
||||||
path,
|
path,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Based on ApiCallService
|
||||||
|
private async checkRateLimit(
|
||||||
|
request: FastifyRequest<{
|
||||||
|
Body?: Record<string, unknown> | undefined,
|
||||||
|
Querystring?: Record<string, unknown> | undefined,
|
||||||
|
Params?: Record<string, unknown> | unknown,
|
||||||
|
}>,
|
||||||
|
reply: FastifyReply,
|
||||||
|
rateLimitKey: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const body = request.method === 'GET'
|
||||||
|
? request.query
|
||||||
|
: request.body;
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive)
|
||||||
|
const token = request.headers.authorization?.startsWith('Bearer ')
|
||||||
|
? request.headers.authorization.slice(7)
|
||||||
|
: body?.['i'];
|
||||||
|
if (token != null && typeof token !== 'string') {
|
||||||
|
reply.code(400);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
|
||||||
|
const [user] = await this.authenticateService.authenticate(token);
|
||||||
|
const actor = user?.id ?? getIpHash(request.ip);
|
||||||
|
|
||||||
|
const limit = {
|
||||||
|
// Group by resource
|
||||||
|
key: rateLimitKey,
|
||||||
|
|
||||||
|
// Maximum of 10 requests / 10 minutes
|
||||||
|
max: 10,
|
||||||
|
duration: 1000 * 60 * 10,
|
||||||
|
|
||||||
|
// Minimum of 250 ms between each request
|
||||||
|
minInterval: 250,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rate limit proxy requests
|
||||||
|
try {
|
||||||
|
await this.rateLimiterService.limit(limit, actor);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
// errはLimiter.LimiterInfoであることが期待される
|
||||||
|
reply.code(429);
|
||||||
|
|
||||||
|
if (hasRateLimitInfo(err)) {
|
||||||
|
const cooldownInSeconds = Math.ceil((err.info.resetMs - Date.now()) / 1000);
|
||||||
|
// もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく
|
||||||
|
reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.send({
|
||||||
|
message: 'Rate limit exceeded. Please try again later.',
|
||||||
|
code: 'RATE_LIMIT_EXCEEDED',
|
||||||
|
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasRateLimitInfo(err: unknown): err is { info: Limiter.LimiterInfo } {
|
||||||
|
return err != null && typeof(err) === 'object' && 'info' in err;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue