wip
This commit is contained in:
parent
d91a4e3dec
commit
5f5aa599ba
|
@ -6,8 +6,11 @@
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as stream from 'node:stream/promises';
|
import * as stream from 'node:stream/promises';
|
||||||
|
import { Transform } from 'node:stream';
|
||||||
|
import { type MultipartFile } from '@fastify/multipart';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { AttachmentFile } from '@/server/api/endpoint-base.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||||
|
@ -200,18 +203,6 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [path, cleanup] = await createTemp();
|
|
||||||
await stream.pipeline(multipartData.file, fs.createWriteStream(path));
|
|
||||||
|
|
||||||
// ファイルサイズが制限を超えていた場合
|
|
||||||
// なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
|
|
||||||
if (multipartData.file.truncated) {
|
|
||||||
cleanup();
|
|
||||||
reply.code(413);
|
|
||||||
reply.send();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = {} as Record<string, unknown>;
|
const fields = {} as Record<string, unknown>;
|
||||||
for (const [k, v] of Object.entries(multipartData.fields)) {
|
for (const [k, v] of Object.entries(multipartData.fields)) {
|
||||||
fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
|
fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
|
||||||
|
@ -226,10 +217,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.authenticateService.authenticate(token).then(([user, app]) => {
|
this.authenticateService.authenticate(token).then(([user, app]) => {
|
||||||
this.call(endpoint, user, app, fields, {
|
this.call(endpoint, user, app, fields, multipartData, request).then((res) => {
|
||||||
name: multipartData.filename,
|
|
||||||
path: path,
|
|
||||||
}, request).then((res) => {
|
|
||||||
this.send(reply, res);
|
this.send(reply, res);
|
||||||
}).catch((err: ApiError) => {
|
}).catch((err: ApiError) => {
|
||||||
this.#sendApiError(reply, err);
|
this.#sendApiError(reply, err);
|
||||||
|
@ -294,10 +282,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
user: MiLocalUser | null | undefined,
|
user: MiLocalUser | null | undefined,
|
||||||
token: MiAccessToken | null | undefined,
|
token: MiAccessToken | null | undefined,
|
||||||
data: any,
|
data: any,
|
||||||
file: {
|
multipartFile: MultipartFile | null,
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
} | null,
|
|
||||||
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
||||||
) {
|
) {
|
||||||
const isSecure = user != null && token == null;
|
const isSecure = user != null && token == null;
|
||||||
|
@ -435,18 +420,86 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let attachmentFile: AttachmentFile | null = null;
|
||||||
|
let cleanup = () => {};
|
||||||
|
if (ep.meta.requireFile && request.method === 'POST' && multipartFile) {
|
||||||
|
const result = await this.handleAttachmentFile(request, multipartFile);
|
||||||
|
attachmentFile = result.attachmentFile;
|
||||||
|
cleanup = result.cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
// API invoking
|
// API invoking
|
||||||
if (this.config.sentryForBackend) {
|
if (this.config.sentryForBackend) {
|
||||||
return await Sentry.startSpan({
|
return await Sentry.startSpan({
|
||||||
name: 'API: ' + ep.name,
|
name: 'API: ' + ep.name,
|
||||||
}, () => ep.exec(data, user, token, file, request.ip, request.headers)
|
}, () => {
|
||||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
|
return ep.exec(data, user, token, attachmentFile, request.ip, request.headers)
|
||||||
|
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id))
|
||||||
|
.finally(() => cleanup());
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return await ep.exec(data, user, token, file, request.ip, request.headers)
|
return await ep.exec(data, user, token, attachmentFile, request.ip, request.headers)
|
||||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id));
|
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id))
|
||||||
|
.finally(() => cleanup());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async handleAttachmentFile(
|
||||||
|
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
||||||
|
multipartFile: MultipartFile,
|
||||||
|
) {
|
||||||
|
function createTooLongError() {
|
||||||
|
return new ApiError({
|
||||||
|
httpStatusCode: 413,
|
||||||
|
kind: 'client',
|
||||||
|
message: 'File size is too large.',
|
||||||
|
code: 'FILE_SIZE_TOO_LARGE',
|
||||||
|
id: 'ff827ce8-9b4b-4808-8511-422222a3362f',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLimitStream(limit: number) {
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
return new Transform({
|
||||||
|
transform(chunk, encoding, callback) {
|
||||||
|
total += chunk.length;
|
||||||
|
if (total > limit) {
|
||||||
|
callback(createTooLongError());
|
||||||
|
} else {
|
||||||
|
callback(null, chunk);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileSizeLimit = this.config.maxFileSize;
|
||||||
|
if (request.headers['content-length'] && Number(request.headers['content-length']) > fileSizeLimit) {
|
||||||
|
throw createTooLongError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [path, cleanup] = await createTemp();
|
||||||
|
await stream.pipeline(multipartFile.file, createLimitStream(fileSizeLimit), fs.createWriteStream(path));
|
||||||
|
|
||||||
|
// ファイルサイズが制限を超えていた場合
|
||||||
|
// なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
|
||||||
|
if (multipartFile.file.truncated) {
|
||||||
|
cleanup();
|
||||||
|
throw createTooLongError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachmentFile = {
|
||||||
|
name: multipartFile.filename,
|
||||||
|
path,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
attachmentFile,
|
||||||
|
cleanup,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
clearInterval(this.userIpHistoriesClearIntervalId);
|
clearInterval(this.userIpHistoriesClearIntervalId);
|
||||||
|
|
|
@ -21,23 +21,23 @@ ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
|
||||||
|
|
||||||
export type Response = Record<string, any> | void;
|
export type Response = Record<string, any> | void;
|
||||||
|
|
||||||
type File = {
|
export type AttachmentFile = {
|
||||||
name: string | null;
|
name: string | null;
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: paramsの型をT['params']のスキーマ定義から推論する
|
// TODO: paramsの型をT['params']のスキーマ定義から推論する
|
||||||
type Executor<T extends IEndpointMeta, Ps extends Schema> =
|
type Executor<T extends IEndpointMeta, Ps extends Schema> =
|
||||||
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
|
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
|
||||||
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
|
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
|
||||||
|
|
||||||
export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
|
export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
|
||||||
public exec: (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
|
public exec: (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
|
||||||
|
|
||||||
constructor(meta: T, paramDef: Ps, cb: Executor<T, Ps>) {
|
constructor(meta: T, paramDef: Ps, cb: Executor<T, Ps>) {
|
||||||
const validate = ajv.compile(paramDef);
|
const validate = ajv.compile(paramDef);
|
||||||
|
|
||||||
this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
|
this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, ip?: string | null, headers?: Record<string, string> | null) => {
|
||||||
let cleanup: undefined | (() => void) = undefined;
|
let cleanup: undefined | (() => void) = undefined;
|
||||||
|
|
||||||
if (meta.requireFile) {
|
if (meta.requireFile) {
|
||||||
|
|
Loading…
Reference in New Issue