Skip to content

Commit

Permalink
Connection disconnect reason (#32)
Browse files Browse the repository at this point in the history
- Adds disconnect packets that can be used to send the client a
disconnect reason
- Adds connection states. Depending on the connection state, a different
kick packet is used
- Adds `connection.disconnect` optional reason (sent as a kick packet)
- When server is shutting down, the configured kick message is sent to
all connections in a valid state. By default, `Server closed`
  • Loading branch information
zefir-git authored Aug 11, 2023
2 parents 99a0b17 + ec58cf4 commit 280f4af
Show file tree
Hide file tree
Showing 16 changed files with 192 additions and 38 deletions.
3 changes: 2 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"port": "25565"
"port": "25565",
"shutdownKickReason": "Server closed"
}
2 changes: 1 addition & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ process.on("SIGINT", () => {
});

server.on("packet.LoginPacket", (packet, conn) => {
new LoginSuccessPacket(packet.data.uuid, packet.data.username).send(conn);
new LoginSuccessPacket(packet.data.uuid, packet.data.username).send(conn).then();
});
5 changes: 5 additions & 0 deletions src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import Logger from "./Logger.js";
export default class Config {
public port: number = 25565;

/**
* Kick reason for when the server is shutting down
*/
public shutdownKickReason: string = "Server closed";

/**
* Get a Config instance from a json file
* @param file The file to read from
Expand Down
60 changes: 56 additions & 4 deletions src/Connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Packet from "./Packet.js";
/**
* A TCP socket connection to the server.
*/
export default class Connection {
class Connection {
/**
* A unique identifier for this connection.
*/
Expand All @@ -19,6 +19,22 @@ export default class Connection {
* The server to which this connection belongs.
*/
public readonly server: Server;
/**
* The state of the connection.
*/
#state: Connection.State = Connection.State.NONE;

/**
* The state of the connection.
*/
public get state(): Connection.State {
return this.#state;
}

/** @internal */
public _setState(state: Connection.State): void {
this.#state = state;
}

/**
* Packet fragment this connection is currently sending to the server.
Expand All @@ -37,9 +53,9 @@ export default class Connection {
if (this.currentPacketFragment.push(data)) {
const p = this.currentPacketFragment.getTypedClient();
if (p) {
p.execute(this, this.server);
this.server.emit("packet", p, this);
this.server.emit(`packet.${p.constructor.name}` as any, p, this);
p.execute(this, this.server);
}
else this.server.emit("unknownPacket", this.currentPacketFragment, this);
this.currentPacketFragment = new Packet();
Expand All @@ -48,9 +64,10 @@ export default class Connection {

/**
* Disconnect this connection.
* @param [reason] The reason for the disconnect.
*/
public disconnect(): Promise<boolean> {
return this.server.connections.disconnect(this.id);
public disconnect(reason?: string): Promise<boolean> {
return this.server.connections.disconnect(this.id, reason);
}

/**
Expand All @@ -60,3 +77,38 @@ export default class Connection {
return !this.socket.destroyed && this.server.connections.get(this.id) !== null;
}
}

namespace Connection {
/**
* Connection state
*/
export enum State {
/**
* None / unknown
*/
NONE,

/**
* Status state
*
* Sender is checking server status
*/
STATUS,

/**
* Login state
*
* Player is connecting to the server
*/
LOGIN,

/**
* Play state
*
* Player is online and communicating game data
*/
PLAY
}
}

export default Connection;
48 changes: 32 additions & 16 deletions src/ConnectionPool.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Connection from "./Connection";
import Connection from "./Connection.js";
import DisconnectLoginPacket from "./packet/server/DisconnectLoginPacket.js";
import DisconnectPlayPacket from "./packet/server/DisconnectPlayPacket.js";

export default class ConnectionPool {
private readonly connections: Connection[] = [];
Expand All @@ -22,28 +24,42 @@ export default class ConnectionPool {

/**
* Disconnect all connections
* @param [reason] The reason for the disconnect
* @returns Whether all connections disconnected successfully
*/
public async disconnect(): Promise<boolean>;
public async disconnectAll (reason?: string): Promise<boolean> {
const promises: Promise<boolean>[] = [];
for (const connection of this.connections)
promises.push(this.disconnect(connection.id, reason));
return (await Promise.all(promises)).every(result => result);
}

/**
* Disconnect a connection
* @param id The ID of the connection to disconnect
* @param [reason] The reason for the disconnect
* @returns Whether the connection was found and disconnected
*/
public async disconnect(id: string): Promise<boolean>;
public async disconnect(id?: string): Promise<boolean> {
const promises: Promise<boolean>[] = [];
if (id) {
const connection = this.get(id);
if (!connection) return false;
const index = this.connections.indexOf(connection);
if (index === -1) return false;
this.connections.splice(index, 1);
connection.server.emit("disconnect", connection);
promises.push(new Promise(resolve => connection.socket.end(() => resolve(true))));
public async disconnect(id: string, reason?: string): Promise<boolean> {
const connection = this.get(id);
if (!connection) return false;
const index = this.connections.indexOf(connection);
if (index === -1) return false;
if (reason) switch (connection.state) {
case Connection.State.LOGIN: {
await new DisconnectLoginPacket({text: reason}).send(connection);
break;
}
case Connection.State.PLAY: {
await new DisconnectPlayPacket({text: reason}).send(connection);
break;
}
default: {
connection.server.logger.warn("Cannot set disconnect reason for state " + Connection.State[connection.state] + " on connection " + connection.id);
}
}
else for (const connection of this.connections)
promises.push(this.disconnect(connection.id));
return (await Promise.all(promises)).every(result => result);
this.connections.splice(index, 1);
connection.server.emit("disconnect", connection);
return new Promise(resolve => connection.socket.end(() => resolve(true)));
}
}
18 changes: 17 additions & 1 deletion src/Packet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ParsedPacket from "./ParsedPacket.js";
import {TypedClientPacket, TypedClientPacketStatic} from "./TypedPacket";
import {TypedClientPacket, TypedClientPacketStatic} from "./types/TypedPacket";
import HandshakePacket from "./packet/client/HandshakePacket.js";
import LoginPacket from "./packet/client/LoginPacket.js";

Expand Down Expand Up @@ -178,6 +178,22 @@ export default class Packet {
return buffer;
}

/**
* Parse chat
* @param buffer
*/
public static parseChat(buffer: Buffer): ChatComponent {
return JSON.parse(Packet.parseString(buffer)) as ChatComponent;
}

/**
* Write chat
* @param value
*/
public static writeChat(value: ChatComponent): Buffer {
return Packet.writeString(JSON.stringify(value));
}

/**
* Get typed client packet
*/
Expand Down
8 changes: 4 additions & 4 deletions src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import path from "node:path";
import Packet from "./Packet.js";
import Config from "./Config.js";
import Logger from "./Logger.js";
import {TypedClientPacket} from "./TypedPacket";
import TypedEventEmitter from "./TypedEventEmitter";
import {TypedClientPacket} from "./types/TypedPacket";
import TypedEventEmitter from "./types/TypedEventEmitter";
import ConnectionPool from "./ConnectionPool.js";
import Connection from "./Connection.js";
import HandshakePacket from "./packet/client/HandshakePacket";
Expand Down Expand Up @@ -91,8 +91,8 @@ export default class Server extends (EventEmitter as new () => TypedEventEmitter
else resolve(void 0);
});
}),
this.connections.disconnect(),
]).then(() => void 0);
this.connections.disconnectAll(this.config.shutdownKickReason),
]);
this.emit("closed");
}

