cvmts/protocol: Make protocols stateless

Instead of creating an instance of a protocol per user and storing state there, we just have the protocol implementations take in all of the state they should need in processMessage(), and store them all globally (with some wrappers to make it easier to handle this). This makes things slightly cleaner (and probably also helps memory usage, since we now don't need to create protocol instances as soon as a user connects/swaps, and they don't need to be garbage collected since they are held in the manager.)
This commit is contained in:
modeco80
2025-06-15 15:03:13 -04:00
parent bce2a0172a
commit 4211941560
6 changed files with 317 additions and 247 deletions

View File

@@ -177,30 +177,27 @@ export default class CollabVMServer implements IProtocolMessageHandler {
user.socket.on('msg', (buf: Buffer, binary: boolean) => { user.socket.on('msg', (buf: Buffer, binary: boolean) => {
try { try {
user.protocol.processMessage(buf); user.processMessage(this, buf);
} catch (err) { } catch (err) {
this.logger.error({ this.logger.error({
ip: user.IP.address, ip: user.IP.address,
username: user.username, username: user.username,
error_message: (err as Error).message error_message: (err as Error).message
}, 'Error in %s#processMessage.', Object.getPrototypeOf(user.protocol).constructor?.name); }, 'Error in %s#processMessage.', Object.getPrototypeOf(user).constructor?.name);
user.kick(); user.kick();
} }
}); });
user.socket.on('disconnect', () => this.connectionClosed(user)); user.socket.on('disconnect', () => this.connectionClosed(user));
// Set ourselves as the handler
user.protocol.setHandler(this as IProtocolMessageHandler);
if (this.Config.auth.enabled) { if (this.Config.auth.enabled) {
user.protocol.sendAuth(this.Config.auth.apiEndpoint); user.sendAuth(this.Config.auth.apiEndpoint);
} }
user.protocol.sendAddUser(this.getAddUser()); user.sendAddUser(this.getAddUser());
if (this.Config.geoip.enabled) { if (this.Config.geoip.enabled) {
let flags = this.getFlags(); let flags = this.getFlags();
user.protocol.sendFlag(flags); user.sendFlag(flags);
} }
} }
@@ -217,8 +214,6 @@ export default class CollabVMServer implements IProtocolMessageHandler {
this.clients.splice(clientIndex, 1); this.clients.splice(clientIndex, 1);
user.protocol.dispose();
this.logger.info(`Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ''}`); this.logger.info(`Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ''}`);
if (!user.username) return; if (!user.username) return;
if (this.TurnQueue.toArray().indexOf(user) !== -1) { if (this.TurnQueue.toArray().indexOf(user) !== -1) {
@@ -227,7 +222,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (hadturn) this.nextTurn(); if (hadturn) this.nextTurn();
} }
this.clients.forEach((c) => c.protocol.sendRemUser([user.username!])); this.clients.forEach((c) => c.sendRemUser([user.username!]));
} }
// Protocol message handlers // Protocol message handlers
@@ -237,7 +232,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!this.Config.auth.enabled) return true; if (!this.Config.auth.enabled) return true;
if (user.rank === Rank.Unregistered && !guestPermission) { if (user.rank === Rank.Unregistered && !guestPermission) {
user.protocol.sendChatMessage('', 'You need to login to do that.'); user.sendChatMessage('', 'You need to login to do that.');
return false; return false;
} }
@@ -252,7 +247,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!this.Config.auth.enabled) return; if (!this.Config.auth.enabled) return;
if (!user.connectedToNode) { if (!user.connectedToNode) {
user.protocol.sendLoginResponse(false, 'You must connect to the VM before logging in.'); user.sendLoginResponse(false, 'You must connect to the VM before logging in.');
return; return;
} }
@@ -261,7 +256,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (res.clientSuccess) { if (res.clientSuccess) {
this.logger.info(`${user.IP.address} logged in as ${res.username}`); this.logger.info(`${user.IP.address} logged in as ${res.username}`);
user.protocol.sendLoginResponse(true, ''); user.sendLoginResponse(true, '');
let old = this.clients.find((c) => c.username === res.username); let old = this.clients.find((c) => c.username === res.username);
if (old) { if (old) {
@@ -274,19 +269,19 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (user.countryCode !== null && user.noFlag) { if (user.countryCode !== null && user.noFlag) {
// privacy // privacy
for (let cl of this.clients.filter((c) => c !== user)) { for (let cl of this.clients.filter((c) => c !== user)) {
cl.protocol.sendRemUser([user.username!]); cl.sendRemUser([user.username!]);
} }
this.renameUser(user, res.username, false); this.renameUser(user, res.username, false);
} else this.renameUser(user, res.username, true); } else this.renameUser(user, res.username, true);
// Set rank // Set rank
user.rank = res.rank; user.rank = res.rank;
if (user.rank === Rank.Admin) { if (user.rank === Rank.Admin) {
user.protocol.sendAdminLoginResponse(true, undefined); user.sendAdminLoginResponse(true, undefined);
} else if (user.rank === Rank.Moderator) { } else if (user.rank === Rank.Moderator) {
user.protocol.sendAdminLoginResponse(true, this.ModPerms); user.sendAdminLoginResponse(true, this.ModPerms);
} }
this.clients.forEach((c) => this.clients.forEach((c) =>
c.protocol.sendAddUser([ c.sendAddUser([
{ {
username: user.username!, username: user.username!,
rank: user.rank rank: user.rank
@@ -294,7 +289,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
]) ])
); );
} else { } else {
user.protocol.sendLoginResponse(false, res.error!); user.sendLoginResponse(false, res.error!);
if (res.error === 'You are banned') { if (res.error === 'You are banned') {
user.kick(); user.kick();
} }
@@ -302,7 +297,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
} catch (err) { } catch (err) {
this.logger.error(`Error authenticating client ${user.IP.address}: ${(err as Error).message}`); this.logger.error(`Error authenticating client ${user.IP.address}: ${(err as Error).message}`);
user.protocol.sendLoginResponse(false, 'There was an internal error while authenticating. Please let a staff member know as soon as possible'); user.sendLoginResponse(false, 'There was an internal error while authenticating. Please let a staff member know as soon as possible');
} }
} }
@@ -323,16 +318,14 @@ export default class CollabVMServer implements IProtocolMessageHandler {
case ProtocolUpgradeCapability.BinRects: case ProtocolUpgradeCapability.BinRects:
enabledCaps.push(cap as ProtocolUpgradeCapability); enabledCaps.push(cap as ProtocolUpgradeCapability);
user.Capabilities.bin = true; user.Capabilities.bin = true;
user.protocol.dispose(); user.protocol = TheProtocolManager.getProtocol('binary1');
user.protocol = TheProtocolManager.createProtocol('binary1', user);
user.protocol.setHandler(this as IProtocolMessageHandler);
break; break;
default: default:
break; break;
} }
} }
user.protocol.sendCapabilities(enabledCaps); user.sendCapabilities(enabledCaps);
return true; return true;
} }
@@ -377,18 +370,18 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!this.authCheck(user, this.Config.auth.guestPermissions.callForReset)) return; if (!this.authCheck(user, this.Config.auth.guestPermissions.callForReset)) return;
if (this.voteCooldown !== 0) { if (this.voteCooldown !== 0) {
user.protocol.sendVoteCooldown(this.voteCooldown); user.sendVoteCooldown(this.voteCooldown);
return; return;
} }
this.startVote(); this.startVote();
this.clients.forEach((c) => c.protocol.sendChatMessage('', `${user.username} has started a vote to reset the VM.`)); this.clients.forEach((c) => c.sendChatMessage('', `${user.username} has started a vote to reset the VM.`));
} }
if (!this.authCheck(user, this.Config.auth.guestPermissions.vote)) return; if (!this.authCheck(user, this.Config.auth.guestPermissions.vote)) return;
if (user.IP.vote !== true) { if (user.IP.vote !== true) {
this.clients.forEach((c) => c.protocol.sendChatMessage('', `${user.username} has voted yes.`)); this.clients.forEach((c) => c.sendChatMessage('', `${user.username} has voted yes.`));
} }
user.IP.vote = true; user.IP.vote = true;
break; break;
@@ -398,7 +391,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!this.authCheck(user, this.Config.auth.guestPermissions.vote)) return; if (!this.authCheck(user, this.Config.auth.guestPermissions.vote)) return;
if (user.IP.vote !== false) { if (user.IP.vote !== false) {
this.clients.forEach((c) => c.protocol.sendChatMessage('', `${user.username} has voted no.`)); this.clients.forEach((c) => c.sendChatMessage('', `${user.username} has voted no.`));
} }
user.IP.vote = false; user.IP.vote = false;
break; break;
@@ -416,13 +409,13 @@ export default class CollabVMServer implements IProtocolMessageHandler {
}; };
if (this.VM.GetState() == VMState.Started) { if (this.VM.GetState() == VMState.Started) {
user.protocol.sendListResponse([listEntry]); user.sendListResponse([listEntry]);
} }
} }
private async connectViewShared(user: User, node: string, viewMode: number | undefined) { private async connectViewShared(user: User, node: string, viewMode: number | undefined) {
if (!user.username || node !== this.Config.collabvm.node) { if (!user.username || node !== this.Config.collabvm.node) {
user.protocol.sendConnectFailResponse(); user.sendConnectFailResponse();
return; return;
} }
@@ -430,23 +423,23 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (viewMode !== undefined) { if (viewMode !== undefined) {
if (viewMode !== 0 && viewMode !== 1) { if (viewMode !== 0 && viewMode !== 1) {
user.protocol.sendConnectFailResponse(); user.sendConnectFailResponse();
return; return;
} }
user.viewMode = viewMode; user.viewMode = viewMode;
} }
user.protocol.sendConnectOKResponse(this.VM.SnapshotsSupported()); user.sendConnectOKResponse(this.VM.SnapshotsSupported());
if (this.ChatHistory.size !== 0) { if (this.ChatHistory.size !== 0) {
let history = this.ChatHistory.toArray() as ChatHistory[]; let history = this.ChatHistory.toArray() as ChatHistory[];
user.protocol.sendChatHistoryMessage(history); user.sendChatHistoryMessage(history);
} }
if (this.Config.collabvm.motd) user.protocol.sendChatMessage('', this.Config.collabvm.motd); if (this.Config.collabvm.motd) user.sendChatMessage('', this.Config.collabvm.motd);
if (this.screenHidden) { if (this.screenHidden) {
user?.protocol.sendScreenResize(1024, 768); user?.sendScreenResize(1024, 768);
user?.protocol.sendScreenUpdate({ user?.sendScreenUpdate({
x: 0, x: 0,
y: 0, y: 0,
data: this.screenHiddenImg data: this.screenHiddenImg
@@ -455,7 +448,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
await this.SendFullScreenWithSize(user); await this.SendFullScreenWithSize(user);
} }
user.protocol.sendSync(Date.now()); user.sendSync(Date.now());
if (this.voteInProgress) this.sendVoteUpdate(user); if (this.voteInProgress) this.sendVoteUpdate(user);
this.sendTurnUpdate(user); this.sendTurnUpdate(user);
@@ -473,12 +466,12 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!user.RenameRateLimit.request()) return; if (!user.RenameRateLimit.request()) return;
if (user.connectedToNode && user.IP.muted) return; if (user.connectedToNode && user.IP.muted) return;
if (this.Config.auth.enabled && user.rank !== Rank.Unregistered) { if (this.Config.auth.enabled && user.rank !== Rank.Unregistered) {
user.protocol.sendChatMessage('', 'Go to your account settings to change your username.'); user.sendChatMessage('', 'Go to your account settings to change your username.');
return; return;
} }
if (this.Config.auth.enabled && newName !== undefined) { if (this.Config.auth.enabled && newName !== undefined) {
// Don't send system message to a user without a username since it was likely an automated attempt by the webapp // Don't send system message to a user without a username since it was likely an automated attempt by the webapp
if (user.username) user.protocol.sendChatMessage('', 'You need to log in to do that.'); if (user.username) user.sendChatMessage('', 'You need to log in to do that.');
if (user.rank !== Rank.Unregistered) return; if (user.rank !== Rank.Unregistered) return;
this.renameUser(user, undefined); this.renameUser(user, undefined);
return; return;
@@ -496,7 +489,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (msg.length > this.Config.collabvm.maxChatLength) msg = msg.substring(0, this.Config.collabvm.maxChatLength); if (msg.length > this.Config.collabvm.maxChatLength) msg = msg.substring(0, this.Config.collabvm.maxChatLength);
if (msg.trim().length < 1) return; if (msg.trim().length < 1) return;
this.clients.forEach((c) => c.protocol.sendChatMessage(user.username!, msg)); this.clients.forEach((c) => c.sendChatMessage(user.username!, msg));
this.ChatHistory.push({ user: user.username, msg: msg }); this.ChatHistory.push({ user: user.username, msg: msg });
user.onChatMsgSent(); user.onChatMsgSent();
} }
@@ -520,23 +513,23 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) { if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) {
user.turnWhitelist = true; user.turnWhitelist = true;
user.protocol.sendChatMessage('', 'You may now take turns.'); user.sendChatMessage('', 'You may now take turns.');
return; return;
} }
if (this.Config.auth.enabled) { if (this.Config.auth.enabled) {
user.protocol.sendChatMessage('', 'This server does not support staff passwords. Please log in to become staff.'); user.sendChatMessage('', 'This server does not support staff passwords. Please log in to become staff.');
return; return;
} }
if (pwdHash === this.Config.collabvm.adminpass) { if (pwdHash === this.Config.collabvm.adminpass) {
user.rank = Rank.Admin; user.rank = Rank.Admin;
user.protocol.sendAdminLoginResponse(true, undefined); user.sendAdminLoginResponse(true, undefined);
} else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) { } else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) {
user.rank = Rank.Moderator; user.rank = Rank.Moderator;
user.protocol.sendAdminLoginResponse(true, this.ModPerms); user.sendAdminLoginResponse(true, this.ModPerms);
} else { } else {
user.protocol.sendAdminLoginResponse(false, undefined); user.sendAdminLoginResponse(false, undefined);
return; return;
} }
@@ -546,7 +539,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
// Update rank // Update rank
this.clients.forEach((c) => this.clients.forEach((c) =>
c.protocol.sendAddUser([ c.sendAddUser([
{ {
username: user.username!, username: user.username!,
rank: user.rank rank: user.rank
@@ -560,7 +553,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (node !== this.Config.collabvm.node) return; if (node !== this.Config.collabvm.node) return;
TheAuditLog.onMonitorCommand(user, command); TheAuditLog.onMonitorCommand(user, command);
let output = await this.VM.MonitorCommand(command); let output = await this.VM.MonitorCommand(command);
user.protocol.sendAdminMonitorResponse(String(output)); user.sendAdminMonitorResponse(String(output));
} }
onAdminRestore(user: User, node: string): void { onAdminRestore(user: User, node: string): void {
@@ -624,7 +617,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
onAdminRename(user: User, target: string, newName: string): void { onAdminRename(user: User, target: string, newName: string): void {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.rename)) return; if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.rename)) return;
if (this.Config.auth.enabled) { if (this.Config.auth.enabled) {
user.protocol.sendChatMessage('', 'Cannot rename users on a server that uses authentication.'); user.sendChatMessage('', 'Cannot rename users on a server that uses authentication.');
} }
var targetUser = this.clients.find((c) => c.username === target); var targetUser = this.clients.find((c) => c.username === target);
if (!targetUser) return; if (!targetUser) return;
@@ -635,7 +628,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.grabip)) return; if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.grabip)) return;
let target = this.clients.find((c) => c.username === username); let target = this.clients.find((c) => c.username === username);
if (!target) return; if (!target) return;
user.protocol.sendAdminIPResponse(username, target.IP.address); user.sendAdminIPResponse(username, target.IP.address);
} }
onAdminBypassTurn(user: User): void { onAdminBypassTurn(user: User): void {
@@ -647,14 +640,14 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.xss)) return; if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.xss)) return;
switch (user.rank) { switch (user.rank) {
case Rank.Admin: case Rank.Admin:
this.clients.forEach((c) => c.protocol.sendChatMessage(user.username!, message)); this.clients.forEach((c) => c.sendChatMessage(user.username!, message));
this.ChatHistory.push({ user: user.username!, msg: message }); this.ChatHistory.push({ user: user.username!, msg: message });
break; break;
case Rank.Moderator: case Rank.Moderator:
this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.protocol.sendChatMessage(user.username!, message)); this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.sendChatMessage(user.username!, message));
this.clients.filter((c) => c.rank === Rank.Admin).forEach((c) => c.protocol.sendChatMessage(user.username!, Utilities.HTMLSanitize(message))); this.clients.filter((c) => c.rank === Rank.Admin).forEach((c) => c.sendChatMessage(user.username!, Utilities.HTMLSanitize(message)));
break; break;
} }
} }
@@ -700,8 +693,8 @@ export default class CollabVMServer implements IProtocolMessageHandler {
this.clients this.clients
.filter((c) => c.rank == Rank.Unregistered) .filter((c) => c.rank == Rank.Unregistered)
.forEach((client) => { .forEach((client) => {
client.protocol.sendScreenResize(1024, 768); client.sendScreenResize(1024, 768);
client.protocol.sendScreenUpdate({ client.sendScreenUpdate({
x: 0, x: 0,
y: 0, y: 0,
data: this.screenHiddenImg data: this.screenHiddenImg
@@ -712,7 +705,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
onAdminSystemMessage(user: User, message: string): void { onAdminSystemMessage(user: User, message: string): void {
if (user.rank !== Rank.Admin) return; if (user.rank !== Rank.Admin) return;
this.clients.forEach((c) => c.protocol.sendChatMessage('', message)); this.clients.forEach((c) => c.sendChatMessage('', message));
} }
// end protocol message handlers // end protocol message handlers
@@ -736,7 +729,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
} else { } else {
newName = newName.trim(); newName = newName.trim();
if (hadName && newName === oldname) { if (hadName && newName === oldname) {
client.protocol.sendSelfRename(ProtocolRenameStatus.Ok, client.username!, client.rank); client.sendSelfRename(ProtocolRenameStatus.Ok, client.username!, client.rank);
return; return;
} }
@@ -754,16 +747,16 @@ export default class CollabVMServer implements IProtocolMessageHandler {
} else client.username = newName; } else client.username = newName;
} }
client.protocol.sendSelfRename(status, client.username!, client.rank); client.sendSelfRename(status, client.username!, client.rank);
if (hadName) { if (hadName) {
this.logger.info(`Rename ${client.IP.address} from ${oldname} to ${client.username}`); this.logger.info(`Rename ${client.IP.address} from ${oldname} to ${client.username}`);
if (announce) this.clients.forEach((c) => c.protocol.sendRename(oldname, client.username!, client.rank)); if (announce) this.clients.forEach((c) => c.sendRename(oldname, client.username!, client.rank));
} else { } else {
this.logger.info(`Rename ${client.IP.address} to ${client.username}`); this.logger.info(`Rename ${client.IP.address} to ${client.username}`);
if (announce) if (announce)
this.clients.forEach((c) => { this.clients.forEach((c) => {
c.protocol.sendAddUser([ c.sendAddUser([
{ {
username: client.username!, username: client.username!,
rank: client.rank rank: client.rank
@@ -771,7 +764,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
]); ]);
if (client.countryCode !== null) { if (client.countryCode !== null) {
c.protocol.sendFlag([ c.sendFlag([
{ {
username: client.username!, username: client.username!,
countryCode: client.countryCode countryCode: client.countryCode
@@ -816,7 +809,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
var currentTurningUser = this.TurnQueue.peek(); var currentTurningUser = this.TurnQueue.peek();
if (client) { if (client) {
client.protocol.sendTurnQueue(turntime, users); client.sendTurnQueue(turntime, users);
return; return;
} }
@@ -827,12 +820,12 @@ export default class CollabVMServer implements IProtocolMessageHandler {
var time; var time;
if (this.indefiniteTurn === null) time = this.TurnTime * 1000 + (turnQueueArr.indexOf(c) - 1) * this.Config.collabvm.turnTime * 1000; if (this.indefiniteTurn === null) time = this.TurnTime * 1000 + (turnQueueArr.indexOf(c) - 1) * this.Config.collabvm.turnTime * 1000;
else time = 9999999999; else time = 9999999999;
c.protocol.sendTurnQueueWaiting(turntime, users, time); c.sendTurnQueueWaiting(turntime, users, time);
} else { } else {
c.protocol.sendTurnQueue(turntime, users); c.sendTurnQueue(turntime, users);
} }
}); });
if (currentTurningUser) currentTurningUser.protocol.sendTurnQueue(turntime, users); if (currentTurningUser) currentTurningUser.sendTurnQueue(turntime, users);
} }
private nextTurn() { private nextTurn() {
clearInterval(this.TurnInterval); clearInterval(this.TurnInterval);
@@ -883,7 +876,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
.filter((c) => c.connectedToNode || c.viewMode == 1) .filter((c) => c.connectedToNode || c.viewMode == 1)
.forEach((c) => { .forEach((c) => {
if (this.screenHidden && c.rank == Rank.Unregistered) return; if (this.screenHidden && c.rank == Rank.Unregistered) return;
c.protocol.sendScreenResize(size.width, size.height); c.sendScreenResize(size.width, size.height);
}); });
} }
@@ -898,7 +891,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
.forEach((c) => { .forEach((c) => {
if (self.screenHidden && c.rank == Rank.Unregistered) return; if (self.screenHidden && c.rank == Rank.Unregistered) return;
c.protocol.sendScreenUpdate({ c.sendScreenUpdate({
x: rect.x, x: rect.x,
y: rect.y, y: rect.y,
data: encoded data: encoded
@@ -930,9 +923,9 @@ export default class CollabVMServer implements IProtocolMessageHandler {
height: displaySize.height height: displaySize.height
}); });
client.protocol.sendScreenResize(displaySize.width, displaySize.height); client.sendScreenResize(displaySize.width, displaySize.height);
client.protocol.sendScreenUpdate({ client.sendScreenUpdate({
x: 0, x: 0,
y: 0, y: 0,
data: encoded data: encoded
@@ -963,7 +956,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
startVote() { startVote() {
if (this.voteInProgress) return; if (this.voteInProgress) return;
this.voteInProgress = true; this.voteInProgress = true;
this.clients.forEach((c) => c.protocol.sendVoteStarted()); this.clients.forEach((c) => c.sendVoteStarted());
this.voteTime = this.Config.collabvm.voteTime; this.voteTime = this.Config.collabvm.voteTime;
this.voteInterval = setInterval(() => { this.voteInterval = setInterval(() => {
this.voteTime--; this.voteTime--;
@@ -978,12 +971,12 @@ export default class CollabVMServer implements IProtocolMessageHandler {
this.voteInProgress = false; this.voteInProgress = false;
clearInterval(this.voteInterval); clearInterval(this.voteInterval);
var count = this.getVoteCounts(); var count = this.getVoteCounts();
this.clients.forEach((c) => c.protocol.sendVoteEnded()); this.clients.forEach((c) => c.sendVoteEnded());
if (result === true || (result === undefined && count.yes >= count.no)) { if (result === true || (result === undefined && count.yes >= count.no)) {
this.clients.forEach((c) => c.protocol.sendChatMessage('', 'The vote to reset the VM has won.')); this.clients.forEach((c) => c.sendChatMessage('', 'The vote to reset the VM has won.'));
this.VM.Reset(); this.VM.Reset();
} else { } else {
this.clients.forEach((c) => c.protocol.sendChatMessage('', 'The vote to reset the VM has lost.')); this.clients.forEach((c) => c.sendChatMessage('', 'The vote to reset the VM has lost.'));
} }
this.clients.forEach((c) => { this.clients.forEach((c) => {
c.IP.vote = null; c.IP.vote = null;
@@ -999,8 +992,8 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!this.voteInProgress) return; if (!this.voteInProgress) return;
var count = this.getVoteCounts(); var count = this.getVoteCounts();
if (client) client.protocol.sendVoteStats(this.voteTime * 1000, count.yes, count.no); if (client) client.sendVoteStats(this.voteTime * 1000, count.yes, count.no);
else this.clients.forEach((c) => c.protocol.sendVoteStats(this.voteTime * 1000, count.yes, count.no)); else this.clients.forEach((c) => c.sendVoteStats(this.voteTime * 1000, count.yes, count.no));
} }
getVoteCounts(): VoteTally { getVoteCounts(): VoteTally {

View File

@@ -7,7 +7,7 @@ import { NetworkClient } from './net/NetworkClient.js';
import { CollabVMCapabilities } from '@cvmts/collab-vm-1.2-binary-protocol'; import { CollabVMCapabilities } from '@cvmts/collab-vm-1.2-binary-protocol';
import pino from 'pino'; import pino from 'pino';
import { BanManager } from './BanManager.js'; import { BanManager } from './BanManager.js';
import { IProtocol } from './protocol/Protocol.js'; import { IProtocol, IProtocolMessageHandler, ListEntry, ProtocolAddUser, ProtocolChatHistory, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, ScreenRect } from './protocol/Protocol.js';
import { TheProtocolManager } from './protocol/Manager.js'; import { TheProtocolManager } from './protocol/Manager.js';
export class User { export class User {
@@ -47,7 +47,7 @@ export class User {
this.Capabilities = new CollabVMCapabilities(); this.Capabilities = new CollabVMCapabilities();
// All clients default to the Guacamole protocol. // All clients default to the Guacamole protocol.
this.protocol = TheProtocolManager.createProtocol(protocol, this); this.protocol = TheProtocolManager.getProtocol(protocol);
this.socket.on('disconnect', () => { this.socket.on('disconnect', () => {
// Unref the ip data for this connection // Unref the ip data for this connection
@@ -90,10 +90,6 @@ export class User {
this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000); this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000);
} }
sendNop() {
this.protocol.sendNop();
}
sendMsg(msg: string) { sendMsg(msg: string) {
if (!this.socket.isOpen()) return; if (!this.socket.isOpen()) return;
clearInterval(this.nopSendInterval); clearInterval(this.nopSendInterval);
@@ -152,6 +148,118 @@ export class User {
this.sendMsg('10.disconnect;'); this.sendMsg('10.disconnect;');
this.socket.close(); this.socket.close();
} }
// These wrap the currently set IProtocol instance to feed state to them.
// This is probably grody, but /shrug. It works, and feels less awful than
// manually wrapping state (and probably prevents mixup bugs too.)
processMessage(handler: IProtocolMessageHandler, buffer: Buffer) {
this.protocol.processMessage(this, handler, buffer);
}
sendNop(): void {
this.protocol.sendNop(this);
}
sendSync(now: number): void {
this.protocol.sendSync(this, now);
}
sendAuth(authServer: string): void {
this.protocol.sendAuth(this, authServer);
}
sendCapabilities(caps: ProtocolUpgradeCapability[]): void {
this.protocol.sendCapabilities(this, caps);
}
sendConnectFailResponse(): void {
this.protocol.sendConnectFailResponse(this);
}
sendConnectOKResponse(votes: boolean): void {
this.protocol.sendConnectOKResponse(this, votes);
}
sendLoginResponse(ok: boolean, message: string | undefined): void {
this.protocol.sendLoginResponse(this, ok, message);
}
sendAdminLoginResponse(ok: boolean, modPerms: number | undefined): void {
this.protocol.sendAdminLoginResponse(this, ok, modPerms);
}
sendAdminMonitorResponse(output: string): void {
this.protocol.sendAdminMonitorResponse(this, output);
}
sendAdminIPResponse(username: string, ip: string): void {
this.protocol.sendAdminIPResponse(this, username, ip);
}
sendChatMessage(username: '' | string, message: string): void {
this.protocol.sendChatMessage(this, username, message);
}
sendChatHistoryMessage(history: ProtocolChatHistory[]): void {
this.protocol.sendChatHistoryMessage(this, history);
}
sendAddUser(users: ProtocolAddUser[]): void {
this.protocol.sendAddUser(this, users);
}
sendRemUser(users: string[]): void {
this.protocol.sendRemUser(this, users);
}
sendFlag(flag: ProtocolFlag[]): void {
this.protocol.sendFlag(this, flag);
}
sendSelfRename(status: ProtocolRenameStatus, newUsername: string, rank: Rank): void {
this.protocol.sendSelfRename(this, status, newUsername, rank);
}
sendRename(oldUsername: string, newUsername: string, rank: Rank): void {
this.protocol.sendRename(this, oldUsername, newUsername, rank);
}
sendListResponse(list: ListEntry[]): void {
this.protocol.sendListResponse(this, list);
}
sendTurnQueue(turnTime: number, users: string[]): void {
this.protocol.sendTurnQueue(this, turnTime, users);
}
sendTurnQueueWaiting(turnTime: number, users: string[], waitTime: number): void {
this.protocol.sendTurnQueueWaiting(this, turnTime, users, waitTime);
}
sendVoteStarted(): void {
this.protocol.sendVoteStarted(this);
}
sendVoteStats(msLeft: number, nrYes: number, nrNo: number): void {
this.protocol.sendVoteStats(this, msLeft, nrYes, nrNo);
}
sendVoteEnded(): void {
this.protocol.sendVoteEnded(this);
}
sendVoteCooldown(ms: number): void {
this.protocol.sendVoteCooldown(this, ms);
}
sendScreenResize(width: number, height: number): void {
this.protocol.sendScreenResize(this, width, height);
}
sendScreenUpdate(rect: ScreenRect): void {
this.protocol.sendScreenUpdate(this, rect);
}
} }
export enum Rank { export enum Rank {

View File

@@ -3,14 +3,15 @@ import { CollabVMProtocolMessage, CollabVMProtocolMessageType } from '@cvmts/col
import { GuacamoleProtocol } from './GuacamoleProtocol.js'; import { GuacamoleProtocol } from './GuacamoleProtocol.js';
import { ScreenRect } from './Protocol'; import { ScreenRect } from './Protocol';
import { User } from '../User.js';
export class BinRectsProtocol extends GuacamoleProtocol { export class BinRectsProtocol extends GuacamoleProtocol {
sendScreenUpdate(rect: ScreenRect): void { sendScreenUpdate(user: User, rect: ScreenRect): void {
let bmsg: CollabVMProtocolMessage = { let bmsg: CollabVMProtocolMessage = {
type: CollabVMProtocolMessageType.rect, type: CollabVMProtocolMessageType.rect,
rect: rect rect: rect
}; };
this.user?.socket.sendBinary(msgpack.encode(bmsg)); user.socket.sendBinary(msgpack.encode(bmsg));
} }
} }

View File

@@ -1,42 +1,37 @@
import pino from 'pino'; import { IProtocol, IProtocolMessageHandler, ListEntry, ProtocolAddUser, ProtocolChatHistory, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, ScreenRect } from './Protocol.js';
import { IProtocol, IProtocolMessageHandler, ListEntry, ProtocolAddUser, ProtocolBase, ProtocolChatHistory, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, ScreenRect } from './Protocol.js';
import { Rank, User } from '../User.js'; import { Rank, User } from '../User.js';
import * as cvm from '@cvmts/cvm-rs'; import * as cvm from '@cvmts/cvm-rs';
// CollabVM protocol implementation for Guacamole. // CollabVM protocol implementation for Guacamole.
export class GuacamoleProtocol extends ProtocolBase implements IProtocol { export class GuacamoleProtocol implements IProtocol {
private logger = pino({ private __processMessage_admin(user: User, handler: IProtocolMessageHandler, decodedElements: string[]): boolean {
name: 'CVMTS.GuacamoleProtocol'
});
private __processMessage_admin(decodedElements: string[]): boolean {
switch (decodedElements[1]) { switch (decodedElements[1]) {
case '2': case '2':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminLogin(this.user!, decodedElements[2]); handler.onAdminLogin(user, decodedElements[2]);
break; break;
case '5': case '5':
if (decodedElements.length !== 4) return false; if (decodedElements.length !== 4) return false;
this.handlers?.onAdminMonitor(this.user!, decodedElements[2], decodedElements[3]); handler.onAdminMonitor(user, decodedElements[2], decodedElements[3]);
break; break;
case '8': case '8':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminRestore(this.user!, decodedElements[2]); handler.onAdminRestore(user, decodedElements[2]);
break; break;
case '10': case '10':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminReboot(this.user!, decodedElements[2]); handler.onAdminReboot(user, decodedElements[2]);
break; break;
case '12': case '12':
if (decodedElements.length < 3) return false; if (decodedElements.length < 3) return false;
this.handlers?.onAdminBanUser(this.user!, decodedElements[2]); handler.onAdminBanUser(user, decodedElements[2]);
case '13': case '13':
{ {
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
let choice = parseInt(decodedElements[2]); let choice = parseInt(decodedElements[2]);
if (choice == undefined) return false; if (choice == undefined) return false;
this.handlers?.onAdminForceVote(this.user!, choice); handler.onAdminForceVote(user, choice);
} }
break; break;
case '14': case '14':
@@ -46,35 +41,35 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
if (decodedElements[3] == '0') temporary = true; if (decodedElements[3] == '0') temporary = true;
else if (decodedElements[3] == '1') temporary = false; else if (decodedElements[3] == '1') temporary = false;
else return false; else return false;
this.handlers?.onAdminMuteUser(this.user!, decodedElements[2], temporary); handler.onAdminMuteUser(user, decodedElements[2], temporary);
} }
break; break;
case '15': case '15':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminKickUser(this.user!, decodedElements[2]); handler.onAdminKickUser(user, decodedElements[2]);
break; break;
case '16': case '16':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminEndTurn(this.user!, decodedElements[2]); handler.onAdminEndTurn(user, decodedElements[2]);
break; break;
case '17': case '17':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminClearQueue(this.user!, decodedElements[2]); handler.onAdminClearQueue(user, decodedElements[2]);
break; break;
case '18': case '18':
if (decodedElements.length !== 4) return false; if (decodedElements.length !== 4) return false;
this.handlers?.onAdminRename(this.user!, decodedElements[2], decodedElements[3]); handler.onAdminRename(user, decodedElements[2], decodedElements[3]);
break; break;
case '19': case '19':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminGetIP(this.user!, decodedElements[2]); handler.onAdminGetIP(user, decodedElements[2]);
break; break;
case '20': case '20':
this.handlers?.onAdminBypassTurn(this.user!); handler.onAdminBypassTurn(user);
break; break;
case '21': case '21':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminRawMessage(this.user!, decodedElements[2]); handler.onAdminRawMessage(user, decodedElements[2]);
break; break;
case '22': case '22':
{ {
@@ -84,11 +79,11 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
if (decodedElements[2] == '0') enabled = false; if (decodedElements[2] == '0') enabled = false;
else if (decodedElements[2] == '1') enabled = true; else if (decodedElements[2] == '1') enabled = true;
else return false; else return false;
this.handlers?.onAdminToggleTurns(this.user!, enabled); handler.onAdminToggleTurns(user, enabled);
} }
break; break;
case '23': case '23':
this.handlers?.onAdminIndefiniteTurn(this.user!); handler.onAdminIndefiniteTurn(user);
break; break;
case '24': case '24':
{ {
@@ -97,43 +92,43 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
if (decodedElements[2] == '0') show = false; if (decodedElements[2] == '0') show = false;
else if (decodedElements[2] == '1') show = true; else if (decodedElements[2] == '1') show = true;
else return false; else return false;
this.handlers?.onAdminHideScreen(this.user!, show); handler.onAdminHideScreen(user, show);
} }
break; break;
case '25': case '25':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminSystemMessage(this.user!, decodedElements[2]); handler.onAdminSystemMessage(user, decodedElements[2]);
break; break;
} }
return true; return true;
} }
processMessage(buffer: Buffer): boolean { processMessage(user: User, handler: IProtocolMessageHandler, buffer: Buffer): boolean {
let decodedElements = cvm.guacDecode(buffer.toString('utf-8')); let decodedElements = cvm.guacDecode(buffer.toString('utf-8'));
if (decodedElements.length < 1) return false; if (decodedElements.length < 1) return false;
// The first element is the "opcode". // The first element is the "opcode".
switch (decodedElements[0]) { switch (decodedElements[0]) {
case 'nop': case 'nop':
this.handlers?.onNop(this.user!); handler.onNop(user);
break; break;
case 'cap': case 'cap':
if (decodedElements.length < 2) return false; if (decodedElements.length < 2) return false;
this.handlers?.onCapabilityUpgrade(this.user!, decodedElements.slice(1)); handler.onCapabilityUpgrade(user, decodedElements.slice(1));
break; break;
case 'login': case 'login':
if (decodedElements.length !== 2) return false; if (decodedElements.length !== 2) return false;
this.handlers?.onLogin(this.user!, decodedElements[1]); handler.onLogin(user, decodedElements[1]);
break; break;
case 'noflag': case 'noflag':
this.handlers?.onNoFlag(this.user!); handler.onNoFlag(user);
break; break;
case 'list': case 'list':
this.handlers?.onList(this.user!); handler.onList(user);
break; break;
case 'connect': case 'connect':
if (decodedElements.length !== 2) return false; if (decodedElements.length !== 2) return false;
this.handlers?.onConnect(this.user!, decodedElements[1]); handler.onConnect(user, decodedElements[1]);
break; break;
case 'view': case 'view':
{ {
@@ -141,15 +136,15 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
let viewMode = parseInt(decodedElements[2]); let viewMode = parseInt(decodedElements[2]);
if (viewMode == undefined) return false; if (viewMode == undefined) return false;
this.handlers?.onView(this.user!, decodedElements[1], viewMode); handler.onView(user, decodedElements[1], viewMode);
} }
break; break;
case 'rename': case 'rename':
this.handlers?.onRename(this.user!, decodedElements[1]); handler.onRename(user, decodedElements[1]);
break; break;
case 'chat': case 'chat':
if (decodedElements.length !== 2) return false; if (decodedElements.length !== 2) return false;
this.handlers?.onChat(this.user!, decodedElements[1]); handler.onChat(user, decodedElements[1]);
break; break;
case 'turn': case 'turn':
let forfeit = false; let forfeit = false;
@@ -161,7 +156,7 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
else if (decodedElements[1] == '1') forfeit = false; else if (decodedElements[1] == '1') forfeit = false;
} }
this.handlers?.onTurnRequest(this.user!, forfeit); handler.onTurnRequest(user, forfeit);
break; break;
case 'mouse': case 'mouse':
if (decodedElements.length !== 4) return false; if (decodedElements.length !== 4) return false;
@@ -171,25 +166,25 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
let mask = parseInt(decodedElements[3]); let mask = parseInt(decodedElements[3]);
if (x === undefined || y === undefined || mask === undefined) return false; if (x === undefined || y === undefined || mask === undefined) return false;
this.handlers?.onMouse(this.user!, x, y, mask); handler.onMouse(user, x, y, mask);
break; break;
case 'key': case 'key':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
var keysym = parseInt(decodedElements[1]); var keysym = parseInt(decodedElements[1]);
var down = parseInt(decodedElements[2]); var down = parseInt(decodedElements[2]);
if (keysym === undefined || (down !== 0 && down !== 1)) return false; if (keysym === undefined || (down !== 0 && down !== 1)) return false;
this.handlers?.onKey(this.user!, keysym, down === 1); handler.onKey(user, keysym, down === 1);
break; break;
case 'vote': case 'vote':
if (decodedElements.length !== 2) return false; if (decodedElements.length !== 2) return false;
let choice = parseInt(decodedElements[1]); let choice = parseInt(decodedElements[1]);
if (choice == undefined) return false; if (choice == undefined) return false;
this.handlers?.onVote(this.user!, choice); handler.onVote(user, choice);
break; break;
case 'admin': case 'admin':
if (decodedElements.length < 2) return false; if (decodedElements.length < 2) return false;
return this.__processMessage_admin(decodedElements); return this.__processMessage_admin(user, handler, decodedElements);
} }
return true; return true;
@@ -197,109 +192,109 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
// Senders // Senders
sendAuth(authServer: string): void { sendAuth(user: User, authServer: string): void {
this.user?.sendMsg(cvm.guacEncode('auth', authServer)); user.sendMsg(cvm.guacEncode('auth', authServer));
} }
sendNop(): void { sendNop(user: User): void {
this.user?.sendMsg(cvm.guacEncode('nop')); user.sendMsg(cvm.guacEncode('nop'));
} }
sendSync(now: number): void { sendSync(user: User, now: number): void {
this.user?.sendMsg(cvm.guacEncode('sync', now.toString())); user.sendMsg(cvm.guacEncode('sync', now.toString()));
} }
sendCapabilities(caps: ProtocolUpgradeCapability[]): void { sendCapabilities(user: User, caps: ProtocolUpgradeCapability[]): void {
let arr = ['cap', ...caps]; let arr = ['cap', ...caps];
this?.user?.sendMsg(cvm.guacEncode(...arr)); user.sendMsg(cvm.guacEncode(...arr));
} }
sendConnectFailResponse(): void { sendConnectFailResponse(user: User): void {
this.user?.sendMsg(cvm.guacEncode('connect', '0')); user.sendMsg(cvm.guacEncode('connect', '0'));
} }
sendConnectOKResponse(votes: boolean): void { sendConnectOKResponse(user: User, votes: boolean): void {
this.user?.sendMsg(cvm.guacEncode('connect', '1', '1', votes ? '1' : '0', '0')); user.sendMsg(cvm.guacEncode('connect', '1', '1', votes ? '1' : '0', '0'));
} }
sendLoginResponse(ok: boolean, message: string | undefined): void { sendLoginResponse(user: User, ok: boolean, message: string | undefined): void {
if (ok) { if (ok) {
this.user?.sendMsg(cvm.guacEncode('login', '1')); user.sendMsg(cvm.guacEncode('login', '1'));
return; return;
} else { } else {
this.user?.sendMsg(cvm.guacEncode('login', '0', message!)); user.sendMsg(cvm.guacEncode('login', '0', message!));
} }
} }
sendAdminLoginResponse(ok: boolean, modPerms: number | undefined): void { sendAdminLoginResponse(user: User, ok: boolean, modPerms: number | undefined): void {
if (ok) { if (ok) {
if (modPerms == undefined) { if (modPerms == undefined) {
this.user?.sendMsg(cvm.guacEncode('admin', '0', '1')); user.sendMsg(cvm.guacEncode('admin', '0', '1'));
} else { } else {
this.user?.sendMsg(cvm.guacEncode('admin', '0', '3', modPerms.toString())); user.sendMsg(cvm.guacEncode('admin', '0', '3', modPerms.toString()));
} }
} else { } else {
this.user?.sendMsg(cvm.guacEncode('admin', '0', '0')); user.sendMsg(cvm.guacEncode('admin', '0', '0'));
} }
} }
sendAdminMonitorResponse(output: string): void { sendAdminMonitorResponse(user: User, output: string): void {
this.user?.sendMsg(cvm.guacEncode('admin', '2', output)); user.sendMsg(cvm.guacEncode('admin', '2', output));
} }
sendAdminIPResponse(username: string, ip: string): void { sendAdminIPResponse(user: User, username: string, ip: string): void {
this.user?.sendMsg(cvm.guacEncode('admin', '19', username, ip)); user.sendMsg(cvm.guacEncode('admin', '19', username, ip));
} }
sendChatMessage(username: string, message: string): void { sendChatMessage(user: User, username: string, message: string): void {
this.user?.sendMsg(cvm.guacEncode('chat', username, message)); user.sendMsg(cvm.guacEncode('chat', username, message));
} }
sendChatHistoryMessage(history: ProtocolChatHistory[]): void { sendChatHistoryMessage(user: User, history: ProtocolChatHistory[]): void {
let arr = ['chat']; let arr = ['chat'];
for (let a of history) { for (let a of history) {
arr.push(a.user, a.msg); arr.push(a.user, a.msg);
} }
this.user?.sendMsg(cvm.guacEncode(...arr)); user.sendMsg(cvm.guacEncode(...arr));
} }
sendAddUser(users: ProtocolAddUser[]): void { sendAddUser(user: User, users: ProtocolAddUser[]): void {
let arr = ['adduser', users.length.toString()]; let arr = ['adduser', users.length.toString()];
for (let user of users) { for (let user of users) {
arr.push(user.username); arr.push(user.username);
arr.push(user.rank.toString()); arr.push(user.rank.toString());
} }
this.user?.sendMsg(cvm.guacEncode(...arr)); user.sendMsg(cvm.guacEncode(...arr));
} }
sendRemUser(users: string[]): void { sendRemUser(user: User, users: string[]): void {
let arr = ['remuser', users.length.toString()]; let arr = ['remuser', users.length.toString()];
for (let user of users) { for (let user of users) {
arr.push(user); arr.push(user);
} }
this.user?.sendMsg(cvm.guacEncode(...arr)); user.sendMsg(cvm.guacEncode(...arr));
} }
sendFlag(flag: ProtocolFlag[]): void { sendFlag(user: User, flag: ProtocolFlag[]): void {
// Basically this does the same as the above manual for of things // Basically this does the same as the above manual for of things
// but in one line of code // but in one line of code
let arr = ['flag', ...flag.flatMap((flag) => [flag.username, flag.countryCode])]; let arr = ['flag', ...flag.flatMap((flag) => [flag.username, flag.countryCode])];
this.user?.sendMsg(cvm.guacEncode(...arr)); user.sendMsg(cvm.guacEncode(...arr));
} }
sendSelfRename(status: ProtocolRenameStatus, newUsername: string, rank: Rank): void { sendSelfRename(user: User, status: ProtocolRenameStatus, newUsername: string, rank: Rank): void {
this.user?.sendMsg(cvm.guacEncode('rename', '0', status.toString(), newUsername)); user.sendMsg(cvm.guacEncode('rename', '0', status.toString(), newUsername));
} }
sendRename(oldUsername: string, newUsername: string, rank: Rank): void { sendRename(user: User, oldUsername: string, newUsername: string, rank: Rank): void {
this.user?.sendMsg(cvm.guacEncode('rename', '1', oldUsername, newUsername)); user.sendMsg(cvm.guacEncode('rename', '1', oldUsername, newUsername));
} }
sendListResponse(list: ListEntry[]): void { sendListResponse(user: User, list: ListEntry[]): void {
let arr = ['list']; let arr = ['list'];
for (let node of list) { for (let node of list) {
arr.push(node.id); arr.push(node.id);
@@ -307,45 +302,45 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
arr.push(node.thumbnail.toString('base64')); arr.push(node.thumbnail.toString('base64'));
} }
this.user?.sendMsg(cvm.guacEncode(...arr)); user.sendMsg(cvm.guacEncode(...arr));
} }
sendVoteStarted(): void { sendVoteStarted(user: User): void {
this.user?.sendMsg(cvm.guacEncode('vote', '0')); user.sendMsg(cvm.guacEncode('vote', '0'));
} }
sendVoteStats(msLeft: number, nrYes: number, nrNo: number): void { sendVoteStats(user: User, msLeft: number, nrYes: number, nrNo: number): void {
this.user?.sendMsg(cvm.guacEncode('vote', '1', msLeft.toString(), nrYes.toString(), nrNo.toString())); user.sendMsg(cvm.guacEncode('vote', '1', msLeft.toString(), nrYes.toString(), nrNo.toString()));
} }
sendVoteEnded(): void { sendVoteEnded(user: User): void {
this.user?.sendMsg(cvm.guacEncode('vote', '2')); user.sendMsg(cvm.guacEncode('vote', '2'));
} }
sendVoteCooldown(ms: number): void { sendVoteCooldown(user: User, ms: number): void {
this.user?.sendMsg(cvm.guacEncode('vote', '3', ms.toString())); user.sendMsg(cvm.guacEncode('vote', '3', ms.toString()));
} }
private getTurnQueueBase(turnTime: number, users: string[]): string[] { private getTurnQueueBase(turnTime: number, users: string[]): string[] {
return ['turn', turnTime.toString(), users.length.toString(), ...users]; return ['turn', turnTime.toString(), users.length.toString(), ...users];
} }
sendTurnQueue(turnTime: number, users: string[]): void { sendTurnQueue(user: User, turnTime: number, users: string[]): void {
this.user?.sendMsg(cvm.guacEncode(...this.getTurnQueueBase(turnTime, users))); user.sendMsg(cvm.guacEncode(...this.getTurnQueueBase(turnTime, users)));
} }
sendTurnQueueWaiting(turnTime: number, users: string[], waitTime: number): void { sendTurnQueueWaiting(user: User, turnTime: number, users: string[], waitTime: number): void {
let queue = this.getTurnQueueBase(turnTime, users); let queue = this.getTurnQueueBase(turnTime, users);
queue.push(waitTime.toString()); queue.push(waitTime.toString());
this.user?.sendMsg(cvm.guacEncode(...queue)); user.sendMsg(cvm.guacEncode(...queue));
} }
sendScreenResize(width: number, height: number): void { sendScreenResize(user: User, width: number, height: number): void {
this.user?.sendMsg(cvm.guacEncode('size', '0', width.toString(), height.toString())); user.sendMsg(cvm.guacEncode('size', '0', width.toString(), height.toString()));
} }
sendScreenUpdate(rect: ScreenRect): void { sendScreenUpdate(user: User, rect: ScreenRect): void {
this.user?.sendMsg(cvm.guacEncode('png', '0', '0', rect.x.toString(), rect.y.toString(), rect.data.toString('base64'))); user.sendMsg(cvm.guacEncode('png', '0', '0', rect.x.toString(), rect.y.toString(), rect.data.toString('base64')));
this.sendSync(Date.now()); this.sendSync(user, Date.now());
} }
} }

View File

@@ -1,24 +1,23 @@
import { IProtocol } from "./Protocol"; import { IProtocol } from './Protocol';
import { User } from "../User"; import { User } from '../User';
// The protocol manager. Holds protocol factories, and provides the ability // The protocol manager.
// to create a protocol by name. Avoids direct dependency on a given list of protocols, // Holds protocols, and provides the ability to obtain them by name.
// and allows (relatively simple) expansion. //
// Avoids direct dependency on a given list of protocols,
// and allows (relatively simple) expansion of the supported protocols.
export class ProtocolManager { export class ProtocolManager {
private protocols = new Map<String, () => IProtocol>(); private protocols = new Map<String, IProtocol>();
// Registers a protocol with the given name. // Registers a protocol with the given name, creates it, and stores it for later use.
registerProtocol(name: string, protocolFactory: () => IProtocol) { registerProtocol(name: string, protocolFactory: () => IProtocol) {
if (!this.protocols.has(name)) this.protocols.set(name, protocolFactory); if (!this.protocols.has(name)) this.protocols.set(name, protocolFactory());
} }
// Creates an instance of a given protocol for a user. // Gets an instance of a protocol.
createProtocol(name: string, user: User): IProtocol { getProtocol(name: string): IProtocol {
if (!this.protocols.has(name)) throw new Error(`ProtocolManager does not have protocol \"${name}\"`); let proto = this.protocols.get(name);
if (proto == undefined) throw new Error(`ProtocolManager does not have protocol \"${name}\"`);
let factory = this.protocols.get(name)!;
let proto = factory();
proto.init(user);
return proto; return proto;
} }
} }

View File

@@ -90,13 +90,6 @@ export interface IProtocolMessageHandler {
// allowing it to be protocol-independent (as long as the client and server // allowing it to be protocol-independent (as long as the client and server
// are able to speak the same protocol.) // are able to speak the same protocol.)
export interface IProtocol { export interface IProtocol {
// don't implement this yourself, extend from ProtocolBase
init(u: User): void;
dispose(): void;
// Sets handler object.
setHandler(handlers: IProtocolMessageHandler): void;
// Protocol implementation stuff // Protocol implementation stuff
// Parses a single message and fires the given handler with deserialized arguments. // Parses a single message and fires the given handler with deserialized arguments.
@@ -104,67 +97,48 @@ export interface IProtocol {
// to handle errors. It should, however, catch invalid parameters without failing. // to handle errors. It should, however, catch invalid parameters without failing.
// //
// This function will perform conversion to text if it is required. // This function will perform conversion to text if it is required.
processMessage(buffer: Buffer): boolean; processMessage(user: User, handler: IProtocolMessageHandler, buffer: Buffer): boolean;
// Senders // Senders
sendNop(): void; sendNop(user: User): void;
sendSync(now: number): void; sendSync(user: User, now: number): void;
sendAuth(authServer: string): void; sendAuth(user: User, authServer: string): void;
sendCapabilities(caps: ProtocolUpgradeCapability[]): void; sendCapabilities(user: User, caps: ProtocolUpgradeCapability[]): void;
sendConnectFailResponse(): void; sendConnectFailResponse(user: User): void;
sendConnectOKResponse(votes: boolean): void; sendConnectOKResponse(user: User, votes: boolean): void;
sendLoginResponse(ok: boolean, message: string | undefined): void; sendLoginResponse(user: User, ok: boolean, message: string | undefined): void;
sendAdminLoginResponse(ok: boolean, modPerms: number | undefined): void; sendAdminLoginResponse(user: User, ok: boolean, modPerms: number | undefined): void;
sendAdminMonitorResponse(output: string): void; sendAdminMonitorResponse(user: User, output: string): void;
sendAdminIPResponse(username: string, ip: string): void; sendAdminIPResponse(user: User, username: string, ip: string): void;
sendChatMessage(username: '' | string, message: string): void; sendChatMessage(user: User, username: '' | string, message: string): void;
sendChatHistoryMessage(history: ProtocolChatHistory[]): void; sendChatHistoryMessage(user: User, history: ProtocolChatHistory[]): void;
sendAddUser(users: ProtocolAddUser[]): void; sendAddUser(user: User, users: ProtocolAddUser[]): void;
sendRemUser(users: string[]): void; sendRemUser(user: User, users: string[]): void;
sendFlag(flag: ProtocolFlag[]): void; sendFlag(user: User, flag: ProtocolFlag[]): void;
sendSelfRename(status: ProtocolRenameStatus, newUsername: string, rank: Rank): void; sendSelfRename(user: User, status: ProtocolRenameStatus, newUsername: string, rank: Rank): void;
sendRename(oldUsername: string, newUsername: string, rank: Rank): void; sendRename(user: User, oldUsername: string, newUsername: string, rank: Rank): void;
sendListResponse(list: ListEntry[]): void; sendListResponse(user: User, list: ListEntry[]): void;
sendTurnQueue(turnTime: number, users: string[]): void; sendTurnQueue(user: User, turnTime: number, users: string[]): void;
sendTurnQueueWaiting(turnTime: number, users: string[], waitTime: number): void; sendTurnQueueWaiting(user: User, turnTime: number, users: string[], waitTime: number): void;
sendVoteStarted(): void; sendVoteStarted(user: User): void;
sendVoteStats(msLeft: number, nrYes: number, nrNo: number): void; sendVoteStats(user: User, msLeft: number, nrYes: number, nrNo: number): void;
sendVoteEnded(): void; sendVoteEnded(user: User): void;
sendVoteCooldown(ms: number): void; sendVoteCooldown(user: User, ms: number): void;
sendScreenResize(width: number, height: number): void; sendScreenResize(user: User, width: number, height: number): void;
// Sends a rectangle update to the user. // Sends a rectangle update to the user.
sendScreenUpdate(rect: ScreenRect): void; sendScreenUpdate(user: User, rect: ScreenRect): void;
}
// Base mixin for all concrete protocols to use. Inherit from this!
export class ProtocolBase {
protected handlers: IProtocolMessageHandler | null = null;
protected user: User | null = null;
init(u: User): void {
this.user = u;
}
dispose(): void {
this.user = null;
this.handlers = null;
}
setHandler(handlers: IProtocolMessageHandler): void {
this.handlers = handlers;
}
} }