Merge branch 'develop' into feature/2024.10
This commit is contained in:
commit
d069d78c21
33 changed files with 594 additions and 178 deletions
|
|
@ -152,7 +152,7 @@ export class ActivityPubServerService {
|
|||
let signature;
|
||||
|
||||
try {
|
||||
signature = httpSignature.parseRequest(request.raw, { 'headers': [] });
|
||||
signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'host', 'date'], authorizationHeaderName: 'signature' });
|
||||
} catch (e) {
|
||||
// not signed, or malformed signature: refuse
|
||||
this.authlogger.warn(`${request.id} ${request.url} not signed, or malformed signature: refuse`);
|
||||
|
|
@ -229,7 +229,7 @@ export class ActivityPubServerService {
|
|||
let signature;
|
||||
|
||||
try {
|
||||
signature = httpSignature.parseRequest(request.raw, { 'headers': [] });
|
||||
signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'digest', 'host', 'date'], authorizationHeaderName: 'signature' });
|
||||
} catch (e) {
|
||||
reply.code(401);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,11 @@ import { bindThis } from '@/decorators.js';
|
|||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||
import { correctFilename } from '@/misc/correct-filename.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 Limiter from 'ratelimiter';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
|
@ -52,6 +56,8 @@ export class FileServerService {
|
|||
private videoProcessingService: VideoProcessingService,
|
||||
private internalStorageService: InternalStorageService,
|
||||
private loggerService: LoggerService,
|
||||
private authenticateService: AuthenticateService,
|
||||
private rateLimiterService: RateLimiterService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('server', 'gray');
|
||||
|
||||
|
|
@ -76,6 +82,8 @@ export class FileServerService {
|
|||
});
|
||||
|
||||
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)
|
||||
.catch(err => this.errorHandler(request, reply, err));
|
||||
});
|
||||
|
|
@ -89,6 +97,20 @@ export class FileServerService {
|
|||
Params: { url: string; };
|
||||
Querystring: { url?: string; };
|
||||
}>('/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)
|
||||
.catch(err => this.errorHandler(request, reply, err));
|
||||
});
|
||||
|
|
@ -572,4 +594,71 @@ export class FileServerService {
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
|
|||
export const meta = {
|
||||
tags: ['federation'],
|
||||
|
||||
requireAdmin: true,
|
||||
requireCredential: true,
|
||||
kind: 'read:federation',
|
||||
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
return await this.mergePack(
|
||||
me,
|
||||
isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null,
|
||||
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, true) : null,
|
||||
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, undefined, true) : null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,16 +50,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private emojiEntityService: EmojiEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const emojis = await this.emojisRepository.find({
|
||||
where: {
|
||||
host: IsNull(),
|
||||
},
|
||||
order: {
|
||||
category: 'ASC',
|
||||
name: 'ASC',
|
||||
},
|
||||
});
|
||||
|
||||
const emojis = await this.emojisRepository.createQueryBuilder()
|
||||
.where('host IS NULL')
|
||||
.orderBy('LOWER(category)', 'ASC')
|
||||
.orderBy('LOWER(name)', 'ASC')
|
||||
.getMany();
|
||||
return {
|
||||
emojis: await this.emojiEntityService.packSimpleMany(emojis),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -66,10 +66,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.cannotReportYourself);
|
||||
}
|
||||
|
||||
if (await this.roleService.isAdministrator(targetUser)) {
|
||||
throw new ApiError(meta.errors.cannotReportAdmin);
|
||||
}
|
||||
|
||||
await this.abuseReportService.report([{
|
||||
targetUserId: targetUser.id,
|
||||
targetUserHost: targetUser.host,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { summaly } from '@misskey-dev/summaly';
|
||||
import { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
|
|
@ -15,9 +16,9 @@ import { LoggerService } from '@/core/LoggerService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { MiMeta } from '@/models/Meta.js';
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import * as Redis from 'ioredis';
|
||||
import { RedisKVCache } from '@/misc/cache.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
export class UrlPreviewService {
|
||||
|
|
@ -36,12 +37,13 @@ export class UrlPreviewService {
|
|||
|
||||
private httpRequestService: HttpRequestService,
|
||||
private loggerService: LoggerService,
|
||||
private utilityService: UtilityService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('url-preview');
|
||||
this.previewCache = new RedisKVCache<SummalyResult>(this.redisClient, 'summaly', {
|
||||
lifetime: 1000 * 60 * 60 * 24, // 1d
|
||||
memoryCacheLifetime: 1000 * 60 * 10, // 10m
|
||||
fetcher: (key: string) => { throw new Error('the UrlPreview cache should never fetch'); },
|
||||
fetcher: () => { throw new Error('the UrlPreview cache should never fetch'); },
|
||||
toRedisConverter: (value) => JSON.stringify(value),
|
||||
fromRedisConverter: (value) => JSON.parse(value),
|
||||
});
|
||||
|
|
@ -65,7 +67,7 @@ export class UrlPreviewService {
|
|||
reply: FastifyReply,
|
||||
): Promise<object | undefined> {
|
||||
const url = request.query.url;
|
||||
if (typeof url !== 'string') {
|
||||
if (typeof url !== 'string' || !URL.canParse(url)) {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
|
@ -87,6 +89,18 @@ export class UrlPreviewService {
|
|||
};
|
||||
}
|
||||
|
||||
const host = new URL(url).host;
|
||||
if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) {
|
||||
reply.code(403);
|
||||
return {
|
||||
error: new ApiError({
|
||||
message: 'URL is blocked',
|
||||
code: 'URL_PREVIEW_BLOCKED',
|
||||
id: '50294652-857b-4b13-9700-8e5c7a8deae8',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const key = `${url}@${lang}`;
|
||||
const cached = await this.previewCache.get(key);
|
||||
if (cached !== undefined) {
|
||||
|
|
@ -170,6 +184,6 @@ export class UrlPreviewService {
|
|||
contentLengthRequired: meta.urlPreviewRequireContentLength,
|
||||
});
|
||||
|
||||
return this.httpRequestService.getJson<SummalyResult>(`${proxy}?${queryStr}`);
|
||||
return this.httpRequestService.getJson<SummalyResult>(`${proxy}?${queryStr}`, 'application/json, */*', undefined, true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue