feat(server): cleanup gateway code (#6118)

This commit is contained in:
liuyi
2024-03-15 02:00:40 +00:00
parent 79ffca314d
commit 533c181640
5 changed files with 156 additions and 164 deletions
@@ -38,27 +38,21 @@ export const GatewayErrorWrapper = (): MethodDecorator => {
return desc; return desc;
} }
desc.value = function (...args: any[]) { desc.value = async function (...args: any[]) {
let result: any;
try { try {
result = originalMethod.apply(this, args); return await originalMethod.apply(this, args);
} catch (e) { } catch (e) {
metrics.socketio.counter('unhandled_errors').add(1); if (e instanceof EventError) {
return {
error: new InternalError(e as Error),
};
}
if (result instanceof Promise) {
return result.catch(e => {
metrics.socketio.counter('unhandled_errors').add(1);
new Logger('EventsGateway').error(e, e.stack);
return { return {
error: new InternalError(e), error: e,
}; };
}); } else {
} else { metrics.socketio.counter('unhandled_errors').add(1);
return result; new Logger('EventsGateway').error(e, (e as Error).stack);
return {
error: new InternalError(e as Error),
};
}
} }
}; };
@@ -85,6 +79,14 @@ type EventResponse<Data = any> =
data: Data; data: Data;
}); });
function Sync(workspaceId: string): `${string}:sync` {
return `${workspaceId}:sync`;
}
function Awareness(workspaceId: string): `${string}:awareness` {
return `${workspaceId}:awareness`;
}
@WebSocketGateway({ @WebSocketGateway({
cors: !AFFiNE.node.prod, cors: !AFFiNE.node.prod,
transports: ['websocket'], transports: ['websocket'],
@@ -113,7 +115,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
metrics.socketio.gauge('realtime_connections').record(this.connectionCount); metrics.socketio.gauge('realtime_connections').record(this.connectionCount);
} }
checkVersion(client: Socket, version?: string) { assertVersion(client: Socket, version?: string) {
if ( if (
// @todo(@darkskygit): remove this flag after 0.12 goes stable // @todo(@darkskygit): remove this flag after 0.12 goes stable
AFFiNE.featureFlags.syncClientVersionCheck && AFFiNE.featureFlags.syncClientVersionCheck &&
@@ -126,14 +128,48 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
version ? ` ${version}` : '' version ? ` ${version}` : ''
} is outdated, please update to ${AFFiNE.version}`, } is outdated, please update to ${AFFiNE.version}`,
}); });
return {
error: new EventError( throw new EventError(
EventErrorCode.VERSION_REJECTED, EventErrorCode.VERSION_REJECTED,
`Client version ${version} is outdated, please update to ${AFFiNE.version}` `Client version ${version} is outdated, please update to ${AFFiNE.version}`
), );
}; }
}
async joinWorkspace(
client: Socket,
room: `${string}:${'sync' | 'awareness'}`
) {
await client.join(room);
}
async leaveWorkspace(
client: Socket,
room: `${string}:${'sync' | 'awareness'}`
) {
await client.leave(room);
}
assertInWorkspace(client: Socket, room: `${string}:${'sync' | 'awareness'}`) {
if (!client.rooms.has(room)) {
throw new NotInWorkspaceError(room);
}
}
async assertWorkspaceAccessible(
workspaceId: string,
userId: string,
permission: Permission = Permission.Read
) {
if (
!(await this.permissions.isWorkspaceMember(
workspaceId,
userId,
permission
))
) {
throw new AccessDeniedError(workspaceId);
} }
return null;
} }
@Auth() @Auth()
@@ -144,29 +180,19 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
@MessageBody('version') version: string | undefined, @MessageBody('version') version: string | undefined,
@ConnectedSocket() client: Socket @ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> { ): Promise<EventResponse<{ clientId: string }>> {
const versionError = this.checkVersion(client, version); this.assertVersion(client, version);
if (versionError) { await this.assertWorkspaceAccessible(
return versionError;
}
const canWrite = await this.permissions.tryCheckWorkspace(
workspaceId, workspaceId,
user.id, user.id,
Permission.Write Permission.Write
); );
if (canWrite) { await this.joinWorkspace(client, Sync(workspaceId));
await client.join(`${workspaceId}:sync`); return {
return { data: {
data: { clientId: client.id,
clientId: client.id, },
}, };
};
} else {
return {
error: new AccessDeniedError(workspaceId),
};
}
} }
@Auth() @Auth()
@@ -177,47 +203,18 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
@MessageBody('version') version: string | undefined, @MessageBody('version') version: string | undefined,
@ConnectedSocket() client: Socket @ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> { ): Promise<EventResponse<{ clientId: string }>> {
const versionError = this.checkVersion(client, version); this.assertVersion(client, version);
if (versionError) { await this.assertWorkspaceAccessible(
return versionError;
}
const canWrite = await this.permissions.tryCheckWorkspace(
workspaceId, workspaceId,
user.id, user.id,
Permission.Write Permission.Write
); );
if (canWrite) { await this.joinWorkspace(client, Awareness(workspaceId));
await client.join(`${workspaceId}:awareness`);
return {
data: {
clientId: client.id,
},
};
} else {
return {
error: new AccessDeniedError(workspaceId),
};
}
}
/**
* @deprecated use `client-handshake-sync` and `client-handshake-awareness` instead
*/
@Auth()
@SubscribeMessage('client-handshake')
async handleClientHandShake(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
const versionError = this.checkVersion(client);
if (versionError) {
return versionError;
}
// should unreachable
return { return {
error: new AccessDeniedError(workspaceId), data: {
clientId: client.id,
},
}; };
} }
@@ -226,14 +223,9 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
@MessageBody() workspaceId: string, @MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket @ConnectedSocket() client: Socket
): Promise<EventResponse> { ): Promise<EventResponse> {
if (client.rooms.has(`${workspaceId}:sync`)) { this.assertInWorkspace(client, Sync(workspaceId));
await client.leave(`${workspaceId}:sync`); await this.leaveWorkspace(client, Sync(workspaceId));
return {}; return {};
} else {
return {
error: new NotInWorkspaceError(workspaceId),
};
}
} }
@SubscribeMessage('client-leave-awareness') @SubscribeMessage('client-leave-awareness')
@@ -241,14 +233,9 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
@MessageBody() workspaceId: string, @MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket @ConnectedSocket() client: Socket
): Promise<EventResponse> { ): Promise<EventResponse> {
if (client.rooms.has(`${workspaceId}:awareness`)) { this.assertInWorkspace(client, Awareness(workspaceId));
await client.leave(`${workspaceId}:awareness`); await this.leaveWorkspace(client, Awareness(workspaceId));
return {}; return {};
} else {
return {
error: new NotInWorkspaceError(workspaceId),
};
}
} }
@SubscribeMessage('client-pre-sync') @SubscribeMessage('client-pre-sync')
@@ -257,11 +244,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
@MessageBody() @MessageBody()
{ workspaceId, timestamp }: { workspaceId: string; timestamp?: number } { workspaceId, timestamp }: { workspaceId: string; timestamp?: number }
): Promise<EventResponse<Record<string, number>>> { ): Promise<EventResponse<Record<string, number>>> {
if (!client.rooms.has(`${workspaceId}:sync`)) { this.assertInWorkspace(client, Sync(workspaceId));
return {
error: new NotInWorkspaceError(workspaceId),
};
}
const stats = await this.docManager.getStats(workspaceId, timestamp); const stats = await this.docManager.getStats(workspaceId, timestamp);
@@ -284,11 +267,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
}, },
@ConnectedSocket() client: Socket @ConnectedSocket() client: Socket
): Promise<EventResponse<{ accepted: true; timestamp?: number }>> { ): Promise<EventResponse<{ accepted: true; timestamp?: number }>> {
if (!client.rooms.has(`${workspaceId}:sync`)) { this.assertInWorkspace(client, Sync(workspaceId));
return {
error: new NotInWorkspaceError(workspaceId),
};
}
const docId = new DocID(guid, workspaceId); const docId = new DocID(guid, workspaceId);
const buffers = updates.map(update => Buffer.from(update, 'base64')); const buffers = updates.map(update => Buffer.from(update, 'base64'));
@@ -299,7 +278,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
); );
client client
.to(`${docId.workspace}:sync`) .to(Sync(workspaceId))
.emit('server-updates', { workspaceId, guid, updates, timestamp }); .emit('server-updates', { workspaceId, guid, updates, timestamp });
return { return {
@@ -310,11 +289,9 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
}; };
} }
@Auth()
@SubscribeMessage('doc-load-v2') @SubscribeMessage('doc-load-v2')
async loadDocV2( async loadDocV2(
@ConnectedSocket() client: Socket, @ConnectedSocket() client: Socket,
@CurrentUser() user: CurrentUser,
@MessageBody() @MessageBody()
{ {
workspaceId, workspaceId,
@@ -326,17 +303,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
stateVector?: string; stateVector?: string;
} }
): Promise<EventResponse<{ missing: string; state?: string }>> { ): Promise<EventResponse<{ missing: string; state?: string }>> {
if (!client.rooms.has(`${workspaceId}:sync`)) { this.assertInWorkspace(client, Sync(workspaceId));
const canRead = await this.permissions.tryCheckWorkspace(
workspaceId,
user.id
);
if (!canRead) {
return {
error: new AccessDeniedError(workspaceId),
};
}
}
const docId = new DocID(guid, workspaceId); const docId = new DocID(guid, workspaceId);
const doc = await this.docManager.get(docId.workspace, docId.guid); const doc = await this.docManager.get(docId.workspace, docId.guid);
@@ -363,40 +330,33 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
}; };
} }
@Auth()
@SubscribeMessage('awareness-init') @SubscribeMessage('awareness-init')
async handleInitAwareness( async handleInitAwareness(
@MessageBody() workspaceId: string, @MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket @ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> { ): Promise<EventResponse<{ clientId: string }>> {
if (client.rooms.has(`${workspaceId}:awareness`)) { this.assertInWorkspace(client, Awareness(workspaceId));
client.to(`${workspaceId}:awareness`).emit('new-client-awareness-init'); client.to(Awareness(workspaceId)).emit('new-client-awareness-init');
return { return {
data: { data: {
clientId: client.id, clientId: client.id,
}, },
}; };
} else {
return {
error: new NotInWorkspaceError(workspaceId),
};
}
} }
@SubscribeMessage('awareness-update') @SubscribeMessage('awareness-update')
async handleHelpGatheringAwareness( async handleHelpGatheringAwareness(
@MessageBody() message: { workspaceId: string; awarenessUpdate: string }, @MessageBody()
{
workspaceId,
awarenessUpdate,
}: { workspaceId: string; awarenessUpdate: string },
@ConnectedSocket() client: Socket @ConnectedSocket() client: Socket
): Promise<EventResponse> { ): Promise<EventResponse> {
if (client.rooms.has(`${message.workspaceId}:awareness`)) { this.assertInWorkspace(client, Awareness(workspaceId));
client client
.to(`${message.workspaceId}:awareness`) .to(Awareness(workspaceId))
.emit('server-awareness-broadcast', message); .emit('server-awareness-broadcast', { workspaceId, awarenessUpdate });
return {}; return {};
} else {
return {
error: new NotInWorkspaceError(message.workspaceId),
};
}
} }
} }
@@ -73,6 +73,28 @@ export class PermissionService {
return this.tryCheckPage(ws, id, user); return this.tryCheckPage(ws, id, user);
} }
/**
* Returns whether a given user is a member of a workspace and has the given or higher permission.
*/
async isWorkspaceMember(
ws: string,
user: string,
permission: Permission
): Promise<boolean> {
const count = await this.prisma.workspaceUserPermission.count({
where: {
workspaceId: ws,
userId: user,
accepted: true,
type: {
gte: permission,
},
},
});
return count !== 0;
}
async checkWorkspace( async checkWorkspace(
ws: string, ws: string,
user?: string, user?: string,
@@ -18,11 +18,16 @@ export const CallTimer = (
return desc; return desc;
} }
desc.value = function (...args: any[]) { desc.value = async function (...args: any[]) {
const timer = metrics[scope].histogram(name, { const timer = metrics[scope].histogram(name, {
description: `function call time costs of ${name}`, description: `function call time costs of ${name}`,
unit: 'ms', unit: 'ms',
}); });
metrics[scope]
.counter(`${name}_calls`, {
description: `function call counts of ${name}`,
})
.add(1, attrs);
const start = Date.now(); const start = Date.now();
@@ -30,19 +35,10 @@ export const CallTimer = (
timer.record(Date.now() - start, attrs); timer.record(Date.now() - start, attrs);
}; };
let result: any;
try { try {
result = originalMethod.apply(this, args); return await originalMethod.apply(this, args);
} catch (e) { } finally {
end(); end();
throw e;
}
if (result instanceof Promise) {
return result.finally(end);
} else {
end();
return result;
} }
}; };
@@ -104,7 +104,7 @@ export class AffineCloudAwarenessProvider implements AwarenessProvider {
uint8ArrayToBase64(awarenessUpdate) uint8ArrayToBase64(awarenessUpdate)
.then(encodedAwarenessUpdate => { .then(encodedAwarenessUpdate => {
this.socket.emit('awareness-update', { this.socket.emit('awareness-update', {
guid: this.workspaceId, workspaceId: this.workspaceId,
awarenessUpdate: encodedAwarenessUpdate, awarenessUpdate: encodedAwarenessUpdate,
}); });
}) })
@@ -120,11 +120,19 @@ export class AffineCloudAwarenessProvider implements AwarenessProvider {
}; };
handleConnect = () => { handleConnect = () => {
this.socket.emit('client-handshake-awareness', { this.socket.emit(
workspaceId: this.workspaceId, 'client-handshake-awareness',
version: runtimeConfig.appVersion, {
}); workspaceId: this.workspaceId,
this.socket.emit('awareness-init', this.workspaceId); version: runtimeConfig.appVersion,
},
(res: any) => {
logger.debug('awareness handshake finished', res);
this.socket.emit('awareness-init', this.workspaceId, (res: any) => {
logger.debug('awareness-init finished', res);
});
}
);
}; };
handleReject = (_msg: RejectByVersion) => { handleReject = (_msg: RejectByVersion) => {
@@ -42,10 +42,16 @@ export class AffineSyncStorage implements SyncStorage {
} }
handleConnect = () => { handleConnect = () => {
this.socket.emit('client-handshake-sync', { this.socket.emit(
workspaceId: this.workspaceId, 'client-handshake-sync',
version: runtimeConfig.appVersion, {
}); workspaceId: this.workspaceId,
version: runtimeConfig.appVersion,
},
(res: any) => {
logger.debug('client handshake finished', res);
}
);
}; };
handleReject = (message: RejectByVersion) => { handleReject = (message: RejectByVersion) => {