From 717375ffb40c0d07a5994c18118de461f8466d6f Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 17 Aug 2023 20:20:19 +0300 Subject: [PATCH 1/9] allow to create custom client & communication between client --- src/client.js | 182 +++++++++++++++++++++++++------------------- src/createClient.js | 5 +- src/index.d.ts | 11 ++- 3 files changed, 114 insertions(+), 84 deletions(-) diff --git a/src/client.js b/src/client.js index 6f1182337..e05d9231c 100644 --- a/src/client.js +++ b/src/client.js @@ -13,9 +13,11 @@ const createDecipher = require('./transforms/encryption').createDecipher const closeTimeout = 30 * 1000 class Client extends EventEmitter { - constructor (isServer, version, customPackets, hideErrors = false) { + constructor (isServer, version, customPackets, hideErrors = false, customCommunication = undefined) { super() this.customPackets = customPackets + // if defined, we don't pipe data into anything, so we don't use serializer / deserializer + this.customCommunication = customCommunication this.version = version this.isServer = !!isServer this.splitter = framing.createSplitter() @@ -39,44 +41,47 @@ class Client extends EventEmitter { } setSerializer (state) { - this.serializer = createSerializer({ isServer: this.isServer, version: this.version, state, customPackets: this.customPackets }) - this.deserializer = createDeserializer({ - isServer: this.isServer, - version: this.version, - state, - packetsToParse: - this.packetsToParse, - customPackets: this.customPackets, - noErrorLogging: this.hideErrors - }) - - this.splitter.recognizeLegacyPing = state === states.HANDSHAKING - - this.serializer.on('error', (e) => { - let parts - if (e.field) { - parts = e.field.split('.') - parts.shift() - } else { parts = [] } - const serializerDirection = !this.isServer ? 'toServer' : 'toClient' - e.field = [this.protocolState, serializerDirection].concat(parts).join('.') - e.message = `Serialization error for ${e.field} : ${e.message}` - if (!this.compressor) { this.serializer.pipe(this.framer) } else { this.serializer.pipe(this.compressor) } - this.emit('error', e) - }) - - this.deserializer.on('error', (e) => { - let parts - if (e.field) { - parts = e.field.split('.') - parts.shift() - } else { parts = [] } - const deserializerDirection = this.isServer ? 'toServer' : 'toClient' - e.field = [this.protocolState, deserializerDirection].concat(parts).join('.') - e.message = `Deserialization error for ${e.field} : ${e.message}` - if (!this.compressor) { this.splitter.pipe(this.deserializer) } else { this.decompressor.pipe(this.deserializer) } - this.emit('error', e) - }) + if (!this.customCommunication) { + this.serializer = createSerializer({ isServer: this.isServer, version: this.version, state, customPackets: this.customPackets }) + this.deserializer = createDeserializer({ + isServer: this.isServer, + version: this.version, + state, + packetsToParse: + this.packetsToParse, + customPackets: this.customPackets, + noErrorLogging: this.hideErrors + }) + + this.splitter.recognizeLegacyPing = state === states.HANDSHAKING + + this.serializer.on('error', (e) => { + let parts + if (e.field) { + parts = e.field.split('.') + parts.shift() + } else { parts = [] } + const serializerDirection = !this.isServer ? 'toServer' : 'toClient' + e.field = [this.protocolState, serializerDirection].concat(parts).join('.') + e.message = `Serialization error for ${e.field} : ${e.message}` + if (!this.compressor) { this.serializer.pipe(this.framer) } else { this.serializer.pipe(this.compressor) } + this.emit('error', e) + }) + + this.deserializer.on('error', (e) => { + let parts + if (e.field) { + parts = e.field.split('.') + parts.shift() + } else { parts = [] } + const deserializerDirection = this.isServer ? 'toServer' : 'toClient' + e.field = [this.protocolState, deserializerDirection].concat(parts).join('.') + e.message = `Deserialization error for ${e.field} : ${e.message}` + if (!this.compressor) { this.splitter.pipe(this.deserializer) } else { this.decompressor.pipe(this.deserializer) } + this.emit('error', e) + }) + } + this._mcBundle = [] const emitPacket = (parsed) => { this.emit('packet', parsed.data, parsed.metadata, parsed.buffer, parsed.fullBuffer) @@ -84,55 +89,67 @@ class Client extends EventEmitter { this.emit('raw.' + parsed.metadata.name, parsed.buffer, parsed.metadata) this.emit('raw', parsed.buffer, parsed.metadata) } - this.deserializer.on('data', (parsed) => { - parsed.metadata.name = parsed.data.name - parsed.data = parsed.data.params - parsed.metadata.state = state - debug('read packet ' + state + '.' + parsed.metadata.name) - if (debug.enabled) { - const s = JSON.stringify(parsed.data, null, 2) - debug(s && s.length > 10000 ? parsed.data : s) - } - if (parsed.metadata.name === 'bundle_delimiter') { - if (this._mcBundle.length) { // End bundle - this._mcBundle.forEach(emitPacket) - emitPacket(parsed) - this._mcBundle = [] - } else { // Start bundle + + if (this.customCommunication) { + this.customCommunication.receiverSetup((/** @type {{name, params, state?}} */parsed) => { + // debug(`receive in ${this.isServer ? 'server' : 'client'}: ${parsed.metadata.name}`) + this.emit(parsed.name, parsed.params, parsed) + }) + } else { + this.deserializer.on('data', (parsed) => { + parsed.metadata.name = parsed.data.name + parsed.data = parsed.data.params + parsed.metadata.state = state + debug('read packet ' + state + '.' + parsed.metadata.name) + if (debug.enabled) { + const s = JSON.stringify(parsed.data, null, 2) + debug(s && s.length > 10000 ? parsed.data : s) + } + if (parsed.metadata.name === 'bundle_delimiter') { + if (this._mcBundle.length) { // End bundle + this._mcBundle.forEach(emitPacket) + emitPacket(parsed) + this._mcBundle = [] + } else { // Start bundle + this._mcBundle.push(parsed) + } + } else if (this._mcBundle.length) { this._mcBundle.push(parsed) + } else { + emitPacket(parsed) } - } else if (this._mcBundle.length) { - this._mcBundle.push(parsed) - } else { - emitPacket(parsed) - } - }) + }) + } } set state (newProperty) { const oldProperty = this.protocolState this.protocolState = newProperty - if (this.serializer) { - if (!this.compressor) { - this.serializer.unpipe() - this.splitter.unpipe(this.deserializer) - } else { - this.serializer.unpipe(this.compressor) - this.decompressor.unpipe(this.deserializer) - } + if (!this.customCommunication) { + if (this.serializer) { + if (!this.compressor) { + this.serializer.unpipe() + this.splitter.unpipe(this.deserializer) + } else { + this.serializer.unpipe(this.compressor) + this.decompressor.unpipe(this.deserializer) + } - this.serializer.removeAllListeners() - this.deserializer.removeAllListeners() + this.serializer.removeAllListeners() + this.deserializer.removeAllListeners() + } } this.setSerializer(this.protocolState) - if (!this.compressor) { - this.serializer.pipe(this.framer) - this.splitter.pipe(this.deserializer) - } else { - this.serializer.pipe(this.compressor) - this.decompressor.pipe(this.deserializer) + if (!this.customCommunication) { + if (!this.compressor) { + this.serializer.pipe(this.framer) + this.splitter.pipe(this.deserializer) + } else { + this.serializer.pipe(this.compressor) + this.decompressor.pipe(this.deserializer) + } } this.emit('state', newProperty, oldProperty) @@ -185,6 +202,7 @@ class Client extends EventEmitter { } end (reason) { + if (this.customCommunication) return this._endReason = reason /* ending the serializer will end the whole chain serializer -> framer -> socket -> splitter -> deserializer */ @@ -214,6 +232,7 @@ class Client extends EventEmitter { } setCompressionThreshold (threshold) { + if (this.customCommunication) return if (this.compressor == null) { this.compressor = compression.createCompressor(threshold) this.compressor.on('error', (err) => this.emit('error', err)) @@ -230,10 +249,15 @@ class Client extends EventEmitter { } write (name, params) { - if (!this.serializer.writable) { return } - debug('writing packet ' + this.state + '.' + name) + if (!this.customCommunication && !this.serializer.writable) { return } + debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name) debug(params) - this.serializer.write({ name, params }) + + if (this.customCommunication) { + this.customCommunication.sendData({ name, params, state: this.state }) + } else { + this.serializer.write({ name, params }) + } } writeBundle (packets) { diff --git a/src/createClient.js b/src/createClient.js index 1c4f0330a..998060032 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -1,6 +1,6 @@ 'use strict' -const Client = require('./client') +const DefaultClient = require('./client') const assert = require('assert') const encrypt = require('./client/encrypt') @@ -32,7 +32,8 @@ function createClient (options) { options.protocolVersion = version.version const hideErrors = options.hideErrors || false - const client = new Client(false, version.minecraftVersion, options.customPackets, hideErrors) + const Client = options.customClient ?? DefaultClient + const client = new Client(false, version.minecraftVersion, options.customPackets, hideErrors, options.customCommunication) tcpDns(client, options) if (options.auth instanceof Function) { diff --git a/src/index.d.ts b/src/index.d.ts index 7135cbb5a..63a1c3555 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -117,7 +117,7 @@ declare module 'minecraft-protocol' { authTitle?: string sessionServer?: string keepAlive?: boolean - closeTimeout?: number + closeTimeout?: number noPongTimeout?: number checkTimeoutInterval?: number version?: string @@ -136,6 +136,11 @@ declare module 'minecraft-protocol' { realms?: RealmsOptions // 1.19+ disableChatSigning?: boolean + customCommunication?: { + sendData({ name, params, state }): void, + receiverSetup(callback: (data: { name, params, state? }) => void): void + } + customClient: Client } export class Server extends EventEmitter { @@ -162,7 +167,7 @@ declare module 'minecraft-protocol' { export interface ServerClient extends Client { id: number // You must call this function when the server receives a message from a player and that message gets - // broadcast to other players in player_chat packets. This function stores these packets so the server + // broadcast to other players in player_chat packets. This function stores these packets so the server // can then verify a player's lastSeenMessages field in inbound chat packets to ensure chain integrity. logSentMessageFromPeer(packet: object): boolean } @@ -201,7 +206,7 @@ declare module 'minecraft-protocol' { state?: States version: string } - + export interface MicrosoftDeviceAuthorizationResponse { device_code: string user_code: string From 0b2b5be208a00a9700a331bb17bbd639fea37048 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 17 Aug 2023 20:49:43 +0300 Subject: [PATCH 2/9] pass client context --- src/client.js | 4 ++-- src/index.d.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client.js b/src/client.js index e05d9231c..0fafa02b6 100644 --- a/src/client.js +++ b/src/client.js @@ -91,7 +91,7 @@ class Client extends EventEmitter { } if (this.customCommunication) { - this.customCommunication.receiverSetup((/** @type {{name, params, state?}} */parsed) => { + this.customCommunication.receiverSetup.call(this, (/** @type {{name, params, state?}} */parsed) => { // debug(`receive in ${this.isServer ? 'server' : 'client'}: ${parsed.metadata.name}`) this.emit(parsed.name, parsed.params, parsed) }) @@ -254,7 +254,7 @@ class Client extends EventEmitter { debug(params) if (this.customCommunication) { - this.customCommunication.sendData({ name, params, state: this.state }) + this.customCommunication.sendData.call(this, { name, params, state: this.state }) } else { this.serializer.write({ name, params }) } diff --git a/src/index.d.ts b/src/index.d.ts index 63a1c3555..30040d5e8 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -137,8 +137,8 @@ declare module 'minecraft-protocol' { // 1.19+ disableChatSigning?: boolean customCommunication?: { - sendData({ name, params, state }): void, - receiverSetup(callback: (data: { name, params, state? }) => void): void + sendData(this: Client, { name, params, state }): void, + receiverSetup(this: Client, callback: (data: { name, params, state? }) => void): void } customClient: Client } From 06cfd7970e229a5c0e07c80ff7c7148ca93c4abf Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 17 Aug 2023 20:58:01 +0300 Subject: [PATCH 3/9] customClient should not be required --- src/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.d.ts b/src/index.d.ts index 30040d5e8..9fb9e4ab4 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -140,7 +140,7 @@ declare module 'minecraft-protocol' { sendData(this: Client, { name, params, state }): void, receiverSetup(this: Client, callback: (data: { name, params, state? }) => void): void } - customClient: Client + customClient?: Client } export class Server extends EventEmitter { From 74555428a8353ad59cc9a8a7eef946e79ac30579 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sun, 3 Sep 2023 10:41:14 +0300 Subject: [PATCH 4/9] allow to override Server impl to use --- src/createServer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/createServer.js b/src/createServer.js index 4fa3477ee..a81e34b92 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,6 +1,6 @@ 'use strict' -const Server = require('./server') +const DefaultServerImpl = require('./server') const NodeRSA = require('node-rsa') const plugins = [ require('./server/handshake'), @@ -20,6 +20,7 @@ function createServer (options = {}) { motd = 'A Minecraft server', 'max-players': maxPlayersOld = 20, maxPlayers: maxPlayersNew = 20, + Server = DefaultServerImpl, version, favicon, customPackets, From 742157146e9a802b989e647aca01b6bad8799b5d Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 18 Sep 2023 15:06:27 +0300 Subject: [PATCH 5/9] better docs & typings --- docs/API.md | 4 ++++ src/index.d.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/API.md b/docs/API.md index 85af2d650..20b69eb79 100644 --- a/docs/API.md +++ b/docs/API.md @@ -34,6 +34,7 @@ automatically logged in and validated against mojang's auth. * enforceSecureProfile (optional) : Kick clients that do not have chat signing keys from Mojang (1.19+) * generatePreview (optional) : Function to generate chat previews. Takes the raw message string and should return the message preview as a string. (1.19-1.19.2) * socketType (optional) : either `tcp` or `ipc`. Switches from a tcp connection to a ipc socket connection (or named pipes on windows). With the `ipc` option `host` becomes the path off the ipc connection on the local filesystem. Example: `\\.\pipe\minecraft-ipc` (Windows) `/tmp/minecraft-ipc.sock` (unix based systems). See the ipcConnection example for an example. + * Server : You can pass a custom server class to use instead of the default one. ## mc.Server(version,[customPackets]) @@ -144,6 +145,9 @@ Returns a `Client` instance and perform login. * realms : An object which should contain one of the following properties: `realmId` or `pickRealm`. When defined will attempt to join a Realm without needing to specify host/port. **The authenticated account must either own the Realm or have been invited to it** * realmId : The id of the Realm to join. * pickRealm(realms) : A function which will have an array of the user Realms (joined/owned) passed to it. The function should return a Realm. + * customCommunication : Allow to create a custom communication between the client and the server so data can be sent and received in a custom way without having to serialize it. Otherwise you should use `socket` option instead where you can supply custom duplex. + * `receiverSetup(this: Client, callback: (data: { name, params }) => void)`: Called when the client is ready to receive data from the server. The `callback` should be called with the handler that will receive the data. + * `sendData(this: Client, { name, params, state })`: Called when the client wants to send data to the server. The `name` is the name of the packet, `params` is the data to send. ## mc.Client(isServer,version,[customPackets]) diff --git a/src/index.d.ts b/src/index.d.ts index 9fb9e4ab4..9f9797d44 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -166,9 +166,9 @@ declare module 'minecraft-protocol' { export interface ServerClient extends Client { id: number - // You must call this function when the server receives a message from a player and that message gets - // broadcast to other players in player_chat packets. This function stores these packets so the server - // can then verify a player's lastSeenMessages field in inbound chat packets to ensure chain integrity. + /** You must call this function when the server receives a message from a player and that message gets + broadcast to other players in player_chat packets. This function stores these packets so the server + can then verify a player's lastSeenMessages field in inbound chat packets to ensure chain integrity. */ logSentMessageFromPeer(packet: object): boolean } @@ -192,12 +192,12 @@ declare module 'minecraft-protocol' { hideErrors?: boolean agent?: Agent validateChannelProtocol?: boolean - // 1.19+ - // Require connecting clients to have chat signing support enabled + /** (1.19+) Require connecting clients to have chat signing support enabled */ enforceSecureProfile?: boolean - // 1.19.1 & 1.19.2 only: If client should send previews of messages they are typing to the server + /** 1.19.1 & 1.19.2 only: If client should send previews of messages they are typing to the server */ enableChatPreview?: boolean socketType?: 'tcp' | 'ipc' + Server?: Server } export interface SerializerOptions { From b675d6ec1c0567b55e95607d726c33033dc810fe Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 18 Sep 2023 15:24:00 +0300 Subject: [PATCH 6/9] refactor: add new client class for customCommunication --- src/client.js | 182 ++++++++++++++++--------------------- src/client/compress.js | 1 + src/createClient.js | 12 ++- src/customChannelClient.js | 60 ++++++++++++ 4 files changed, 149 insertions(+), 106 deletions(-) create mode 100644 src/customChannelClient.js diff --git a/src/client.js b/src/client.js index 0fafa02b6..6f1182337 100644 --- a/src/client.js +++ b/src/client.js @@ -13,11 +13,9 @@ const createDecipher = require('./transforms/encryption').createDecipher const closeTimeout = 30 * 1000 class Client extends EventEmitter { - constructor (isServer, version, customPackets, hideErrors = false, customCommunication = undefined) { + constructor (isServer, version, customPackets, hideErrors = false) { super() this.customPackets = customPackets - // if defined, we don't pipe data into anything, so we don't use serializer / deserializer - this.customCommunication = customCommunication this.version = version this.isServer = !!isServer this.splitter = framing.createSplitter() @@ -41,47 +39,44 @@ class Client extends EventEmitter { } setSerializer (state) { - if (!this.customCommunication) { - this.serializer = createSerializer({ isServer: this.isServer, version: this.version, state, customPackets: this.customPackets }) - this.deserializer = createDeserializer({ - isServer: this.isServer, - version: this.version, - state, - packetsToParse: - this.packetsToParse, - customPackets: this.customPackets, - noErrorLogging: this.hideErrors - }) - - this.splitter.recognizeLegacyPing = state === states.HANDSHAKING - - this.serializer.on('error', (e) => { - let parts - if (e.field) { - parts = e.field.split('.') - parts.shift() - } else { parts = [] } - const serializerDirection = !this.isServer ? 'toServer' : 'toClient' - e.field = [this.protocolState, serializerDirection].concat(parts).join('.') - e.message = `Serialization error for ${e.field} : ${e.message}` - if (!this.compressor) { this.serializer.pipe(this.framer) } else { this.serializer.pipe(this.compressor) } - this.emit('error', e) - }) - - this.deserializer.on('error', (e) => { - let parts - if (e.field) { - parts = e.field.split('.') - parts.shift() - } else { parts = [] } - const deserializerDirection = this.isServer ? 'toServer' : 'toClient' - e.field = [this.protocolState, deserializerDirection].concat(parts).join('.') - e.message = `Deserialization error for ${e.field} : ${e.message}` - if (!this.compressor) { this.splitter.pipe(this.deserializer) } else { this.decompressor.pipe(this.deserializer) } - this.emit('error', e) - }) - } - + this.serializer = createSerializer({ isServer: this.isServer, version: this.version, state, customPackets: this.customPackets }) + this.deserializer = createDeserializer({ + isServer: this.isServer, + version: this.version, + state, + packetsToParse: + this.packetsToParse, + customPackets: this.customPackets, + noErrorLogging: this.hideErrors + }) + + this.splitter.recognizeLegacyPing = state === states.HANDSHAKING + + this.serializer.on('error', (e) => { + let parts + if (e.field) { + parts = e.field.split('.') + parts.shift() + } else { parts = [] } + const serializerDirection = !this.isServer ? 'toServer' : 'toClient' + e.field = [this.protocolState, serializerDirection].concat(parts).join('.') + e.message = `Serialization error for ${e.field} : ${e.message}` + if (!this.compressor) { this.serializer.pipe(this.framer) } else { this.serializer.pipe(this.compressor) } + this.emit('error', e) + }) + + this.deserializer.on('error', (e) => { + let parts + if (e.field) { + parts = e.field.split('.') + parts.shift() + } else { parts = [] } + const deserializerDirection = this.isServer ? 'toServer' : 'toClient' + e.field = [this.protocolState, deserializerDirection].concat(parts).join('.') + e.message = `Deserialization error for ${e.field} : ${e.message}` + if (!this.compressor) { this.splitter.pipe(this.deserializer) } else { this.decompressor.pipe(this.deserializer) } + this.emit('error', e) + }) this._mcBundle = [] const emitPacket = (parsed) => { this.emit('packet', parsed.data, parsed.metadata, parsed.buffer, parsed.fullBuffer) @@ -89,67 +84,55 @@ class Client extends EventEmitter { this.emit('raw.' + parsed.metadata.name, parsed.buffer, parsed.metadata) this.emit('raw', parsed.buffer, parsed.metadata) } - - if (this.customCommunication) { - this.customCommunication.receiverSetup.call(this, (/** @type {{name, params, state?}} */parsed) => { - // debug(`receive in ${this.isServer ? 'server' : 'client'}: ${parsed.metadata.name}`) - this.emit(parsed.name, parsed.params, parsed) - }) - } else { - this.deserializer.on('data', (parsed) => { - parsed.metadata.name = parsed.data.name - parsed.data = parsed.data.params - parsed.metadata.state = state - debug('read packet ' + state + '.' + parsed.metadata.name) - if (debug.enabled) { - const s = JSON.stringify(parsed.data, null, 2) - debug(s && s.length > 10000 ? parsed.data : s) - } - if (parsed.metadata.name === 'bundle_delimiter') { - if (this._mcBundle.length) { // End bundle - this._mcBundle.forEach(emitPacket) - emitPacket(parsed) - this._mcBundle = [] - } else { // Start bundle - this._mcBundle.push(parsed) - } - } else if (this._mcBundle.length) { - this._mcBundle.push(parsed) - } else { + this.deserializer.on('data', (parsed) => { + parsed.metadata.name = parsed.data.name + parsed.data = parsed.data.params + parsed.metadata.state = state + debug('read packet ' + state + '.' + parsed.metadata.name) + if (debug.enabled) { + const s = JSON.stringify(parsed.data, null, 2) + debug(s && s.length > 10000 ? parsed.data : s) + } + if (parsed.metadata.name === 'bundle_delimiter') { + if (this._mcBundle.length) { // End bundle + this._mcBundle.forEach(emitPacket) emitPacket(parsed) + this._mcBundle = [] + } else { // Start bundle + this._mcBundle.push(parsed) } - }) - } + } else if (this._mcBundle.length) { + this._mcBundle.push(parsed) + } else { + emitPacket(parsed) + } + }) } set state (newProperty) { const oldProperty = this.protocolState this.protocolState = newProperty - if (!this.customCommunication) { - if (this.serializer) { - if (!this.compressor) { - this.serializer.unpipe() - this.splitter.unpipe(this.deserializer) - } else { - this.serializer.unpipe(this.compressor) - this.decompressor.unpipe(this.deserializer) - } - - this.serializer.removeAllListeners() - this.deserializer.removeAllListeners() + if (this.serializer) { + if (!this.compressor) { + this.serializer.unpipe() + this.splitter.unpipe(this.deserializer) + } else { + this.serializer.unpipe(this.compressor) + this.decompressor.unpipe(this.deserializer) } + + this.serializer.removeAllListeners() + this.deserializer.removeAllListeners() } this.setSerializer(this.protocolState) - if (!this.customCommunication) { - if (!this.compressor) { - this.serializer.pipe(this.framer) - this.splitter.pipe(this.deserializer) - } else { - this.serializer.pipe(this.compressor) - this.decompressor.pipe(this.deserializer) - } + if (!this.compressor) { + this.serializer.pipe(this.framer) + this.splitter.pipe(this.deserializer) + } else { + this.serializer.pipe(this.compressor) + this.decompressor.pipe(this.deserializer) } this.emit('state', newProperty, oldProperty) @@ -202,7 +185,6 @@ class Client extends EventEmitter { } end (reason) { - if (this.customCommunication) return this._endReason = reason /* ending the serializer will end the whole chain serializer -> framer -> socket -> splitter -> deserializer */ @@ -232,7 +214,6 @@ class Client extends EventEmitter { } setCompressionThreshold (threshold) { - if (this.customCommunication) return if (this.compressor == null) { this.compressor = compression.createCompressor(threshold) this.compressor.on('error', (err) => this.emit('error', err)) @@ -249,15 +230,10 @@ class Client extends EventEmitter { } write (name, params) { - if (!this.customCommunication && !this.serializer.writable) { return } - debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name) + if (!this.serializer.writable) { return } + debug('writing packet ' + this.state + '.' + name) debug(params) - - if (this.customCommunication) { - this.customCommunication.sendData.call(this, { name, params, state: this.state }) - } else { - this.serializer.write({ name, params }) - } + this.serializer.write({ name, params }) } writeBundle (packets) { diff --git a/src/client/compress.js b/src/client/compress.js index 1f99b005e..f1d752d83 100644 --- a/src/client/compress.js +++ b/src/client/compress.js @@ -1,4 +1,5 @@ module.exports = function (client, options) { + if (options.customCommunication) return client.once('compress', onCompressionRequest) client.on('set_compression', onCompressionRequest) diff --git a/src/createClient.js b/src/createClient.js index 998060032..4587671ca 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -1,6 +1,6 @@ 'use strict' -const DefaultClient = require('./client') +const Client = require('./client') const assert = require('assert') const encrypt = require('./client/encrypt') @@ -14,6 +14,7 @@ const tcpDns = require('./client/tcp_dns') const autoVersion = require('./client/autoVersion') const pluginChannels = require('./client/pluginChannels') const versionChecking = require('./client/versionChecking') +const CustomChannelClient = require('./customChannelClient') module.exports = createClient @@ -32,8 +33,13 @@ function createClient (options) { options.protocolVersion = version.version const hideErrors = options.hideErrors || false - const Client = options.customClient ?? DefaultClient - const client = new Client(false, version.minecraftVersion, options.customPackets, hideErrors, options.customCommunication) + /** @type {CustomChannelClient | DefaultClient} */ + let client + if (options.customCommunication) { + client = new CustomChannelClient(false, version.minecraftVersion, options.customCommunication, hideErrors) + } else { + client = new Client(false, version.minecraftVersion, options.customPackets, hideErrors, options.customCommunication) + } tcpDns(client, options) if (options.auth instanceof Function) { diff --git a/src/customChannelClient.js b/src/customChannelClient.js new file mode 100644 index 000000000..3fa6b7af9 --- /dev/null +++ b/src/customChannelClient.js @@ -0,0 +1,60 @@ +'use strict' +const EventEmitter = require('events').EventEmitter +const debug = require('debug')('minecraft-protocol') +const states = require('./states') + +class CustomChannelClient extends EventEmitter { + constructor (isServer, version, customCommunication) { + super() + this.customCommunication = customCommunication + this.version = version + this.isServer = !!isServer + this.state = states.HANDSHAKING + } + + get state () { + return this.protocolState + } + + setSerializer (state) { + this.customCommunication.receiverSetup.call(this, (/** @type {{name, params, state?}} */parsed) => { + debug(`receive in ${this.isServer ? 'server' : 'client'}: ${parsed.name}`) + this.emit(parsed.name, parsed.params, parsed) + this.emit('packet_name', parsed.name, parsed.params, parsed) + }) + } + + set state (newProperty) { + const oldProperty = this.protocolState + this.protocolState = newProperty + + this.setSerializer(this.protocolState) + + this.emit('state', newProperty, oldProperty) + } + + end (reason) { + this._endReason = reason + } + + write (name, params) { + debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name) + debug(params) + + if (this.customCommunication) { + this.customCommunication.sendData.call(this, { name, params, state: this.state }) + } else { + this.serializer.write({ name, params }) + } + } + + writeBundle (packets) { + // no-op + } + + writeRaw (buffer) { + // no-op + } +} + +module.exports = CustomChannelClient From 745b5bed642f54e3fc2d8b1c7a01f18006ee79bb Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 18 Sep 2023 15:32:29 +0300 Subject: [PATCH 7/9] fix doc --- docs/API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/API.md b/docs/API.md index 20b69eb79..3f9f4260b 100644 --- a/docs/API.md +++ b/docs/API.md @@ -145,7 +145,7 @@ Returns a `Client` instance and perform login. * realms : An object which should contain one of the following properties: `realmId` or `pickRealm`. When defined will attempt to join a Realm without needing to specify host/port. **The authenticated account must either own the Realm or have been invited to it** * realmId : The id of the Realm to join. * pickRealm(realms) : A function which will have an array of the user Realms (joined/owned) passed to it. The function should return a Realm. - * customCommunication : Allow to create a custom communication between the client and the server so data can be sent and received in a custom way without having to serialize it. Otherwise you should use `socket` option instead where you can supply custom duplex. + * customCommunication : Allow to create a custom communication between the client and the server so data can be sent and received in a custom way without having to serialize it. Otherwise you should use `stream` option instead where you can supply custom duplex. * `receiverSetup(this: Client, callback: (data: { name, params }) => void)`: Called when the client is ready to receive data from the server. The `callback` should be called with the handler that will receive the data. * `sendData(this: Client, { name, params, state })`: Called when the client wants to send data to the server. The `name` is the name of the packet, `params` is the data to send. From 36ef99f7006709bee1152ad3eb95f0085946c46d Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sat, 23 Sep 2023 23:54:17 +0300 Subject: [PATCH 8/9] move custom client to prismarine web client --- docs/API.md | 26 ++++++++--------- src/client/compress.js | 1 - src/createClient.js | 12 ++------ src/customChannelClient.js | 60 -------------------------------------- src/index.d.ts | 7 ++--- 5 files changed, 17 insertions(+), 89 deletions(-) delete mode 100644 src/customChannelClient.js diff --git a/docs/API.md b/docs/API.md index 3f9f4260b..fd5482035 100644 --- a/docs/API.md +++ b/docs/API.md @@ -12,7 +12,7 @@ automatically logged in and validated against mojang's auth. * kickTimeout : default to `10*1000` (10s), kick client that doesn't answer to keepalive after that time * checkTimeoutInterval : default to `4*1000` (4s), send keepalive packet at that period * online-mode : default to true - * beforePing : allow customisation of the answer to ping the server does. + * beforePing : allow customisation of the answer to ping the server does. It takes a function with argument response and client, response is the default json response, and client is client who sent a ping. It can take as third argument a callback. If the callback is passed, the function should pass its result to the callback, if not it should return. If the result is `false` instead of a response object then the connection is terminated and no ping is returned to the client. @@ -35,7 +35,7 @@ automatically logged in and validated against mojang's auth. * generatePreview (optional) : Function to generate chat previews. Takes the raw message string and should return the message preview as a string. (1.19-1.19.2) * socketType (optional) : either `tcp` or `ipc`. Switches from a tcp connection to a ipc socket connection (or named pipes on windows). With the `ipc` option `host` becomes the path off the ipc connection on the local filesystem. Example: `\\.\pipe\minecraft-ipc` (Windows) `/tmp/minecraft-ipc.sock` (unix based systems). See the ipcConnection example for an example. * Server : You can pass a custom server class to use instead of the default one. - + ## mc.Server(version,[customPackets]) Create a server instance for `version` of minecraft. @@ -113,7 +113,7 @@ Returns a `Client` instance and perform login. is blank, and `profilesFolder` is specified, we auth with the tokens there instead. If neither `password` or `profilesFolder` are specified, we connect in offline mode. * host : default to localhost - * session : An object holding clientToken, accessToken and selectedProfile. Generated after logging in using username + password with mojang auth or after logging in using microsoft auth. `clientToken`, `accessToken` and `selectedProfile: {name: '', id: ''}` can be set inside of `session` when using createClient to login with a client and access Token instead of a password. `session` is also emitted by the `Client` instance with the event 'session' after successful authentication. + * session : An object holding clientToken, accessToken and selectedProfile. Generated after logging in using username + password with mojang auth or after logging in using microsoft auth. `clientToken`, `accessToken` and `selectedProfile: {name: '', id: ''}` can be set inside of `session` when using createClient to login with a client and access Token instead of a password. `session` is also emitted by the `Client` instance with the event 'session' after successful authentication. * clientToken : generated if a password is given or can be set when when using createClient * accessToken : generated if a password or microsoft account is given or can be set when using createBot * selectedProfile : generated if a password or microsoft account is given. Can be set as a object with property `name` and `id` that specifies the selected profile. @@ -130,24 +130,22 @@ Returns a `Client` instance and perform login. * hideErrors : do not display errors, default to false * skipValidation : do not try to validate given session, defaults to false * stream : a stream to use as connection - * connect : a function taking the client as parameter and that should client.setSocket(socket) + * connect : a function taking the client as parameter and that should client.setSocket(socket) and client.emit('connect') when appropriate (see the proxy examples for an example of use) - * agent : a http agent that can be used to set proxy settings for yggdrasil authentication (see proxy-agent on npm) + * agent : a http agent that can be used to set proxy settings for yggdrasil authentication (see proxy-agent on npm) * fakeHost : (optional) hostname to send to the server in the set_protocol packet * profilesFolder : optional - * (mojang account) the path to the folder that contains your `launcher_profiles.json`. defaults to your minecraft folder if it exists, otherwise the local directory. set to `false` to disable managing profiles + * (mojang account) the path to the folder that contains your `launcher_profiles.json`. defaults to your minecraft folder if it exists, otherwise the local directory. set to `false` to disable managing profiles * (microsoft account) the path to store authentication caches, defaults to .minecraft * onMsaCode(data) : (optional) callback called when signing in with a microsoft account with device code auth. `data` is an object documented [here](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code#device-authorization-response) * id : a numeric client id used for referring to multiple clients in a server * validateChannelProtocol (optional) : whether or not to enable protocol validation for custom protocols using plugin channels. Defaults to true * disableChatSigning (optional) : Don't try obtaining chat signing keys from Mojang (1.19+) - * realms : An object which should contain one of the following properties: `realmId` or `pickRealm`. When defined will attempt to join a Realm without needing to specify host/port. **The authenticated account must either own the Realm or have been invited to it** - * realmId : The id of the Realm to join. - * pickRealm(realms) : A function which will have an array of the user Realms (joined/owned) passed to it. The function should return a Realm. - * customCommunication : Allow to create a custom communication between the client and the server so data can be sent and received in a custom way without having to serialize it. Otherwise you should use `stream` option instead where you can supply custom duplex. - * `receiverSetup(this: Client, callback: (data: { name, params }) => void)`: Called when the client is ready to receive data from the server. The `callback` should be called with the handler that will receive the data. - * `sendData(this: Client, { name, params, state })`: Called when the client wants to send data to the server. The `name` is the name of the packet, `params` is the data to send. + * realms : An object which should contain one of the following properties: `realmId` or `pickRealm`. When defined will attempt to join a Realm without needing to specify host/port. **The authenticated account must either own the Realm or have been invited to it** + * realmId : The id of the Realm to join. + * pickRealm(realms) : A function which will have an array of the user Realms (joined/owned) passed to it. The function should return a Realm. + * Client : You can pass a custom client class to use instead of the default one, which would allow you to create completely custom communication. Also note that you can use the `stream` option instead where you can supply custom duplex, but this will still use serialization/deserialization of packets. ## mc.Client(isServer,version,[customPackets]) @@ -239,7 +237,7 @@ The client's version ### `packet` event -Called with every packet parsed. Takes four paramaters, the JSON data we parsed, the packet metadata (name, state), the buffer (raw data) and the full buffer (includes surplus data and may include the data of following packets on versions below 1.8) +Called with every packet parsed. Takes four paramaters, the JSON data we parsed, the packet metadata (name, state), the buffer (raw data) and the full buffer (includes surplus data and may include the data of following packets on versions below 1.8) ### `raw` event @@ -276,7 +274,7 @@ Called when a chat message from another player arrives. The emitted object conta * type -- the message type - on 1.19, which format string to use to render message ; below, the place where the message is displayed (for example chat or action bar) * sender -- the UUID of the player sending the message * senderTeam -- scoreboard team of the player (pre 1.19) -* senderName -- Name of the sender +* senderName -- Name of the sender * targetName -- Name of the target (for outgoing commands like /tell). Only in 1.19.2+ * verified -- true if message is signed, false if not signed, undefined on versions prior to 1.19 diff --git a/src/client/compress.js b/src/client/compress.js index f1d752d83..1f99b005e 100644 --- a/src/client/compress.js +++ b/src/client/compress.js @@ -1,5 +1,4 @@ module.exports = function (client, options) { - if (options.customCommunication) return client.once('compress', onCompressionRequest) client.on('set_compression', onCompressionRequest) diff --git a/src/createClient.js b/src/createClient.js index 4587671ca..2cc51b012 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -1,6 +1,6 @@ 'use strict' -const Client = require('./client') +const DefaultClientImpl = require('./client') const assert = require('assert') const encrypt = require('./client/encrypt') @@ -14,7 +14,6 @@ const tcpDns = require('./client/tcp_dns') const autoVersion = require('./client/autoVersion') const pluginChannels = require('./client/pluginChannels') const versionChecking = require('./client/versionChecking') -const CustomChannelClient = require('./customChannelClient') module.exports = createClient @@ -32,14 +31,9 @@ function createClient (options) { options.majorVersion = version.majorVersion options.protocolVersion = version.version const hideErrors = options.hideErrors || false + const Client = options.Client || DefaultClientImpl - /** @type {CustomChannelClient | DefaultClient} */ - let client - if (options.customCommunication) { - client = new CustomChannelClient(false, version.minecraftVersion, options.customCommunication, hideErrors) - } else { - client = new Client(false, version.minecraftVersion, options.customPackets, hideErrors, options.customCommunication) - } + const client = new Client(false, version.minecraftVersion, options.customCommunication, hideErrors) tcpDns(client, options) if (options.auth instanceof Function) { diff --git a/src/customChannelClient.js b/src/customChannelClient.js deleted file mode 100644 index 3fa6b7af9..000000000 --- a/src/customChannelClient.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict' -const EventEmitter = require('events').EventEmitter -const debug = require('debug')('minecraft-protocol') -const states = require('./states') - -class CustomChannelClient extends EventEmitter { - constructor (isServer, version, customCommunication) { - super() - this.customCommunication = customCommunication - this.version = version - this.isServer = !!isServer - this.state = states.HANDSHAKING - } - - get state () { - return this.protocolState - } - - setSerializer (state) { - this.customCommunication.receiverSetup.call(this, (/** @type {{name, params, state?}} */parsed) => { - debug(`receive in ${this.isServer ? 'server' : 'client'}: ${parsed.name}`) - this.emit(parsed.name, parsed.params, parsed) - this.emit('packet_name', parsed.name, parsed.params, parsed) - }) - } - - set state (newProperty) { - const oldProperty = this.protocolState - this.protocolState = newProperty - - this.setSerializer(this.protocolState) - - this.emit('state', newProperty, oldProperty) - } - - end (reason) { - this._endReason = reason - } - - write (name, params) { - debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name) - debug(params) - - if (this.customCommunication) { - this.customCommunication.sendData.call(this, { name, params, state: this.state }) - } else { - this.serializer.write({ name, params }) - } - } - - writeBundle (packets) { - // no-op - } - - writeRaw (buffer) { - // no-op - } -} - -module.exports = CustomChannelClient diff --git a/src/index.d.ts b/src/index.d.ts index 9f9797d44..052998d97 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -136,11 +136,8 @@ declare module 'minecraft-protocol' { realms?: RealmsOptions // 1.19+ disableChatSigning?: boolean - customCommunication?: { - sendData(this: Client, { name, params, state }): void, - receiverSetup(this: Client, callback: (data: { name, params, state? }) => void): void - } - customClient?: Client + /** Pass custom client implementation if needed. */ + Client?: Client } export class Server extends EventEmitter { From 28d27ff08916a468ecc038977cef9af97a42f1b6 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sat, 23 Sep 2023 23:55:38 +0300 Subject: [PATCH 9/9] restore customPackets --- src/createClient.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/createClient.js b/src/createClient.js index 2cc51b012..97a6336d0 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -33,7 +33,7 @@ function createClient (options) { const hideErrors = options.hideErrors || false const Client = options.Client || DefaultClientImpl - const client = new Client(false, version.minecraftVersion, options.customCommunication, hideErrors) + const client = new Client(false, version.minecraftVersion, options.customPackets, hideErrors) tcpDns(client, options) if (options.auth instanceof Function) {