Merge commit from fork

* Tighten security on channels

* Fix main channel

* add comments, improve typing

* fix indent

* fix: missing membership checks in chat-room

* remove unnecessary check in chat-user

* fix

* refactor: use exists

* fix

---------

Co-authored-by: Julia Johannesen <julia@insertdomain.name>
This commit is contained in:
かっこかり
2026-03-09 08:18:14 +09:00
committed by GitHub
parent b361a10c48
commit 06e74508a2
9 changed files with 96 additions and 27 deletions

View File

@@ -52,7 +52,7 @@ export default class Connection {
public token?: MiAccessToken; public token?: MiAccessToken;
private wsConnection: WebSocket.WebSocket; private wsConnection: WebSocket.WebSocket;
public subscriber: StreamEventEmitter; public subscriber: StreamEventEmitter;
private channels: Channel[] = []; private channels: Map<string, Channel> = new Map();
private subscribingNotes: Partial<Record<string, number>> = {}; private subscribingNotes: Partial<Record<string, number>> = {};
public userProfile: MiUserProfile | null = null; public userProfile: MiUserProfile | null = null;
public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {}; public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
@@ -262,7 +262,11 @@ export default class Connection {
*/ */
@bindThis @bindThis
public async connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) { public async connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
if (this.channels.length >= MAX_CHANNELS_PER_CONNECTION) { if (this.channels.has(id)) {
this.disconnectChannel(id);
}
if (this.channels.size >= MAX_CHANNELS_PER_CONNECTION) {
return; return;
} }
@@ -278,8 +282,12 @@ export default class Connection {
} }
// 共有可能チャンネルに接続しようとしていて、かつそのチャンネルに既に接続していたら無意味なので無視 // 共有可能チャンネルに接続しようとしていて、かつそのチャンネルに既に接続していたら無意味なので無視
if (channelConstructor.shouldShare && this.channels.some(c => c.chName === channel)) { if (channelConstructor.shouldShare) {
return; for (const c of this.channels.values()) {
if (c.chName === channel) {
return;
}
}
} }
const contextId = ContextIdFactory.create(); const contextId = ContextIdFactory.create();
@@ -289,8 +297,13 @@ export default class Connection {
}, contextId); }, contextId);
const ch: Channel = await this.moduleRef.create<Channel>(channelConstructor, contextId); const ch: Channel = await this.moduleRef.create<Channel>(channelConstructor, contextId);
this.channels.push(ch); this.channels.set(ch.id, ch);
ch.init(params ?? {}); const valid = await ch.init(params ?? {});
if (typeof valid === 'boolean' && !valid) {
// 初期化処理の結果、接続拒否されたので切断
this.disconnectChannel(id);
return;
}
if (pong) { if (pong) {
this.sendMessageToWs('connected', { this.sendMessageToWs('connected', {
@@ -332,11 +345,11 @@ export default class Connection {
*/ */
@bindThis @bindThis
public disconnectChannel(id: string) { public disconnectChannel(id: string) {
const channel = this.channels.find(c => c.id === id); const channel = this.channels.get(id);
if (channel) { if (channel) {
if (channel.dispose) channel.dispose(); if (channel.dispose) channel.dispose();
this.channels = this.channels.filter(c => c.id !== id); this.channels.delete(id);
} }
} }
@@ -351,7 +364,7 @@ export default class Connection {
if (typeof data.type !== 'string') return; if (typeof data.type !== 'string') return;
if (typeof data.body === 'undefined') return; if (typeof data.body === 'undefined') return;
const channel = this.channels.find(c => c.id === data.id); const channel = this.channels.get(data.id);
if (channel != null && channel.onMessage != null) { if (channel != null && channel.onMessage != null) {
channel.onMessage(data.type, data.body); channel.onMessage(data.type, data.body);
} }
@@ -363,7 +376,7 @@ export default class Connection {
@bindThis @bindThis
public dispose() { public dispose() {
if (this.fetchIntervalId) clearInterval(this.fetchIntervalId); if (this.fetchIntervalId) clearInterval(this.fetchIntervalId);
for (const c of this.channels.filter(c => c.dispose)) { for (const c of this.channels.values()) {
if (c.dispose) c.dispose(); if (c.dispose) c.dispose();
} }
} }

View File

@@ -8,6 +8,7 @@ import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { isUserRelated } from '@/misc/is-user-related.js'; import { isUserRelated } from '@/misc/is-user-related.js';
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js'; import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
import { isChannelRelated } from '@/misc/is-channel-related.js'; import { isChannelRelated } from '@/misc/is-channel-related.js';
import type { Awaitable } from '@/types.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import type Connection from './Connection.js'; import type Connection from './Connection.js';
@@ -141,7 +142,14 @@ export default abstract class Channel {
}); });
} }
public abstract init(params: JsonObject): void; /**
* チャンネルの初期化処理(接続時点での接続可否チェックを兼ねる)
*
* - `void / Promise<void>` を返す場合は、チェックなし
* - `true / Promise<true>` を返す場合は、接続可能
* - `false / Promise<false>` を返す場合は、接続不可(接続を切断)
*/
public abstract init(params: JsonObject): Awaitable<void | boolean>;
public dispose?(): void; public dispose?(): void;

View File

@@ -4,6 +4,8 @@
*/ */
import { Inject, Injectable, Scope } from '@nestjs/common'; import { Inject, Injectable, Scope } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { AntennasRepository } from '@/models/_.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js'; import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@@ -25,6 +27,9 @@ export class AntennaChannel extends Channel {
@Inject(REQUEST) @Inject(REQUEST)
request: ChannelRequest, request: ChannelRequest,
@Inject(DI.antennasRepository)
private antennasReposiotry: AntennasRepository,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService, private noteStreamingHidingService: NoteStreamingHidingService,
) { ) {
@@ -33,12 +38,25 @@ export class AntennaChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: JsonObject) { public async init(params: JsonObject): Promise<boolean> {
if (typeof params.antennaId !== 'string') return; if (typeof params.antennaId !== 'string') return false;
if (!this.user) return false;
this.antennaId = params.antennaId; this.antennaId = params.antennaId;
const antennaExists = await this.antennasReposiotry.exists({
where: {
id: this.antennaId,
userId: this.user.id,
},
});
if (!antennaExists) return false;
// Subscribe stream // Subscribe stream
this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent); this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);
return true;
} }
@bindThis @bindThis

View File

@@ -4,12 +4,14 @@
*/ */
import { Inject, Injectable, Scope } from '@nestjs/common'; import { Inject, Injectable, Scope } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js'; import type { JsonObject } from '@/misc/json-value.js';
import { ChatService } from '@/core/ChatService.js'; import { ChatService } from '@/core/ChatService.js';
import Channel, { type ChannelRequest } from '../channel.js'; import Channel, { type ChannelRequest } from '../channel.js';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import type { ChatRoomsRepository } from '@/models/_.js';
@Injectable({ scope: Scope.TRANSIENT }) @Injectable({ scope: Scope.TRANSIENT })
export class ChatRoomChannel extends Channel { export class ChatRoomChannel extends Channel {
@@ -23,17 +25,31 @@ export class ChatRoomChannel extends Channel {
@Inject(REQUEST) @Inject(REQUEST)
request: ChannelRequest, request: ChannelRequest,
@Inject(DI.chatRoomsRepository)
private chatRoomsRepository: ChatRoomsRepository,
private chatService: ChatService, private chatService: ChatService,
) { ) {
super(request); super(request);
} }
@bindThis @bindThis
public async init(params: JsonObject) { public async init(params: JsonObject): Promise<boolean> {
if (typeof params.roomId !== 'string') return; if (typeof params.roomId !== 'string') return false;
if (!this.user) return false;
this.roomId = params.roomId; this.roomId = params.roomId;
const room = await this.chatRoomsRepository.findOneBy({
id: this.roomId,
});
if (room == null) return false;
if (!(await this.chatService.hasPermissionToViewRoomTimeline(this.user.id, room))) return false;
this.subscriber.on(`chatRoomStream:${this.roomId}`, this.onEvent); this.subscriber.on(`chatRoomStream:${this.roomId}`, this.onEvent);
return true;
} }
@bindThis @bindThis

View File

@@ -29,11 +29,16 @@ export class ChatUserChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: JsonObject) { public async init(params: JsonObject): Promise<boolean> {
if (typeof params.otherId !== 'string') return; if (typeof params.otherId !== 'string') return false;
if (!this.user) return false;
if (params.otherId === this.user.id) return false;
this.otherId = params.otherId; this.otherId = params.otherId;
this.subscriber.on(`chatUserStream:${this.user!.id}-${this.otherId}`, this.onEvent); this.subscriber.on(`chatUserStream:${this.user.id}-${this.otherId}`, this.onEvent);
return true;
} }
@bindThis @bindThis

View File

@@ -32,17 +32,19 @@ export class HashtagChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: JsonObject) { public async init(params: JsonObject): Promise<boolean> {
if (!Array.isArray(params.q)) return; if (!Array.isArray(params.q)) return false;
if (!params.q.every((x): x is string[] => ( if (!params.q.every((x): x is string[] => (
Array.isArray(x) && Array.isArray(x) &&
x.length >= 1 && x.length >= 1 &&
x.every(y => typeof y === 'string') x.every(y => typeof y === 'string')
))) return; ))) return false;
this.q = params.q; this.q = params.q;
// Subscribe stream // Subscribe stream
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
return true;
} }
@bindThis @bindThis

View File

@@ -28,9 +28,10 @@ export class MainChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: JsonObject) { public async init(params: JsonObject): Promise<boolean> {
// Subscribe main stream channel if (!this.user) return false;
this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
this.subscriber.on(`mainStream:${this.user.id}`, async data => {
switch (data.type) { switch (data.type) {
case 'notification': { case 'notification': {
// Ignore notifications from instances the user has muted // Ignore notifications from instances the user has muted
@@ -61,5 +62,7 @@ export class MainChannel extends Channel {
this.send(data.type, data.body); this.send(data.type, data.body);
}); });
return true;
} }
} }

View File

@@ -45,8 +45,8 @@ export class UserListChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: JsonObject) { public async init(params: JsonObject): Promise<boolean> {
if (typeof params.listId !== 'string') return; if (typeof params.listId !== 'string') return false;
this.listId = params.listId; this.listId = params.listId;
this.withFiles = !!(params.withFiles ?? false); this.withFiles = !!(params.withFiles ?? false);
this.withRenotes = !!(params.withRenotes ?? true); this.withRenotes = !!(params.withRenotes ?? true);
@@ -58,7 +58,7 @@ export class UserListChannel extends Channel {
userId: this.user!.id, userId: this.user!.id,
}, },
}); });
if (!listExist) return; if (!listExist) return false;
// Subscribe stream // Subscribe stream
this.subscriber.on(`userListStream:${this.listId}`, this.send); this.subscriber.on(`userListStream:${this.listId}`, this.send);
@@ -67,6 +67,8 @@ export class UserListChannel extends Channel {
this.updateListUsers(); this.updateListUsers();
this.listUsersClock = setInterval(this.updateListUsers, 5000); this.listUsersClock = setInterval(this.updateListUsers, 5000);
return true;
} }
@bindThis @bindThis

View File

@@ -414,3 +414,5 @@ export type FilterUnionByProperty<
Property extends string | number | symbol, Property extends string | number | symbol,
Condition, Condition,
> = Union extends Record<Property, Condition> ? Union : never; > = Union extends Record<Property, Condition> ? Union : never;
export type Awaitable<T> = T | Promise<T>;