From 7b73dd2d62333725f3974e9ec041e936e37a19c9 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 23 Jan 2023 20:07:48 +0900 Subject: enhance(server): onlyServer / onlyQueue オプションを復活 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/boot/common.ts | 35 +++++++++++++++++++++++++++++++++++ packages/backend/src/boot/master.ts | 33 +++++++++------------------------ packages/backend/src/boot/worker.ts | 23 +++++++++-------------- 3 files changed, 53 insertions(+), 38 deletions(-) create mode 100644 packages/backend/src/boot/common.ts (limited to 'packages/backend/src') diff --git a/packages/backend/src/boot/common.ts b/packages/backend/src/boot/common.ts new file mode 100644 index 0000000000..04aa26e652 --- /dev/null +++ b/packages/backend/src/boot/common.ts @@ -0,0 +1,35 @@ +import { NestFactory } from '@nestjs/core'; +import { ChartManagementService } from '@/core/chart/ChartManagementService.js'; +import { QueueProcessorService } from '@/queue/QueueProcessorService.js'; +import { NestLogger } from '@/NestLogger.js'; +import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js'; +import { JanitorService } from '@/daemons/JanitorService.js'; +import { QueueStatsService } from '@/daemons/QueueStatsService.js'; +import { ServerStatsService } from '@/daemons/ServerStatsService.js'; +import { ServerService } from '@/server/ServerService.js'; +import { MainModule } from '@/MainModule.js'; + +export async function server() { + const app = await NestFactory.createApplicationContext(MainModule, { + logger: new NestLogger(), + }); + app.enableShutdownHooks(); + + const serverService = app.get(ServerService); + serverService.launch(); + + app.get(ChartManagementService).start(); + app.get(JanitorService).start(); + app.get(QueueStatsService).start(); + app.get(ServerStatsService).start(); +} + +export async function jobQueue() { + const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, { + logger: new NestLogger(), + }); + jobQueue.enableShutdownHooks(); + + jobQueue.get(QueueProcessorService).start(); + jobQueue.get(ChartManagementService).start(); +} diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 93cb3131ba..f5d936fadf 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -6,21 +6,12 @@ import cluster from 'node:cluster'; import chalk from 'chalk'; import chalkTemplate from 'chalk-template'; import semver from 'semver'; -import { NestFactory } from '@nestjs/core'; import Logger from '@/logger.js'; import { loadConfig } from '@/config.js'; import type { Config } from '@/config.js'; -import { lessThan } from '@/misc/prelude/array.js'; import { showMachineInfo } from '@/misc/show-machine-info.js'; -import { DaemonModule } from '@/daemons/DaemonModule.js'; -import { JanitorService } from '@/daemons/JanitorService.js'; -import { QueueStatsService } from '@/daemons/QueueStatsService.js'; -import { ServerStatsService } from '@/daemons/ServerStatsService.js'; -import { NestLogger } from '@/NestLogger.js'; -import { ChartManagementService } from '@/core/chart/ChartManagementService.js'; -import { ServerService } from '@/server/ServerService.js'; -import { MainModule } from '@/MainModule.js'; -import { envOption } from '../env.js'; +import { envOption } from '@/env.js'; +import { jobQueue, server } from './common.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -73,14 +64,13 @@ export async function masterMain() { process.exit(1); } - const app = await NestFactory.createApplicationContext(MainModule, { - logger: new NestLogger(), - }); - app.enableShutdownHooks(); - - // start server - const serverService = app.get(ServerService); - serverService.launch(); + if (envOption.onlyServer) { + await server(); + } else if (envOption.onlyQueue) { + await jobQueue(); + } else { + await server(); + } bootLogger.succ('Misskey initialized'); @@ -89,11 +79,6 @@ export async function masterMain() { } bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); - - app.get(ChartManagementService).start(); - app.get(JanitorService).start(); - app.get(QueueStatsService).start(); - app.get(ServerStatsService).start(); } function showEnvironment(): void { diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts index e0574643b7..ab75aaa572 100644 --- a/packages/backend/src/boot/worker.ts +++ b/packages/backend/src/boot/worker.ts @@ -1,23 +1,18 @@ import cluster from 'node:cluster'; -import { NestFactory } from '@nestjs/core'; -import { ChartManagementService } from '@/core/chart/ChartManagementService.js'; -import { QueueProcessorService } from '@/queue/QueueProcessorService.js'; -import { NestLogger } from '@/NestLogger.js'; -import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js'; +import { envOption } from '@/env.js'; +import { jobQueue, server } from './common.js'; /** * Init worker process */ export async function workerMain() { - const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, { - logger: new NestLogger(), - }); - jobQueue.enableShutdownHooks(); - - // start job queue - jobQueue.get(QueueProcessorService).start(); - - jobQueue.get(ChartManagementService).start(); + if (envOption.onlyServer) { + await server(); + } else if (envOption.onlyQueue) { + await jobQueue(); + } else { + await jobQueue(); + } if (cluster.isWorker) { // Send a 'ready' message to parent process -- cgit v1.2.3-freya From 238f923b419b487337fbcaa5ba415bf2f823f27e Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 24 Jan 2023 08:31:02 +0900 Subject: refactor(server): httpRequestServiceのUndiciFetcher依存をなるべくカプセル化 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/DownloadService.ts | 33 ++++++-------- packages/backend/src/core/HttpRequestService.ts | 53 ++++++++++++---------- .../src/core/activitypub/ApResolverService.ts | 10 ++-- 3 files changed, 49 insertions(+), 47 deletions(-) (limited to 'packages/backend/src') diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index a3078bff45..e4632f857a 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -6,6 +6,7 @@ import IPCIDR from 'ip-cidr'; import PrivateIp from 'private-ip'; import got, * as Got from 'got'; import chalk from 'chalk'; +import { buildConnector } from 'undici'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js'; @@ -13,7 +14,6 @@ import { createTemp } from '@/misc/create-temp.js'; import { StatusError } from '@/misc/status-error.js'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; -import { buildConnector } from 'undici'; const pipeline = util.promisify(stream.pipeline); import { bindThis } from '@/decorators.js'; @@ -32,23 +32,20 @@ export class DownloadService { ) { this.logger = this.loggerService.getLogger('download'); - this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption( - { - connect: process.env.NODE_ENV === 'development' ? - this.httpRequestService.clientDefaults.connect - : - this.httpRequestService.getConnectorWithIpCheck( - buildConnector({ - ...this.httpRequestService.clientDefaults.connect, - }), - (ip) => !this.isPrivateIp(ip) - ), - bodyTimeout: 30 * 1000, - }, - { - connect: this.httpRequestService.clientDefaults.connect, - } - ), this.logger); + this.undiciFetcher = this.httpRequestService.createFetcher({ + connect: process.env.NODE_ENV === 'development' ? + this.httpRequestService.clientDefaults.connect + : + this.httpRequestService.getConnectorWithIpCheck( + buildConnector({ + ...this.httpRequestService.clientDefaults.connect, + }), + (ip) => !this.isPrivateIp(ip), + ), + bodyTimeout: 30 * 1000, + }, { + connect: this.httpRequestService.clientDefaults.connect, + }, this.logger); } @bindThis diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 2864ad4405..3a70ac77f9 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -1,14 +1,14 @@ import * as http from 'node:http'; import * as https from 'node:https'; +import { LookupFunction } from 'node:net'; import CacheableLookup from 'cacheable-lookup'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { Inject, Injectable } from '@nestjs/common'; +import * as undici from 'undici'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { StatusError } from '@/misc/status-error.js'; import { bindThis } from '@/decorators.js'; -import * as undici from 'undici'; -import { LookupFunction } from 'node:net'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; @@ -62,7 +62,7 @@ export class UndiciFetcher { undici.buildConnector(args.agentOptions.connect as undici.buildConnector.BuildOptions)(options, (err, socket) => { this.logger?.debug('Socket connector called', socket); if (err) { - this.logger?.debug(`Socket error`, err); + this.logger?.debug('Socket error', err); cb(new Error(`Error while socket connecting\n${err}`), null); return; } @@ -79,20 +79,20 @@ export class UndiciFetcher { uri: args.proxy.uri, - connect: (process.env.NODE_ENV !== 'production' && typeof (args.proxy?.options?.connect ?? args.agentOptions.connect) !== 'function') + connect: (process.env.NODE_ENV !== 'production' && typeof (args.proxy.options?.connect ?? args.agentOptions.connect) !== 'function') ? (options, cb) => { // Custom connector for debug undici.buildConnector((args.proxy?.options?.connect ?? args.agentOptions.connect) as undici.buildConnector.BuildOptions)(options, (err, socket) => { this.logger?.debug('Socket connector called (secure)', socket); if (err) { - this.logger?.debug(`Socket error`, err); + this.logger?.debug('Socket error', err); cb(new Error(`Error while socket connecting\n${err}`), null); return; } this.logger?.debug(`Socket connected (secure): port ${socket.localPort} => remote ${socket.remoteAddress}`); cb(null, socket); }); - } : (args.proxy?.options?.connect ?? args.agentOptions.connect), + } : (args.proxy.options?.connect ?? args.agentOptions.connect), }) : this.nonProxiedAgent; } @@ -115,7 +115,7 @@ export class UndiciFetcher { public async fetch( url: string | URL, options: undici.RequestInit = {}, - privateOptions: { noOkError?: boolean; bypassProxy?: boolean; } = { noOkError: false, bypassProxy: false } + privateOptions: { noOkError?: boolean; bypassProxy?: boolean; } = { noOkError: false, bypassProxy: false }, ): Promise { const res = await undici.fetch(url, { dispatcher: this.getAgentByUrl(new URL(url), privateOptions.bypassProxy), @@ -142,7 +142,7 @@ export class UndiciFetcher { headers: Object.assign({ Accept: accept, }, headers ?? {}), - } + }, ); return await res.json() as T; @@ -156,7 +156,7 @@ export class UndiciFetcher { headers: Object.assign({ Accept: accept, }, headers ?? {}), - } + }, ); return await res.text(); @@ -219,18 +219,18 @@ export class HttpRequestService { maxCachedSessions: 300, // TLSセッションのキャッシュ数 https://github.com/nodejs/undici/blob/v5.14.0/lib/core/connect.js#L80 lookup: this.dnsCache.lookup as LookupFunction, // https://github.com/nodejs/undici/blob/v5.14.0/lib/core/connect.js#L98 }, - } + }; this.maxSockets = Math.max(64, this.config.deliverJobConcurrency ?? 128); - this.defaultFetcher = new UndiciFetcher(this.getStandardUndiciFetcherOption(), this.logger); + this.defaultFetcher = this.createFetcher({}, {}, this.logger); this.fetch = this.defaultFetcher.fetch; this.getHtml = this.defaultFetcher.getHtml; - this.defaultJsonFetcher = new UndiciFetcher(this.getStandardUndiciFetcherOption({ + this.defaultJsonFetcher = this.createFetcher({ maxResponseSize: 1024 * 256, - }), this.logger); + }, {}, this.logger); this.getJson = this.defaultJsonFetcher.getJson; @@ -272,23 +272,28 @@ export class HttpRequestService { } @bindThis - public getStandardUndiciFetcherOption(opts: undici.Agent.Options = {}, proxyOpts: undici.Agent.Options = {}) { + private getStandardUndiciFetcherOption(opts: undici.Agent.Options = {}, proxyOpts: undici.Agent.Options = {}) { return { agentOptions: { ...this.clientDefaults, ...opts, }, ...(this.config.proxy ? { - proxy: { - uri: this.config.proxy, - options: { - connections: this.maxSockets, - ...proxyOpts, - } - } + proxy: { + uri: this.config.proxy, + options: { + connections: this.maxSockets, + ...proxyOpts, + }, + }, } : {}), userAgent: this.config.userAgent, - } + }; + } + + @bindThis + public createFetcher(opts: undici.Agent.Options = {}, proxyOpts: undici.Agent.Options = {}, logger: Logger) { + return new UndiciFetcher(this.getStandardUndiciFetcherOption(opts, proxyOpts), logger); } /** @@ -314,13 +319,13 @@ export class HttpRequestService { connector(options, (err, socket) => { this.logger.debug('Socket connector (with ip checker) called', socket); if (err) { - this.logger.error(`Socket error`, err) + this.logger.error('Socket error', err); cb(new Error(`Error while socket connecting\n${err}`), null); return; } if (socket.remoteAddress == undefined) { - this.logger.error(`Socket error: remoteAddress is undefined`); + this.logger.error('Socket error: remoteAddress is undefined'); cb(new Error('remoteAddress is undefined (maybe socket destroyed)'), null); return; } diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index e51ae37954..94bb6d6c55 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -8,13 +8,13 @@ import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js' import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import type Logger from '@/logger.js'; import { isCollectionOrOrderedCollection } from './type.js'; import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; import { ApRequestService } from './ApRequestService.js'; -import { LoggerService } from '@/core/LoggerService.js'; import type { IObject, ICollection, IOrderedCollection } from './type.js'; -import type Logger from '@/logger.js'; export class Resolver { private history: Set; @@ -39,10 +39,10 @@ export class Resolver { private recursionLimit = 100, ) { this.history = new Set(); - this.logger = this.loggerService?.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる - this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption({ + this.logger = this.loggerService.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる + this.undiciFetcher = this.httpRequestService.createFetcher({ maxRedirections: 0, - }), this.logger); + }, {}, this.logger); } @bindThis -- cgit v1.2.3-freya From 869854eae736cf5c910f533191ea0d1f7c9755dd Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 24 Jan 2023 08:32:17 +0900 Subject: コミット漏れ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/DownloadService.ts | 1 - packages/backend/src/core/activitypub/ApRequestService.ts | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) (limited to 'packages/backend/src') diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index e4632f857a..c5b2bcaef4 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -4,7 +4,6 @@ import * as util from 'node:util'; import { Inject, Injectable } from '@nestjs/common'; import IPCIDR from 'ip-cidr'; import PrivateIp from 'private-ip'; -import got, * as Got from 'got'; import chalk from 'chalk'; import { buildConnector } from 'undici'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index ab22a0c411..fd7319702e 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -41,10 +41,10 @@ export class ApRequestService { private httpRequestService: HttpRequestService, private loggerService: LoggerService, ) { - this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる - this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption({ + this.logger = this.loggerService.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる + this.undiciFetcher = this.httpRequestService.createFetcher({ maxRedirections: 0, - }), this.logger ); + }, {}, this.logger); } @bindThis @@ -169,7 +169,7 @@ export class ApRequestService { method: req.request.method, headers: req.request.headers, body, - } + }, ); } @@ -197,7 +197,7 @@ export class ApRequestService { { method: req.request.method, headers: req.request.headers, - } + }, ); return await res.json(); -- cgit v1.2.3-freya From 9fd1b35d958523205444fecb7e9184072a1720e4 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 24 Jan 2023 01:34:14 +0000 Subject: fix TypeError: Cannot read properties of undefined (reading 'getLogger') --- packages/backend/src/core/activitypub/ApResolverService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/backend/src') diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 94bb6d6c55..ca7760af81 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -39,7 +39,7 @@ export class Resolver { private recursionLimit = 100, ) { this.history = new Set(); - this.logger = this.loggerService.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる + this.logger = this.loggerService?.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる this.undiciFetcher = this.httpRequestService.createFetcher({ maxRedirections: 0, }, {}, this.logger); -- cgit v1.2.3-freya From b62894ff56b33f96dd979e309e015858cafcc33a Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 24 Jan 2023 15:02:16 +0900 Subject: use minified css --- packages/backend/src/server/web/views/base.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/backend/src') diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index b27bbcbce0..d05901baec 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -35,7 +35,7 @@ html link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg') - link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.css') + link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css') link(rel='modulepreload' href=`/vite/${clientEntry.file}`) if !config.clientManifestExists -- cgit v1.2.3-freya From 6589e8a3909e42d8d116b06a608d5cfefd6aa7ed Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 24 Jan 2023 15:54:14 +0900 Subject: Fix #9710 ? (#9712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * update pnpm-lock * use our own DevNull * fix * deliverJobConcurrencyをmacSocketsで割ってソケット数にする --- packages/backend/src/core/HttpRequestService.ts | 37 +++++++++++++++++++--- .../src/core/activitypub/ApRequestService.ts | 7 ++-- packages/backend/src/misc/dev-null.ts | 11 +++++++ pnpm-lock.yaml | 13 +------- 4 files changed, 49 insertions(+), 19 deletions(-) create mode 100644 packages/backend/src/misc/dev-null.ts (limited to 'packages/backend/src') diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 3a70ac77f9..cd859d0023 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -134,9 +134,34 @@ export class UndiciFetcher { return res; } + @bindThis + public async request( + url: string | URL, + options: { dispatcher?: undici.Dispatcher } & Omit & Partial> = {}, + privateOptions: { noOkError?: boolean; bypassProxy?: boolean; } = { noOkError: false, bypassProxy: false }, + ): Promise { + const res = await undici.request(url, { + dispatcher: this.getAgentByUrl(new URL(url), privateOptions.bypassProxy), + ...options, + headers: { + 'user-agent': this.userAgent ?? '', + ...(options.headers ?? {}), + }, + }).catch((err) => { + this.logger?.error(`fetch error to ${typeof url === 'string' ? url : url.href}`, err); + throw new StatusError('Resource Unreachable', 500, 'Resource Unreachable'); + }); + + if (res.statusCode >= 400) { + throw new StatusError(`${res.statusCode}`, res.statusCode, ''); + } + + return res; + } + @bindThis public async getJson(url: string, accept = 'application/json, */*', headers?: Record): Promise { - const res = await this.fetch( + const { body } = await this.request( url, { headers: Object.assign({ @@ -145,12 +170,12 @@ export class UndiciFetcher { }, ); - return await res.json() as T; + return await body.json() as T; } @bindThis public async getHtml(url: string, accept = 'text/html, */*', headers?: Record): Promise { - const res = await this.fetch( + const { body } = await this.request( url, { headers: Object.assign({ @@ -159,7 +184,7 @@ export class UndiciFetcher { }, ); - return await res.text(); + return await body.text(); } } @@ -167,6 +192,7 @@ export class UndiciFetcher { export class HttpRequestService { public defaultFetcher: UndiciFetcher; public fetch: UndiciFetcher['fetch']; + public request: UndiciFetcher['request']; public getHtml: UndiciFetcher['getHtml']; public defaultJsonFetcher: UndiciFetcher; public getJson: UndiciFetcher['getJson']; @@ -221,11 +247,12 @@ export class HttpRequestService { }, }; - this.maxSockets = Math.max(64, this.config.deliverJobConcurrency ?? 128); + this.maxSockets = Math.max(64, ((this.config.deliverJobConcurrency ?? 128) / (this.config.clusterLimit ?? 1))); this.defaultFetcher = this.createFetcher({}, {}, this.logger); this.fetch = this.defaultFetcher.fetch; + this.request = this.defaultFetcher.request; this.getHtml = this.defaultFetcher.getHtml; this.defaultJsonFetcher = this.createFetcher({ diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index fd7319702e..db87475c4c 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -9,10 +9,12 @@ import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js' import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; +import type { Dispatcher } from 'undici'; +import { DevNull } from '@/misc/dev-null.js'; type Request = { url: string; - method: string; + method: Dispatcher.HttpMethod; headers: Record; }; @@ -163,7 +165,7 @@ export class ApRequestService { }, }); - await this.undiciFetcher.fetch( + const response = await this.undiciFetcher.request( url, { method: req.request.method, @@ -171,6 +173,7 @@ export class ApRequestService { body, }, ); + response.body.pipe(new DevNull()); } /** diff --git a/packages/backend/src/misc/dev-null.ts b/packages/backend/src/misc/dev-null.ts new file mode 100644 index 0000000000..38b9d82669 --- /dev/null +++ b/packages/backend/src/misc/dev-null.ts @@ -0,0 +1,11 @@ +import { Writable, WritableOptions } from "node:stream"; + +export class DevNull extends Writable implements NodeJS.WritableStream { + constructor(opts?: WritableOptions) { + super(opts); + } + + _write (chunk: any, encoding: BufferEncoding, cb: (err?: Error | null) => void) { + setImmediate(cb); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cdbbd3b825..3fdaf1449a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3801,7 +3801,7 @@ packages: /axios/0.24.0: resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==} dependencies: - follow-redirects: 1.15.2 + follow-redirects: 1.15.2_debug@4.3.4 transitivePeerDependencies: - debug dev: false @@ -6973,16 +6973,6 @@ packages: readable-stream: 2.3.7 dev: false - /follow-redirects/1.15.2: - resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dev: false - /follow-redirects/1.15.2_debug@4.3.4: resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} engines: {node: '>=4.0'} @@ -6993,7 +6983,6 @@ packages: optional: true dependencies: debug: 4.3.4 - dev: true /for-each/0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} -- cgit v1.2.3-freya From 8cab16c824f27a1ebc6b67c8012504eb58e50e20 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 24 Jan 2023 17:51:09 +0900 Subject: fix(server): /api/signin always returns 429 when request header x-forwarded-for contains client port Fix #9408 --- packages/backend/src/misc/get-ip-hash.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'packages/backend/src') diff --git a/packages/backend/src/misc/get-ip-hash.ts b/packages/backend/src/misc/get-ip-hash.ts index 379325bb13..70e61aef8c 100644 --- a/packages/backend/src/misc/get-ip-hash.ts +++ b/packages/backend/src/misc/get-ip-hash.ts @@ -1,9 +1,14 @@ import IPCIDR from 'ip-cidr'; export function getIpHash(ip: string) { - // because a single person may control many IPv6 addresses, - // only a /64 subnet prefix of any IP will be taken into account. - // (this means for IPv4 the entire address is used) - const prefix = IPCIDR.createAddress(ip).mask(64); - return 'ip-' + BigInt('0b' + prefix).toString(36); + try { + // because a single person may control many IPv6 addresses, + // only a /64 subnet prefix of any IP will be taken into account. + // (this means for IPv4 the entire address is used) + const prefix = IPCIDR.createAddress(ip).mask(64); + return 'ip-' + BigInt('0b' + prefix).toString(36); + } catch (e) { + const prefix = IPCIDR.createAddress(ip.replace(/:[0-9]+$/, '')).mask(64); + return 'ip-' + BigInt('0b' + prefix).toString(36); + } } -- cgit v1.2.3-freya From 520ed8cb4d04004717e25185a8805e05ece413d7 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 25 Jan 2023 11:18:16 +0900 Subject: refactor(server): fix type errors --- packages/backend/src/core/AntennaService.ts | 10 ++++++++-- packages/backend/src/core/RoleService.ts | 26 ++++++++++++++++---------- 2 files changed, 24 insertions(+), 12 deletions(-) (limited to 'packages/backend/src') diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index be755f7dab..7db8c43ea5 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -77,10 +77,16 @@ export class AntennaService implements OnApplicationShutdown { const { type, body } = obj.message as StreamMessages['internal']['payload']; switch (type) { case 'antennaCreated': - this.antennas.push(body); + this.antennas.push({ + ...body, + createdAt: new Date(body.createdAt), + }); break; case 'antennaUpdated': - this.antennas[this.antennas.findIndex(a => a.id === body.id)] = body; + this.antennas[this.antennas.findIndex(a => a.id === body.id)] = { + ...body, + createdAt: new Date(body.createdAt), + }; break; case 'antennaDeleted': this.antennas = this.antennas.filter(a => a.id !== body.id); diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index c0f5eae3d7..f8f9231cdd 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -91,10 +91,12 @@ export class RoleService implements OnApplicationShutdown { case 'roleCreated': { const cached = this.rolesCache.get(null); if (cached) { - body.createdAt = new Date(body.createdAt); - body.updatedAt = new Date(body.updatedAt); - body.lastUsedAt = new Date(body.lastUsedAt); - cached.push(body); + cached.push({ + ...body, + createdAt: new Date(body.createdAt), + updatedAt: new Date(body.updatedAt), + lastUsedAt: new Date(body.lastUsedAt), + }); } break; } @@ -103,10 +105,12 @@ export class RoleService implements OnApplicationShutdown { if (cached) { const i = cached.findIndex(x => x.id === body.id); if (i > -1) { - body.createdAt = new Date(body.createdAt); - body.updatedAt = new Date(body.updatedAt); - body.lastUsedAt = new Date(body.lastUsedAt); - cached[i] = body; + cached[i] = { + ...body, + createdAt: new Date(body.createdAt), + updatedAt: new Date(body.updatedAt), + lastUsedAt: new Date(body.lastUsedAt), + }; } } break; @@ -121,8 +125,10 @@ export class RoleService implements OnApplicationShutdown { case 'userRoleAssigned': { const cached = this.roleAssignmentByUserIdCache.get(body.userId); if (cached) { - body.createdAt = new Date(body.createdAt); - cached.push(body); + cached.push({ + ...body, + createdAt: new Date(body.createdAt), + }); } break; } -- cgit v1.2.3-freya From 2a2e8d0cf6a83c4a10e3a62331d6fbd3adb416a6 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 25 Jan 2023 11:23:57 +0900 Subject: refactor(server): fix type errors --- packages/backend/src/core/activitypub/ApRendererService.ts | 2 +- packages/backend/src/core/activitypub/models/ApPersonService.ts | 8 ++++---- packages/backend/src/server/api/stream/types.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) (limited to 'packages/backend/src') diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 29f216aa1d..91a2767e69 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -21,11 +21,11 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import type { UserKeypair } from '@/models/entities/UserKeypair.js'; import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js'; +import { bindThis } from '@/decorators.js'; import { LdSignatureService } from './LdSignatureService.js'; import { ApMfmService } from './ApMfmService.js'; import type { IActivity, IObject } from './type.js'; import type { IIdentifier } from './models/identifier.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ApRendererService { diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index e08f33c906..f86b5e6f96 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -566,22 +566,22 @@ export class ApPersonService implements OnModuleInit { this.logger.info(`Updating the featured: ${user.uri}`); - if (resolver == null) resolver = this.apResolverService.createResolver(); + const _resolver = resolver ?? this.apResolverService.createResolver(); // Resolve to (Ordered)Collection Object - const collection = await resolver.resolveCollection(user.featured); + const collection = await _resolver.resolveCollection(user.featured); if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection'); // Resolve to Object(may be Note) arrays const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems; - const items = await Promise.all(toArray(unresolvedItems).map(x => resolver.resolve(x))); + const items = await Promise.all(toArray(unresolvedItems).map(x => _resolver.resolve(x))); // Resolve and regist Notes const limit = promiseLimit(2); const featuredNotes = await Promise.all(items .filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも .slice(0, 5) - .map(item => limit(() => this.apNoteService.resolveNote(item, resolver)))); + .map(item => limit(() => this.apNoteService.resolveNote(item, _resolver)))); await this.db.transaction(async transactionalEntityManager => { await transactionalEntityManager.delete(UserNotePining, { userId: user.id }); diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index a442529bb3..ef65b27ddc 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -30,7 +30,7 @@ export interface InternalStreamTypes { remoteUserUpdated: Serialized<{ id: User['id']; }>; follow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>; unfollow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>; - policiesUpdated: Serialized; + policiesUpdated: Serialized; roleCreated: Serialized; roleDeleted: Serialized; roleUpdated: Serialized; -- cgit v1.2.3-freya From d4fb201d05fc4e0da85f21eece7cc6baf30b6d05 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 25 Jan 2023 12:00:04 +0900 Subject: fix(server): node-fetchおよびgotを使う以前の実装に戻す MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit see #9710 --- packages/backend/package.json | 3 +- packages/backend/src/core/CaptchaService.ts | 17 +- packages/backend/src/core/DownloadService.ts | 76 +++-- .../src/core/FetchInstanceMetadataService.ts | 5 +- packages/backend/src/core/HttpRequestService.ts | 377 +++++---------------- packages/backend/src/core/S3Service.ts | 2 +- .../src/core/activitypub/ApRequestService.ts | 35 +- .../src/core/activitypub/ApResolverService.ts | 10 +- .../src/core/activitypub/LdSignatureService.ts | 19 +- .../processors/WebhookDeliverProcessorService.ts | 39 +-- .../backend/src/server/api/endpoints/fetch-rss.ts | 17 +- .../src/server/api/endpoints/notes/translate.ts | 22 +- pnpm-lock.yaml | 4 +- 13 files changed, 201 insertions(+), 425 deletions(-) (limited to 'packages/backend/src') diff --git a/packages/backend/package.json b/packages/backend/package.json index 6cac949ea7..586b95f2e5 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -63,7 +63,7 @@ "file-type": "18.2.0", "fluent-ffmpeg": "2.1.2", "form-data": "^4.0.0", - "got": "12.5.3", + "got": "^12.5.3", "hpagent": "1.2.0", "ioredis": "4.28.5", "ip-cidr": "3.0.11", @@ -119,7 +119,6 @@ "typeorm": "0.3.11", "typescript": "4.9.4", "ulid": "2.3.0", - "undici": "^5.16.0", "unzipper": "0.10.11", "uuid": "9.0.0", "vary": "1.1.2", diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index 1e98914052..c8428a26b0 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -21,18 +21,13 @@ export class CaptchaService { response, }); - const res = await this.httpRequestService.fetch( - url, - { - method: 'POST', - body: params, + const res = await this.httpRequestService.send(url, { + method: 'POST', + body: JSON.stringify(params), + headers: { + 'Content-Type': 'application/json', }, - { - noOkError: true, - } - ).catch(err => { - throw `${err.message ?? err}`; - }); + }, { throwErrorWhenResponseNotOk: false }); if (!res.ok) { throw `${res.status}`; diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index c5b2bcaef4..a971e06fd8 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -5,10 +5,10 @@ import { Inject, Injectable } from '@nestjs/common'; import IPCIDR from 'ip-cidr'; import PrivateIp from 'private-ip'; import chalk from 'chalk'; -import { buildConnector } from 'undici'; +import got, * as Got from 'got'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; import { createTemp } from '@/misc/create-temp.js'; import { StatusError } from '@/misc/status-error.js'; import { LoggerService } from '@/core/LoggerService.js'; @@ -20,7 +20,6 @@ import { bindThis } from '@/decorators.js'; @Injectable() export class DownloadService { private logger: Logger; - private undiciFetcher: UndiciFetcher; constructor( @Inject(DI.config) @@ -30,21 +29,6 @@ export class DownloadService { private loggerService: LoggerService, ) { this.logger = this.loggerService.getLogger('download'); - - this.undiciFetcher = this.httpRequestService.createFetcher({ - connect: process.env.NODE_ENV === 'development' ? - this.httpRequestService.clientDefaults.connect - : - this.httpRequestService.getConnectorWithIpCheck( - buildConnector({ - ...this.httpRequestService.clientDefaults.connect, - }), - (ip) => !this.isPrivateIp(ip), - ), - bodyTimeout: 30 * 1000, - }, { - connect: this.httpRequestService.clientDefaults.connect, - }, this.logger); } @bindThis @@ -55,13 +39,59 @@ export class DownloadService { const operationTimeout = 60 * 1000; const maxSize = this.config.maxFileSize ?? 262144000; - const response = await this.undiciFetcher.fetch(url); + const req = got.stream(url, { + headers: { + 'User-Agent': this.config.userAgent, + }, + timeout: { + lookup: timeout, + connect: timeout, + secureConnect: timeout, + socket: timeout, // read timeout + response: timeout, + send: timeout, + request: operationTimeout, // whole operation timeout + }, + agent: { + http: this.httpRequestService.httpAgent, + https: this.httpRequestService.httpsAgent, + }, + http2: false, // default + retry: { + limit: 0, + }, + }).on('response', (res: Got.Response) => { + if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) { + if (this.isPrivateIp(res.ip)) { + this.logger.warn(`Blocked address: ${res.ip}`); + req.destroy(); + } + } - if (response.body === null) { - throw new StatusError('No body', 400, 'No body'); - } + const contentLength = res.headers['content-length']; + if (contentLength != null) { + const size = Number(contentLength); + if (size > maxSize) { + this.logger.warn(`maxSize exceeded (${size} > ${maxSize}) on response`); + req.destroy(); + } + } + }).on('downloadProgress', (progress: Got.Progress) => { + if (progress.transferred > maxSize) { + this.logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`); + req.destroy(); + } + }); - await pipeline(stream.Readable.fromWeb(response.body), fs.createWriteStream(path)); + try { + await pipeline(req, fs.createWriteStream(path)); + } catch (e) { + if (e instanceof Got.HTTPError) { + throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage); + } else { + throw e; + } + } this.logger.succ(`Download finished: ${chalk.cyan(url)}`); } diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index cb9d099a22..35f30deeb4 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -2,6 +2,7 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import { JSDOM } from 'jsdom'; import tinycolor from 'tinycolor2'; +import fetch from 'node-fetch'; import type { Instance } from '@/models/entities/Instance.js'; import type { InstancesRepository } from '@/models/index.js'; import { AppLockService } from '@/core/AppLockService.js'; @@ -190,7 +191,9 @@ export class FetchInstanceMetadataService { const faviconUrl = url + '/favicon.ico'; - const favicon = await this.httpRequestService.fetch(faviconUrl, {}, { noOkError: true }); + const favicon = await this.httpRequestService.send(faviconUrl, { + method: 'HEAD', + }, { throwErrorWhenResponseNotOk: false }); if (favicon.ok) { return faviconUrl; diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index cd859d0023..baf74acfa6 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -1,284 +1,67 @@ import * as http from 'node:http'; import * as https from 'node:https'; -import { LookupFunction } from 'node:net'; import CacheableLookup from 'cacheable-lookup'; +import fetch from 'node-fetch'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { Inject, Injectable } from '@nestjs/common'; -import * as undici from 'undici'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { StatusError } from '@/misc/status-error.js'; import { bindThis } from '@/decorators.js'; -import { LoggerService } from '@/core/LoggerService.js'; -import type Logger from '@/logger.js'; +import type { Response } from 'node-fetch'; +import type { URL } from 'node:url'; -// true to allow, false to deny -export type IpChecker = (ip: string) => boolean; - -/* - * Child class to create and save Agent for fetch. - * You should construct this when you want - * to change timeout, size limit, socket connect function, etc. - */ -export class UndiciFetcher { +@Injectable() +export class HttpRequestService { /** - * Get http non-proxy agent (undici) + * Get http non-proxy agent */ - public nonProxiedAgent: undici.Agent; + private http: http.Agent; /** - * Get http proxy or non-proxy agent (undici) + * Get https non-proxy agent */ - public agent: undici.ProxyAgent | undici.Agent; - - private proxyBypassHosts: string[]; - private userAgent: string | undefined; - - private logger: Logger | undefined; - - constructor( - args: { - agentOptions: undici.Agent.Options; - proxy?: { - uri: string; - options?: undici.Agent.Options; // Override of agentOptions - }, - proxyBypassHosts?: string[]; - userAgent?: string; - }, - logger?: Logger, - ) { - this.logger = logger; - this.logger?.debug('UndiciFetcher constructor', args); - - this.proxyBypassHosts = args.proxyBypassHosts ?? []; - this.userAgent = args.userAgent; - - this.nonProxiedAgent = new undici.Agent({ - ...args.agentOptions, - connect: (process.env.NODE_ENV !== 'production' && typeof args.agentOptions.connect !== 'function') - ? (options, cb) => { - // Custom connector for debug - undici.buildConnector(args.agentOptions.connect as undici.buildConnector.BuildOptions)(options, (err, socket) => { - this.logger?.debug('Socket connector called', socket); - if (err) { - this.logger?.debug('Socket error', err); - cb(new Error(`Error while socket connecting\n${err}`), null); - return; - } - this.logger?.debug(`Socket connected: port ${socket.localPort} => remote ${socket.remoteAddress}`); - cb(null, socket); - }); - } : args.agentOptions.connect, - }); - - this.agent = args.proxy - ? new undici.ProxyAgent({ - ...args.agentOptions, - ...args.proxy.options, - - uri: args.proxy.uri, - - connect: (process.env.NODE_ENV !== 'production' && typeof (args.proxy.options?.connect ?? args.agentOptions.connect) !== 'function') - ? (options, cb) => { - // Custom connector for debug - undici.buildConnector((args.proxy?.options?.connect ?? args.agentOptions.connect) as undici.buildConnector.BuildOptions)(options, (err, socket) => { - this.logger?.debug('Socket connector called (secure)', socket); - if (err) { - this.logger?.debug('Socket error', err); - cb(new Error(`Error while socket connecting\n${err}`), null); - return; - } - this.logger?.debug(`Socket connected (secure): port ${socket.localPort} => remote ${socket.remoteAddress}`); - cb(null, socket); - }); - } : (args.proxy.options?.connect ?? args.agentOptions.connect), - }) - : this.nonProxiedAgent; - } + private https: https.Agent; /** - * Get agent by URL - * @param url URL - * @param bypassProxy Allways bypass proxy + * Get http proxy or non-proxy agent */ - @bindThis - public getAgentByUrl(url: URL, bypassProxy = false): undici.Agent | undici.ProxyAgent { - if (bypassProxy || this.proxyBypassHosts.includes(url.hostname)) { - return this.nonProxiedAgent; - } else { - return this.agent; - } - } - - @bindThis - public async fetch( - url: string | URL, - options: undici.RequestInit = {}, - privateOptions: { noOkError?: boolean; bypassProxy?: boolean; } = { noOkError: false, bypassProxy: false }, - ): Promise { - const res = await undici.fetch(url, { - dispatcher: this.getAgentByUrl(new URL(url), privateOptions.bypassProxy), - ...options, - headers: { - 'User-Agent': this.userAgent ?? '', - ...(options.headers ?? {}), - }, - }).catch((err) => { - this.logger?.error(`fetch error to ${typeof url === 'string' ? url : url.href}`, err); - throw new StatusError('Resource Unreachable', 500, 'Resource Unreachable'); - }); - if (!res.ok && !privateOptions.noOkError) { - throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText); - } - return res; - } - - @bindThis - public async request( - url: string | URL, - options: { dispatcher?: undici.Dispatcher } & Omit & Partial> = {}, - privateOptions: { noOkError?: boolean; bypassProxy?: boolean; } = { noOkError: false, bypassProxy: false }, - ): Promise { - const res = await undici.request(url, { - dispatcher: this.getAgentByUrl(new URL(url), privateOptions.bypassProxy), - ...options, - headers: { - 'user-agent': this.userAgent ?? '', - ...(options.headers ?? {}), - }, - }).catch((err) => { - this.logger?.error(`fetch error to ${typeof url === 'string' ? url : url.href}`, err); - throw new StatusError('Resource Unreachable', 500, 'Resource Unreachable'); - }); - - if (res.statusCode >= 400) { - throw new StatusError(`${res.statusCode}`, res.statusCode, ''); - } - - return res; - } - - @bindThis - public async getJson(url: string, accept = 'application/json, */*', headers?: Record): Promise { - const { body } = await this.request( - url, - { - headers: Object.assign({ - Accept: accept, - }, headers ?? {}), - }, - ); - - return await body.json() as T; - } - - @bindThis - public async getHtml(url: string, accept = 'text/html, */*', headers?: Record): Promise { - const { body } = await this.request( - url, - { - headers: Object.assign({ - Accept: accept, - }, headers ?? {}), - }, - ); - - return await body.text(); - } -} - -@Injectable() -export class HttpRequestService { - public defaultFetcher: UndiciFetcher; - public fetch: UndiciFetcher['fetch']; - public request: UndiciFetcher['request']; - public getHtml: UndiciFetcher['getHtml']; - public defaultJsonFetcher: UndiciFetcher; - public getJson: UndiciFetcher['getJson']; - - //#region for old http/https, only used in S3Service - // http non-proxy agent - private http: http.Agent; - - // https non-proxy agent - private https: https.Agent; - - // http proxy or non-proxy agent public httpAgent: http.Agent; - // https proxy or non-proxy agent + /** + * Get https proxy or non-proxy agent + */ public httpsAgent: https.Agent; - //#endregion - - public readonly dnsCache: CacheableLookup; - public readonly clientDefaults: undici.Agent.Options; - private maxSockets: number; - - private logger: Logger; constructor( @Inject(DI.config) private config: Config, - private loggerService: LoggerService, ) { - this.logger = this.loggerService.getLogger('http-request'); - - this.dnsCache = new CacheableLookup({ + const cache = new CacheableLookup({ maxTtl: 3600, // 1hours errorTtl: 30, // 30secs lookup: false, // nativeのdns.lookupにfallbackしない }); - - this.clientDefaults = { - keepAliveTimeout: 30 * 1000, - keepAliveMaxTimeout: 10 * 60 * 1000, - keepAliveTimeoutThreshold: 1 * 1000, - strictContentLength: true, - headersTimeout: 10 * 1000, - bodyTimeout: 10 * 1000, - maxHeaderSize: 16364, // default - maxResponseSize: 10 * 1024 * 1024, - maxRedirections: 3, - connect: { - timeout: 10 * 1000, // コネクションが確立するまでのタイムアウト - maxCachedSessions: 300, // TLSセッションのキャッシュ数 https://github.com/nodejs/undici/blob/v5.14.0/lib/core/connect.js#L80 - lookup: this.dnsCache.lookup as LookupFunction, // https://github.com/nodejs/undici/blob/v5.14.0/lib/core/connect.js#L98 - }, - }; - - this.maxSockets = Math.max(64, ((this.config.deliverJobConcurrency ?? 128) / (this.config.clusterLimit ?? 1))); - - this.defaultFetcher = this.createFetcher({}, {}, this.logger); - - this.fetch = this.defaultFetcher.fetch; - this.request = this.defaultFetcher.request; - this.getHtml = this.defaultFetcher.getHtml; - - this.defaultJsonFetcher = this.createFetcher({ - maxResponseSize: 1024 * 256, - }, {}, this.logger); - - this.getJson = this.defaultJsonFetcher.getJson; - - //#region for old http/https, only used in S3Service + this.http = new http.Agent({ keepAlive: true, keepAliveMsecs: 30 * 1000, - lookup: this.dnsCache.lookup, + lookup: cache.lookup, } as http.AgentOptions); this.https = new https.Agent({ keepAlive: true, keepAliveMsecs: 30 * 1000, - lookup: this.dnsCache.lookup, + lookup: cache.lookup, } as https.AgentOptions); - + + const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128); + this.httpAgent = config.proxy ? new HttpProxyAgent({ keepAlive: true, keepAliveMsecs: 30 * 1000, - maxSockets: this.maxSockets, + maxSockets, maxFreeSockets: 256, scheduling: 'lifo', proxy: config.proxy, @@ -289,47 +72,21 @@ export class HttpRequestService { ? new HttpsProxyAgent({ keepAlive: true, keepAliveMsecs: 30 * 1000, - maxSockets: this.maxSockets, + maxSockets, maxFreeSockets: 256, scheduling: 'lifo', proxy: config.proxy, }) : this.https; - //#endregion - } - - @bindThis - private getStandardUndiciFetcherOption(opts: undici.Agent.Options = {}, proxyOpts: undici.Agent.Options = {}) { - return { - agentOptions: { - ...this.clientDefaults, - ...opts, - }, - ...(this.config.proxy ? { - proxy: { - uri: this.config.proxy, - options: { - connections: this.maxSockets, - ...proxyOpts, - }, - }, - } : {}), - userAgent: this.config.userAgent, - }; - } - - @bindThis - public createFetcher(opts: undici.Agent.Options = {}, proxyOpts: undici.Agent.Options = {}, logger: Logger) { - return new UndiciFetcher(this.getStandardUndiciFetcherOption(opts, proxyOpts), logger); } /** - * Get http agent by URL + * Get agent by URL * @param url URL * @param bypassProxy Allways bypass proxy */ @bindThis - public getHttpAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent { + public getAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent { if (bypassProxy || (this.config.proxyBypassHosts || []).includes(url.hostname)) { return url.protocol === 'http:' ? this.http : this.https; } else { @@ -337,37 +94,67 @@ export class HttpRequestService { } } - /** - * check ip - */ @bindThis - public getConnectorWithIpCheck(connector: undici.buildConnector.connector, checkIp: IpChecker): undici.buildConnector.connectorAsync { - return (options, cb) => { - connector(options, (err, socket) => { - this.logger.debug('Socket connector (with ip checker) called', socket); - if (err) { - this.logger.error('Socket error', err); - cb(new Error(`Error while socket connecting\n${err}`), null); - return; - } + public async getJson(url: string, accept = 'application/json, */*', headers?: Record): Promise { + const res = await this.send(url, { + method: 'GET', + headers: Object.assign({ + 'User-Agent': this.config.userAgent, + Accept: accept, + }, headers ?? {}), + timeout: 5000, + size: 1024 * 256, + }); + + return await res.json(); + } + + @bindThis + public async getHtml(url: string, accept = 'text/html, */*', headers?: Record): Promise { + const res = await this.send(url, { + method: 'GET', + headers: Object.assign({ + 'User-Agent': this.config.userAgent, + Accept: accept, + }, headers ?? {}), + timeout: 5000, + }); + + return await res.text(); + } - if (socket.remoteAddress == undefined) { - this.logger.error('Socket error: remoteAddress is undefined'); - cb(new Error('remoteAddress is undefined (maybe socket destroyed)'), null); - return; - } + @bindThis + public async send(url: string, args: { + method?: string, + body?: string, + headers?: Record, + timeout?: number, + size?: number, + } = {}, extra: { + throwErrorWhenResponseNotOk: boolean; + } = { + throwErrorWhenResponseNotOk: true, + }): Promise { + const timeout = args.timeout ?? 5000; + + const controller = new AbortController(); + setTimeout(() => { + controller.abort(); + }, timeout); + + const res = await fetch(url, { + method: args.method ?? 'GET', + headers: args.headers, + body: args.body, + size: args.size ?? 10 * 1024 * 1024, + agent: (url) => this.getAgentByUrl(url), + signal: controller.signal, + }); - // allow - if (checkIp(socket.remoteAddress)) { - this.logger.debug(`Socket connected (ip ok): ${socket.localPort} => ${socket.remoteAddress}`); - cb(null, socket); - return; - } + if (!res.ok && extra.throwErrorWhenResponseNotOk) { + throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText); + } - this.logger.error('IP is not allowed', socket); - cb(new StatusError('IP is not allowed', 403, 'IP is not allowed'), null); - socket.destroy(); - }); - }; + return res; } } diff --git a/packages/backend/src/core/S3Service.ts b/packages/backend/src/core/S3Service.ts index 930188ce6e..0ce69aaa74 100644 --- a/packages/backend/src/core/S3Service.ts +++ b/packages/backend/src/core/S3Service.ts @@ -33,7 +33,7 @@ export class S3Service { ? false : meta.objectStorageS3ForcePathStyle, httpOptions: { - agent: this.httpRequestService.getHttpAgentByUrl(new URL(u), !meta.objectStorageUseProxy), + agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy), }, }); } diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index db87475c4c..b949f34841 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -5,16 +5,14 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { User } from '@/models/entities/User.js'; import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js'; -import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; -import type { Dispatcher } from 'undici'; -import { DevNull } from '@/misc/dev-null.js'; type Request = { url: string; - method: Dispatcher.HttpMethod; + method: string; headers: Record; }; @@ -32,7 +30,6 @@ type PrivateKey = { @Injectable() export class ApRequestService { - private undiciFetcher: UndiciFetcher; private logger: Logger; constructor( @@ -44,9 +41,6 @@ export class ApRequestService { private loggerService: LoggerService, ) { this.logger = this.loggerService.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる - this.undiciFetcher = this.httpRequestService.createFetcher({ - maxRedirections: 0, - }, {}, this.logger); } @bindThis @@ -165,15 +159,11 @@ export class ApRequestService { }, }); - const response = await this.undiciFetcher.request( - url, - { - method: req.request.method, - headers: req.request.headers, - body, - }, - ); - response.body.pipe(new DevNull()); + await this.httpRequestService.send(url, { + method: req.request.method, + headers: req.request.headers, + body, + }); } /** @@ -195,13 +185,10 @@ export class ApRequestService { }, }); - const res = await this.httpRequestService.fetch( - url, - { - method: req.request.method, - headers: req.request.headers, - }, - ); + const res = await this.httpRequestService.send(url, { + method: req.request.method, + headers: req.request.headers, + }); return await res.json(); } diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index ca7760af81..8ead29a5f2 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -4,7 +4,7 @@ import { InstanceActorService } from '@/core/InstanceActorService.js'; import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; -import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; @@ -19,7 +19,6 @@ import type { IObject, ICollection, IOrderedCollection } from './type.js'; export class Resolver { private history: Set; private user?: ILocalUser; - private undiciFetcher: UndiciFetcher; private logger: Logger; constructor( @@ -39,10 +38,7 @@ export class Resolver { private recursionLimit = 100, ) { this.history = new Set(); - this.logger = this.loggerService?.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる - this.undiciFetcher = this.httpRequestService.createFetcher({ - maxRedirections: 0, - }, {}, this.logger); + this.logger = this.loggerService.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる } @bindThis @@ -106,7 +102,7 @@ export class Resolver { const object = (this.user ? await this.apRequestService.signedGet(value, this.user) as IObject - : await this.undiciFetcher.getJson(value, 'application/activity+json, application/ld+json')); + : await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject; if (object == null || ( Array.isArray(object['@context']) ? diff --git a/packages/backend/src/core/activitypub/LdSignatureService.ts b/packages/backend/src/core/activitypub/LdSignatureService.ts index 4e4b7dce2d..5a1e01574d 100644 --- a/packages/backend/src/core/activitypub/LdSignatureService.ts +++ b/packages/backend/src/core/activitypub/LdSignatureService.ts @@ -9,7 +9,7 @@ import { CONTEXTS } from './misc/contexts.js'; class LdSignature { public debug = false; public preLoad = true; - public loderTimeout = 10 * 1000; + public loderTimeout = 5000; constructor( private httpRequestService: HttpRequestService, @@ -115,19 +115,12 @@ class LdSignature { @bindThis private async fetchDocument(url: string) { - const json = await this.httpRequestService.fetch( - url, - { - headers: { - Accept: 'application/ld+json, application/json', - }, - // TODO - //timeout: this.loderTimeout, + const json = await this.httpRequestService.send(url, { + headers: { + Accept: 'application/ld+json, application/json', }, - { - noOkError: true, - } - ).then(res => { + timeout: this.loderTimeout, + }, { throwErrorWhenResponseNotOk: false }).then(res => { if (!res.ok) { throw `${res.status} ${res.statusText}`; } else { diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts index f0543a5ed1..57210b25d2 100644 --- a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts @@ -6,10 +6,10 @@ import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { StatusError } from '@/misc/status-error.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { WebhookDeliverJobData } from '../types.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class WebhookDeliverProcessorService { @@ -33,26 +33,23 @@ export class WebhookDeliverProcessorService { try { this.logger.debug(`delivering ${job.data.webhookId}`); - const res = await this.httpRequestService.fetch( - job.data.to, - { - method: 'POST', - headers: { - 'User-Agent': 'Misskey-Hooks', - 'X-Misskey-Host': this.config.host, - 'X-Misskey-Hook-Id': job.data.webhookId, - 'X-Misskey-Hook-Secret': job.data.secret, - }, - body: JSON.stringify({ - hookId: job.data.webhookId, - userId: job.data.userId, - eventId: job.data.eventId, - createdAt: job.data.createdAt, - type: job.data.type, - body: job.data.content, - }), - } - ); + const res = await this.httpRequestService.send(job.data.to, { + method: 'POST', + headers: { + 'User-Agent': 'Misskey-Hooks', + 'X-Misskey-Host': this.config.host, + 'X-Misskey-Hook-Id': job.data.webhookId, + 'X-Misskey-Hook-Secret': job.data.secret, + }, + body: JSON.stringify({ + hookId: job.data.webhookId, + userId: job.data.userId, + eventId: job.data.eventId, + createdAt: job.data.createdAt, + type: job.data.type, + body: job.data.content, + }), + }); this.webhooksRepository.update({ id: job.data.webhookId }, { latestSentAt: new Date(), diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index ae6a87513d..5849d3111f 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -33,16 +33,13 @@ export default class extends Endpoint { private httpRequestService: HttpRequestService, ) { super(meta, paramDef, async (ps, me) => { - const res = await this.httpRequestService.fetch( - ps.url, - { - method: 'GET', - headers: { - Accept: 'application/rss+xml, */*', - }, - // timeout: 5000, - } - ); + const res = await this.httpRequestService.send(ps.url, { + method: 'GET', + headers: { + Accept: 'application/rss+xml, */*', + }, + timeout: 5000, + }); const text = await res.text(); diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index ab19771676..66655234a1 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -7,8 +7,8 @@ import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['notes'], @@ -83,20 +83,14 @@ export default class extends Endpoint { const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; - const res = await this.httpRequestService.fetch( - endpoint, - { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Accept: 'application/json, */*', - }, - body: params.toString(), + const res = await this.httpRequestService.send(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json, */*', }, - { - noOkError: false, - } - ); + body: params.toString(), + }); const json = (await res.json()) as { translations: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3fdaf1449a..bfe98f9a84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,7 +142,7 @@ importers: file-type: 18.2.0 fluent-ffmpeg: 2.1.2 form-data: ^4.0.0 - got: 12.5.3 + got: ^12.5.3 hpagent: 1.2.0 ioredis: 4.28.5 ip-cidr: 3.0.11 @@ -201,7 +201,6 @@ importers: typeorm: 0.3.11 typescript: 4.9.4 ulid: 2.3.0 - undici: ^5.16.0 unzipper: 0.10.11 uuid: 9.0.0 vary: 1.1.2 @@ -306,7 +305,6 @@ importers: typeorm: 0.3.11_ioredis@4.28.5+pg@8.8.0 typescript: 4.9.4 ulid: 2.3.0 - undici: 5.16.0 unzipper: 0.10.11 uuid: 9.0.0 vary: 1.1.2 -- cgit v1.2.3-freya From 945c50db1f2e2e2a3612ee327adc5575da4b5ccb Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 25 Jan 2023 12:31:03 +0900 Subject: Update ApRequestService.ts --- packages/backend/src/core/activitypub/ApRequestService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'packages/backend/src') diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index b949f34841..bfd53dfabf 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -40,7 +40,8 @@ export class ApRequestService { private httpRequestService: HttpRequestService, private loggerService: LoggerService, ) { - this.logger = this.loggerService.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる } @bindThis -- cgit v1.2.3-freya From 3ba5541a6623fc1256c51aade666fd5cdef5c073 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 25 Jan 2023 12:36:39 +0900 Subject: Update ApResolverService.ts --- packages/backend/src/core/activitypub/ApResolverService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'packages/backend/src') diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 8ead29a5f2..7e962cb127 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -38,7 +38,8 @@ export class Resolver { private recursionLimit = 100, ) { this.history = new Set(); - this.logger = this.loggerService.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + this.logger = this.loggerService?.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる } @bindThis -- cgit v1.2.3-freya From 5ffa106cc1ada8c0680a57cef027114232258870 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 25 Jan 2023 19:34:10 +0900 Subject: サードパーティからも自身のロールを確認できるように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close #9700 --- packages/backend/src/core/entities/UserEntityService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/backend/src') diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 34b523e143..f532b5bf6e 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -496,10 +496,10 @@ export class UserEntityService implements OnModuleInit { showTimelineReplies: user.showTimelineReplies ?? falsy, achievements: profile!.achievements, loggedInDays: profile!.loggedInDates.length, + policies: this.roleService.getUserPolicies(user.id), } : {}), ...(opts.includeSecrets ? { - policies: this.roleService.getUserPolicies(user.id), email: profile!.email, emailVerified: profile!.emailVerified, securityKeysList: profile!.twoFactorEnabled -- cgit v1.2.3-freya