summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-07-02 15:12:11 +0900
committerGitHub <noreply@github.com>2022-07-02 15:12:11 +0900
commiteccc90c843f63b2dc08d8fbf80e4f54a601e477d (patch)
treea26e23b56d711806862bdfaafeb9b826d25465a8 /packages/backend/src/server/api
parentrefactor(client): refactoring (diff)
downloadsharkey-eccc90c843f63b2dc08d8fbf80e4f54a601e477d.tar.gz
sharkey-eccc90c843f63b2dc08d8fbf80e4f54a601e477d.tar.bz2
sharkey-eccc90c843f63b2dc08d8fbf80e4f54a601e477d.zip
feat: Log user ips (#8872)
* wip * store ip and headers * Update admin-file.vue * require admin for view ip/headers * IP (recent) 消した * admin必須 * opt in * clean ips periodically * respect logging setting in drive/files/create
Diffstat (limited to 'packages/backend/src/server/api')
-rw-r--r--packages/backend/src/server/api/api-handler.ts34
-rw-r--r--packages/backend/src/server/api/call.ts2
-rw-r--r--packages/backend/src/server/api/define.ts34
-rw-r--r--packages/backend/src/server/api/endpoints.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/drive/show-file.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/admin/get-user-ips.ts31
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/create.ts21
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts6
10 files changed, 125 insertions, 27 deletions
diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts
index c22c868c80..34ff970b4c 100644
--- a/packages/backend/src/server/api/api-handler.ts
+++ b/packages/backend/src/server/api/api-handler.ts
@@ -1,10 +1,19 @@
import Koa from 'koa';
+import { User } from '@/models/entities/user.js';
+import { UserIps } from '@/models/index.js';
+import { fetchMeta } from '@/misc/fetch-meta.js';
import { IEndpoint } from './endpoints.js';
import authenticate, { AuthenticationError } from './authenticate.js';
import call from './call.js';
import { ApiError } from './error.js';
+const userIpHistories = new Map<User['id'], Set<string>>();
+
+setInterval(() => {
+ userIpHistories.clear();
+}, 1000 * 60 * 60);
+
export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res) => {
const body = ctx.is('multipart/form-data')
? (ctx.request as any).body
@@ -44,6 +53,31 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res
}).catch((e: ApiError) => {
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
});
+
+ // Log IP
+ if (user) {
+ fetchMeta().then(meta => {
+ if (!meta.enableIpLogging) return;
+ const ip = ctx.ip;
+ const ips = userIpHistories.get(user.id);
+ if (ips == null || !ips.has(ip)) {
+ if (ips == null) {
+ userIpHistories.set(user.id, new Set([ip]));
+ } else {
+ ips.add(ip);
+ }
+
+ try {
+ UserIps.insert({
+ createdAt: new Date(),
+ userId: user.id,
+ ip: ip,
+ });
+ } catch {
+ }
+ }
+ });
+ }
}).catch(e => {
if (e instanceof AuthenticationError) {
reply(403, new ApiError({
diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts
index 75bbc9f908..aa130459a3 100644
--- a/packages/backend/src/server/api/call.ts
+++ b/packages/backend/src/server/api/call.ts
@@ -116,7 +116,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
// API invoking
const before = performance.now();
- return await ep.exec(data, user, token, ctx?.file).catch((e: Error) => {
+ return await ep.exec(data, user, token, ctx?.file, ctx?.ip, ctx?.headers).catch((e: Error) => {
if (e instanceof ApiError) {
throw e;
} else {
diff --git a/packages/backend/src/server/api/define.ts b/packages/backend/src/server/api/define.ts
index 47dcb44ea8..c1b56b8a83 100644
--- a/packages/backend/src/server/api/define.ts
+++ b/packages/backend/src/server/api/define.ts
@@ -1,16 +1,16 @@
import * as fs from 'node:fs';
import Ajv from 'ajv';
import { CacheableLocalUser, ILocalUser } from '@/models/entities/user.js';
-import { IEndpointMeta } from './endpoints.js';
-import { ApiError } from './error.js';
import { Schema, SchemaType } from '@/misc/schema.js';
import { AccessToken } from '@/models/entities/access-token.js';
+import { IEndpointMeta } from './endpoints.js';
+import { ApiError } from './error.js';
export type Response = Record<string, any> | void;
// TODO: paramsの型をT['params']のスキーマ定義から推論する
type executor<T extends IEndpointMeta, Ps extends Schema> =
- (params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any) =>
+ (params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
const ajv = new Ajv({
@@ -20,23 +20,27 @@ const ajv = new Ajv({
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, paramDef: Ps, cb: executor<T, Ps>)
- : (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => Promise<any> {
+ : (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => Promise<any> {
const validate = ajv.compile(paramDef);
- return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => {
- function cleanup() {
- fs.unlink(file.path, () => {});
- }
+ return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => {
+ let cleanup: undefined | (() => void) = undefined;
+
+ if (meta.requireFile) {
+ cleanup = () => {
+ fs.unlink(file.path, () => {});
+ };
- if (meta.requireFile && file == null) return Promise.reject(new ApiError({
- message: 'File required.',
- code: 'FILE_REQUIRED',
- id: '4267801e-70d1-416a-b011-4ee502885d8b',
- }));
+ if (file == null) return Promise.reject(new ApiError({
+ message: 'File required.',
+ code: 'FILE_REQUIRED',
+ id: '4267801e-70d1-416a-b011-4ee502885d8b',
+ }));
+ }
const valid = validate(params);
if (!valid) {
- if (file) cleanup();
+ if (file) cleanup!();
const errors = validate.errors!;
const err = new ApiError({
@@ -50,6 +54,6 @@ export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, pa
return Promise.reject(err);
}
- return cb(params as SchemaType<Ps>, user, token, file, cleanup);
+ return cb(params as SchemaType<Ps>, user, token, file, cleanup, ip, headers);
};
}
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 1a3fc199dc..f019677542 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -35,6 +35,7 @@ import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/fed
import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
+import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
import * as ep___admin_invite from './endpoints/admin/invite.js';
import * as ep___admin_moderators_add from './endpoints/admin/moderators/add.js';
import * as ep___admin_moderators_remove from './endpoints/admin/moderators/remove.js';
@@ -348,6 +349,7 @@ const eps = [
['admin/federation/update-instance', ep___admin_federation_updateInstance],
['admin/get-index-stats', ep___admin_getIndexStats],
['admin/get-table-stats', ep___admin_getTableStats],
+ ['admin/get-user-ips', ep___admin_getUserIps],
['admin/invite', ep___admin_invite],
['admin/moderators/add', ep___admin_moderators_add],
['admin/moderators/remove', ep___admin_moderators_remove],
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
index 039df74f1b..e9117a23c8 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
@@ -1,6 +1,6 @@
+import { DriveFiles } from '@/models/index.js';
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
-import { DriveFiles } from '@/models/index.js';
export const meta = {
tags: ['admin'],
@@ -184,5 +184,10 @@ export default define(meta, paramDef, async (ps, me) => {
throw new ApiError(meta.errors.noSuchFile);
}
+ if (!me.isAdmin) {
+ delete file.requestIp;
+ delete file.requestHeaders;
+ }
+
return file;
});
diff --git a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
new file mode 100644
index 0000000000..e8b9cb3b09
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
@@ -0,0 +1,31 @@
+import { UserIps } from '@/models/index.js';
+import define from '../../define.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireAdmin: true,
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['userId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+export default define(meta, paramDef, async (ps, me) => {
+ const ips = await UserIps.find({
+ where: { userId: ps.userId },
+ order: { createdAt: 'DESC' },
+ take: 30,
+ });
+
+ return ips.map(x => ({
+ ip: x.ip,
+ createdAt: x.createdAt.toISOString(),
+ }));
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 8d50486ef6..8b71628959 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -1,7 +1,7 @@
import config from '@/config/index.js';
-import define from '../../define.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
+import define from '../../define.js';
export const meta = {
tags: ['meta'],
@@ -304,6 +304,10 @@ export const meta = {
type: 'boolean',
optional: true, nullable: false,
},
+ enableIpLogging: {
+ type: 'boolean',
+ optional: true, nullable: false,
+ },
},
},
} as const;
@@ -360,7 +364,6 @@ export default define(meta, paramDef, async (ps, me) => {
pinnedPages: instance.pinnedPages,
pinnedClipId: instance.pinnedClipId,
cacheRemoteFiles: instance.cacheRemoteFiles,
-
useStarForReactionFallback: instance.useStarForReactionFallback,
pinnedUsers: instance.pinnedUsers,
hiddenTags: instance.hiddenTags,
@@ -397,5 +400,6 @@ export default define(meta, paramDef, async (ps, me) => {
objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
deeplAuthKey: instance.deeplAuthKey,
deeplIsPro: instance.deeplIsPro,
+ enableIpLogging: instance.enableIpLogging,
};
});
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 09e43301b7..4dc4726a29 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -1,8 +1,8 @@
-import define from '../../define.js';
import { Meta } from '@/models/entities/meta.js';
import { insertModerationLog } from '@/services/insert-moderation-log.js';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits.js';
import { db } from '@/db/postgre.js';
+import define from '../../define.js';
export const meta = {
tags: ['admin'],
@@ -96,6 +96,7 @@ export const paramDef = {
objectStorageUseProxy: { type: 'boolean' },
objectStorageSetPublicRead: { type: 'boolean' },
objectStorageS3ForcePathStyle: { type: 'boolean' },
+ enableIpLogging: { type: 'boolean' },
},
required: [],
} as const;
@@ -396,6 +397,10 @@ export default define(meta, paramDef, async (ps, me) => {
set.deeplIsPro = ps.deeplIsPro;
}
+ if (ps.enableIpLogging !== undefined) {
+ set.enableIpLogging = ps.enableIpLogging;
+ }
+
await db.transaction(async transactionalEntityManager => {
const metas = await transactionalEntityManager.find(Meta, {
order: {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts
index 7397fd9ce9..3a76a5d98d 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/create.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts
@@ -1,10 +1,11 @@
import ms from 'ms';
import { addFile } from '@/services/drive/add-file.js';
+import { DriveFiles } from '@/models/index.js';
+import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
+import { fetchMeta } from '@/misc/fetch-meta.js';
import define from '../../../define.js';
import { apiLogger } from '../../../logger.js';
import { ApiError } from '../../../error.js';
-import { DriveFiles } from '@/models/index.js';
-import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
export const meta = {
tags: ['drive'],
@@ -50,7 +51,7 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user, _, file, cleanup) => {
+export default define(meta, paramDef, async (ps, user, _, file, cleanup, ip, headers) => {
// Get 'name' parameter
let name = ps.name || file.originalname;
if (name !== undefined && name !== null) {
@@ -66,9 +67,21 @@ export default define(meta, paramDef, async (ps, user, _, file, cleanup) => {
name = null;
}
+ const meta = await fetchMeta();
+
try {
// Create file
- const driveFile = await addFile({ user, path: file.path, name, comment: ps.comment, folderId: ps.folderId, force: ps.force, sensitive: ps.isSensitive });
+ const driveFile = await addFile({
+ user,
+ path: file.path,
+ name,
+ comment: ps.comment,
+ folderId: ps.folderId,
+ force: ps.force,
+ sensitive: ps.isSensitive,
+ requestIp: meta.enableIpLogging ? ip : null,
+ requestHeaders: meta.enableIpLogging ? headers : null,
+ });
return await DriveFiles.pack(driveFile, { self: true });
} catch (e) {
if (e instanceof Error || typeof e === 'string') {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
index 53f2298f21..eb8071c3c9 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
@@ -1,9 +1,9 @@
import ms from 'ms';
import { uploadFromUrl } from '@/services/drive/upload-from-url.js';
-import define from '../../../define.js';
import { DriveFiles } from '@/models/index.js';
import { publishMainStream } from '@/services/stream.js';
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
+import define from '../../../define.js';
export const meta = {
tags: ['drive'],
@@ -34,8 +34,8 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment }).then(file => {
+export default define(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => {
+ uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment, requestIp: ip, requestHeaders: headers }).then(file => {
DriveFiles.pack(file, { self: true }).then(packedFile => {
publishMainStream(user.id, 'urlUploadFinished', {
marker: ps.marker,