Expand Down
9 changes: 7 additions & 2 deletions src/ServerPacket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ export default abstract class ServerPacket extends Packet {
* Send packet to a connection
* @param connection
*/
public send(connection: Connection): void {
connection.socket.write(this.dataBuffer);
public send(connection: Connection): Promise<void> {
return new Promise((resolve, reject) => {
connection.socket.write(this.dataBuffer, (err) => {
if (err) reject(err);
else resolve();
});
});
}
}
8 changes: 5 additions & 3 deletions src/packet/client/HandshakePacket.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {TypedClientPacket, TypedClientPacketStatic} from "../../TypedPacket";
import {TypedClientPacket, TypedClientPacketStatic} from "../../types/TypedPacket";
import StaticImplements from "../../decorator/StaticImplements.js";
import Server from "../../Server";
import ParsedPacket from "../../ParsedPacket";
import Connection from "../../Connection";
import Connection from "../../Connection.js";

@StaticImplements<TypedClientPacketStatic>()
export default class HandshakePacket {
Expand All @@ -25,7 +25,9 @@ export default class HandshakePacket {
} as const;
}

execute(_conn: Connection, _server: Server): void {}
execute(conn: Connection, _server: Server): void {
conn._setState(Connection.State.LOGIN);
}

public static readonly id = 0x00;

Expand Down
8 changes: 5 additions & 3 deletions src/packet/client/LoginPacket.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {TypedClientPacket, TypedClientPacketStatic} from "../../TypedPacket";
import {TypedClientPacket, TypedClientPacketStatic} from "../../types/TypedPacket";
import StaticImplements from "../../decorator/StaticImplements.js";
import ParsedPacket from "../../ParsedPacket.js";
import Server from "../../Server";
import Connection from "../../Connection";
import Connection from "../../Connection.js";

@StaticImplements<TypedClientPacketStatic>()
export default class LoginPacket {
Expand All @@ -24,7 +24,9 @@ export default class LoginPacket {
}
}

execute(_conn: Connection, _server: Server): void {}
execute(conn: Connection, _server: Server): void {
conn._setState(Connection.State.LOGIN);
}

public static readonly id = 0x00;

Expand Down
12 changes: 12 additions & 0 deletions src/packet/server/DisconnectLoginPacket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import ServerPacket from "../../ServerPacket.js";

export default class DisconnectLoginPacket extends ServerPacket {
public static readonly id = 0x00;

public constructor(reason: ChatComponent) {
super(Buffer.concat([
ServerPacket.writeVarInt(DisconnectLoginPacket.id),
ServerPacket.writeChat(reason)
]));
}
}
12 changes: 12 additions & 0 deletions src/packet/server/DisconnectPlayPacket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import ServerPacket from "../../ServerPacket.js";

export default class DisconnectPlayPacket extends ServerPacket {
public static readonly id = 0x1a;

public constructor(reason: ChatComponent) {
super(Buffer.concat([
ServerPacket.writeVarInt(DisconnectPlayPacket.id),
ServerPacket.writeChat(reason)
]));
}
}
6 changes: 6 additions & 0 deletions src/packet/server/LoginSuccessPacket.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ServerPacket from "../../ServerPacket.js";
import Connection from "../../Connection.js";

/**
* A Minecraft protocol client-bound LoginSuccess packet.
Expand All @@ -14,4 +15,9 @@ export default class LoginSuccessPacket extends ServerPacket {
ServerPacket.writeVarInt(0)
]));
}

public override send(connection: Connection) {
connection._setState(Connection.State.PLAY);
return super.send(connection);
}
}
25 changes: 25 additions & 0 deletions src/types/ChatComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
type ChatComponent = {
bold?: boolean;
italic?: boolean;
underlined?: boolean;
strikethrough?: boolean;
obfuscated?: boolean;
font?: string;
color?: string;
insertion?: string;
clickEvent?: {
action: "open_url" | "open_file" | "run_command" | "suggest_command" | "change_page" | "copy_to_clipboard";
value: string;
}
hoverEvent?: {
action: "show_text" | "show_item" | "show_entity";
}
text?: string;
extra?: [ChatComponent, ...ChatComponent[]];
} | {translate: string; with?: ChatComponent[]} | {keybind: string} | {
score: {
name: string;
objective: string;
value?: string;
}
};
File renamed without changes.
6 changes: 3 additions & 3 deletions src/TypedPacket.ts → src/types/TypedPacket.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Server from "./Server";
import ParsedPacket from "./ParsedPacket";
import Connection from "./Connection";
import Server from "../Server";
import ParsedPacket from "../ParsedPacket";
import Connection from "../Connection";

export interface TypedClientPacket {
readonly packet: ParsedPacket;
Expand Down

0 comments on commit 280f4af

Please sign in to comment.