diff options
Diffstat (limited to 'packages/backend')
13 files changed, 94 insertions, 72 deletions
diff --git a/packages/backend/package.json b/packages/backend/package.json index f49acff701..c7a8a6c223 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -71,25 +71,25 @@ "utf-8-validate": "6.0.5" }, "dependencies": { - "@aws-sdk/client-s3": "3.940.0", - "@aws-sdk/lib-storage": "3.940.0", + "@aws-sdk/client-s3": "3.948.0", + "@aws-sdk/lib-storage": "3.948.0", "@discordapp/twemoji": "16.0.1", - "@fastify/accepts": "5.0.3", - "@fastify/cors": "11.1.0", + "@fastify/accepts": "5.0.4", + "@fastify/cors": "11.2.0", "@fastify/express": "4.0.2", - "@fastify/http-proxy": "11.3.0", + "@fastify/http-proxy": "11.4.1", "@fastify/multipart": "9.3.0", "@fastify/static": "8.3.0", "@kitajs/html": "4.2.11", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.2.5", - "@napi-rs/canvas": "0.1.83", + "@napi-rs/canvas": "0.1.84", "@nestjs/common": "11.1.9", "@nestjs/core": "11.1.9", "@nestjs/testing": "11.1.9", "@peertube/http-signature": "1.7.0", - "@sentry/node": "10.27.0", - "@sentry/profiling-node": "10.27.0", + "@sentry/node": "10.29.0", + "@sentry/profiling-node": "10.29.0", "@simplewebauthn/server": "13.2.2", "@sinonjs/fake-timers": "15.0.0", "@smithy/node-http-handler": "4.4.5", @@ -104,11 +104,11 @@ "bcryptjs": "3.0.3", "blurhash": "2.0.5", "body-parser": "2.2.1", - "bullmq": "5.65.0", + "bullmq": "5.65.1", "cacheable-lookup": "7.0.0", "chalk": "5.6.2", "chalk-template": "1.1.2", - "chokidar": "4.0.3", + "chokidar": "5.0.0", "color-convert": "3.1.3", "content-disposition": "1.0.1", "date-fns": "4.1.0", @@ -166,13 +166,13 @@ "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "systeminformation": "5.27.11", + "systeminformation": "5.27.14", "tinycolor2": "1.6.0", "tmp": "0.2.5", "tsc-alias": "1.8.16", - "typeorm": "0.3.27", + "typeorm": "0.3.28", "typescript": "5.9.3", - "ulid": "3.0.1", + "ulid": "3.0.2", "vary": "1.1.2", "web-push": "3.6.7", "ws": "8.18.3", @@ -182,7 +182,7 @@ "@jest/globals": "29.7.0", "@kitajs/ts-html-plugin": "4.1.3", "@nestjs/platform-express": "11.1.9", - "@sentry/vue": "10.27.0", + "@sentry/vue": "10.29.0", "@simplewebauthn/types": "12.0.0", "@swc/jest": "0.2.39", "@types/accepts": "1.3.7", @@ -196,7 +196,7 @@ "@types/jsonld": "1.5.15", "@types/mime-types": "3.0.1", "@types/ms": "2.1.0", - "@types/node": "24.10.1", + "@types/node": "24.10.2", "@types/nodemailer": "7.0.4", "@types/oauth2orize": "1.11.5", "@types/oauth2orize-pkce": "0.1.2", @@ -207,7 +207,7 @@ "@types/rename": "1.0.7", "@types/sanitize-html": "2.16.0", "@types/semver": "7.7.1", - "@types/simple-oauth2": "5.0.7", + "@types/simple-oauth2": "5.0.8", "@types/sinonjs__fake-timers": "15.0.1", "@types/supertest": "6.0.3", "@types/tinycolor2": "1.4.6", @@ -215,13 +215,13 @@ "@types/vary": "1.1.3", "@types/web-push": "3.6.4", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.48.0", - "@typescript-eslint/parser": "8.48.0", + "@typescript-eslint/eslint-plugin": "8.49.0", + "@typescript-eslint/parser": "8.49.0", "aws-sdk-client-mock": "4.1.0", "cbor": "10.0.11", "cross-env": "10.1.0", "eslint-plugin-import": "2.32.0", - "execa": "9.6.0", + "execa": "9.6.1", "fkill": "10.0.1", "jest": "29.7.0", "jest-mock": "29.7.0", @@ -230,6 +230,6 @@ "pid-port": "2.0.0", "simple-oauth2": "5.1.0", "supertest": "7.1.4", - "vite": "7.2.4" + "vite": "7.2.7" } } diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index f9852d3578..657d7869fa 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -30,6 +30,7 @@ type Source = { socket?: string; trustProxy?: FastifyServerOptions['trustProxy']; chmodSocket?: string; + enableIpRateLimit?: boolean; disableHsts?: boolean; db: { host: string; @@ -120,8 +121,9 @@ export type Config = { url: string; port: number; socket: string | undefined; - trustProxy: FastifyServerOptions['trustProxy']; + trustProxy: NonNullable<FastifyServerOptions['trustProxy']>; chmodSocket: string | undefined; + enableIpRateLimit: boolean; disableHsts: boolean | undefined; db: { host: string; @@ -263,9 +265,17 @@ export function loadConfig(): Config { url: url.origin, port: config.port ?? parseInt(process.env.PORT ?? '', 10), socket: config.socket, - trustProxy: config.trustProxy, + trustProxy: config.trustProxy ?? [ + '10.0.0.0/8', + '172.16.0.0/12', + '192.168.0.0/16', + '127.0.0.1/32', + '::1/128', + 'fc00::/7', + ], chmodSocket: config.chmodSocket, disableHsts: config.disableHsts, + enableIpRateLimit: config.enableIpRateLimit ?? true, host, hostname, scheme, diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts index 7a005400bb..cbae280030 100644 --- a/packages/backend/src/core/AiService.ts +++ b/packages/backend/src/core/AiService.ts @@ -7,7 +7,6 @@ import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; import { Injectable } from '@nestjs/common'; -import si from 'systeminformation'; import { Mutex } from 'async-mutex'; import fetch from 'node-fetch'; import { bindThis } from '@/decorators.js'; @@ -84,6 +83,7 @@ export class AiService { @bindThis private async getCpuFlags(): Promise<string[]> { + const si = await import('systeminformation'); const str = await si.cpuFlags(); return str.split(/\s+/); } diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index d229efb123..a972e5861c 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -4,13 +4,12 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import si from 'systeminformation'; import Xev from 'xev'; import * as osUtils from 'os-utils'; import { bindThis } from '@/decorators.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; import { MiMeta } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; const ev = new Xev(); @@ -97,12 +96,14 @@ function cpuUsage(): Promise<number> { // MEMORY STAT async function mem() { + const si = await import('systeminformation'); const data = await si.mem(); return data; } // NETWORK STAT async function net() { + const si = await import('systeminformation'); const iface = await si.networkInterfaceDefault(); const data = await si.networkStats(iface); return data[0]; @@ -110,5 +111,6 @@ async function net() { // FS STAT async function fs() { + const si = await import('systeminformation'); return await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 })); } diff --git a/packages/backend/src/misc/show-machine-info.ts b/packages/backend/src/misc/show-machine-info.ts index 8ddec35f23..b279eb9546 100644 --- a/packages/backend/src/misc/show-machine-info.ts +++ b/packages/backend/src/misc/show-machine-info.ts @@ -4,15 +4,11 @@ */ import * as os from 'node:os'; -import sysUtils from 'systeminformation'; import type Logger from '@/logger.js'; export async function showMachineInfo(parentLogger: Logger) { const logger = parentLogger.createSubLogger('machine'); logger.debug(`Hostname: ${os.hostname()}`); logger.debug(`Platform: ${process.platform} Arch: ${process.arch}`); - const mem = await sysUtils.mem(); - const totalmem = (mem.total / 1024 / 1024 / 1024).toFixed(1); - const availmem = (mem.available / 1024 / 1024 / 1024).toFixed(1); - logger.debug(`CPU: ${os.cpus().length} core MEM: ${totalmem}GB (available: ${availmem}GB)`); + logger.debug(`CPU: ${os.cpus().length} core MEM: ${(os.totalmem() / 1024 / 1024 / 1024).toFixed(1)}GB (available: ${(os.freemem() / 1024 / 1024 / 1024).toFixed(1)}GB)`); } diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 4e05322b12..ef9ac81f95 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown { @bindThis public async launch(): Promise<void> { const fastify = Fastify({ - trustProxy: this.config.trustProxy ?? false, + trustProxy: this.config.trustProxy, logger: false, }); this.#fastify = fastify; diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 27c79ab438..8bae46d9fb 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -313,11 +313,14 @@ export class ApiCallService implements OnApplicationShutdown { } if (ep.meta.limit) { - // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. - let limitActor: string; + let limitActor: string | null = null; if (user) { limitActor = user.id; - } else { + } else if (this.config.enableIpRateLimit) { + if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) { + this.logger.warn('Recieved API request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.'); + } + limitActor = getIpHash(request.ip); } @@ -330,7 +333,7 @@ export class ApiCallService implements OnApplicationShutdown { // TODO: 毎リクエスト計算するのもあれだしキャッシュしたい const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1; - if (factor > 0) { + if (limitActor != null && factor > 0) { // Rate limit const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor); if (rateLimit != null) { diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 3e889372d8..00e8828242 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -15,6 +15,7 @@ import type { UserSecurityKeysRepository, UsersRepository, } from '@/models/_.js'; +import type Logger from '@/logger.js'; import type { Config } from '@/config.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; import type { MiLocalUser } from '@/models/User.js'; @@ -23,6 +24,7 @@ import { bindThis } from '@/decorators.js'; import { WebAuthnService } from '@/core/WebAuthnService.js'; import { UserAuthService } from '@/core/UserAuthService.js'; import { CaptchaService } from '@/core/CaptchaService.js'; +import { LoggerService } from '@/core/LoggerService.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { RateLimiterService } from './RateLimiterService.js'; import { SigninService } from './SigninService.js'; @@ -31,6 +33,8 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; @Injectable() export class SigninApiService { + private logger: Logger; + constructor( @Inject(DI.config) private config: Config, @@ -50,6 +54,7 @@ export class SigninApiService { @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, + private loggerService: LoggerService, private idService: IdService, private rateLimiterService: RateLimiterService, private signinService: SigninService, @@ -57,6 +62,7 @@ export class SigninApiService { private webAuthnService: WebAuthnService, private captchaService: CaptchaService, ) { + this.logger = this.loggerService.getLogger('Signin'); } @bindThis @@ -90,16 +96,21 @@ export class SigninApiService { } // not more than 1 attempt per second and not more than 10 attempts per hour - const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip)); - if (rateLimit != null) { - reply.code(429); - return { - error: { - message: 'Too many failed attempts to sign in. Try again later.', - code: 'TOO_MANY_AUTHENTICATION_FAILURES', - id: '22d05606-fbcf-421a-a2db-b32610dcfd1b', - }, - }; + if (this.config.enableIpRateLimit) { + if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) { + this.logger.warn('Recieved signin request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.'); + } + const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip)); + if (rateLimit != null) { + reply.code(429); + return { + error: { + message: 'Too many failed attempts to sign in. Try again later.', + code: 'TOO_MANY_AUTHENTICATION_FAILURES', + id: '22d05606-fbcf-421a-a2db-b32610dcfd1b', + }, + }; + } } if (typeof username !== 'string') { diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts index 9ba23c54e2..920f9d0b3a 100644 --- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts +++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts @@ -84,19 +84,25 @@ export class SigninWithPasskeyApiService { return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' }); }; - try { + if (this.config.enableIpRateLimit) { + if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) { + this.logger.warn('Recieved signin with passkey request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.'); + } + + try { // Not more than 1 API call per 250ms and not more than 100 attempts per 30min // NOTE: 1 Sign-in require 2 API calls - await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip)); - } catch (err) { - reply.code(429); - return { - error: { - message: 'Too many failed attempts to sign in. Try again later.', - code: 'TOO_MANY_AUTHENTICATION_FAILURES', - id: '22d05606-fbcf-421a-a2db-b32610dcfd1b', - }, - }; + await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip)); + } catch (err) { + reply.code(429); + return { + error: { + message: 'Too many failed attempts to sign in. Try again later.', + code: 'TOO_MANY_AUTHENTICATION_FAILURES', + id: '22d05606-fbcf-421a-a2db-b32610dcfd1b', + }, + }; + } } // Initiate Passkey Auth challenge with context diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts index f3e440b4cb..86158d7e22 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts @@ -52,18 +52,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- super(meta, paramDef, async (ps, me) => { const jobs = await this.deliverQueue.getJobs(['delayed']); - const res = [] as [string, number][]; + const counts = new Map<string, number>(); for (const job of jobs) { const host = new URL(job.data.to).host; - if (res.find(x => x[0] === host)) { - res.find(x => x[0] === host)![1]++; - } else { - res.push([host, 1]); - } + counts.set(host, (counts.get(host) ?? 0) + 1); } - res.sort((a, b) => b[1] - a[1]); + const res = [...counts.entries()].sort((a, b) => b[1] - a[1]); return res; }); diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts index e7589cba81..ad6a823b8f 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts @@ -52,18 +52,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- super(meta, paramDef, async (ps, me) => { const jobs = await this.inboxQueue.getJobs(['delayed']); - const res = [] as [string, number][]; + const counts = new Map<string, number>(); for (const job of jobs) { const host = new URL(job.data.signature.keyId).host; - if (res.find(x => x[0] === host)) { - res.find(x => x[0] === host)![1]++; - } else { - res.push([host, 1]); - } + counts.set(host, (counts.get(host) ?? 0) + 1); } - res.sort((a, b) => b[1] - a[1]); + const res = [...counts.entries()].sort((a, b) => b[1] - a[1]); return res; }); diff --git a/packages/backend/src/server/api/endpoints/admin/server-info.ts b/packages/backend/src/server/api/endpoints/admin/server-info.ts index 80b6a4d32e..603be514c8 100644 --- a/packages/backend/src/server/api/endpoints/admin/server-info.ts +++ b/packages/backend/src/server/api/endpoints/admin/server-info.ts @@ -4,7 +4,6 @@ */ import * as os from 'node:os'; -import si from 'systeminformation'; import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import * as Redis from 'ioredis'; @@ -112,6 +111,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ) { super(meta, paramDef, async () => { + const si = await import('systeminformation'); + const memStats = await si.mem(); const fsStats = await si.fsSize(); const netInterface = await si.networkInterfaceDefault(); diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index 8301c85f2e..0e8dc73ad9 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -4,7 +4,6 @@ */ import * as os from 'node:os'; -import si from 'systeminformation'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { MiMeta } from '@/models/_.js'; @@ -93,6 +92,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- }, }; + const si = await import('systeminformation'); + const memStats = await si.mem(); const fsStats = await si.fsSize(); |