From 2125ad0543d82fef720b6f72d42f637c8c52d6e0 Mon Sep 17 00:00:00 2001 From: Pronin Egor <42776347+MrZillaGold@users.noreply.github.com> Date: Fri, 21 Jan 2022 02:43:16 +1000 Subject: [PATCH 001/171] docs(readme): add SteveProxy to projects list (#948) --- docs/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/README.md b/docs/README.md index ca25145f6..36389b533 100644 --- a/docs/README.md +++ b/docs/README.md @@ -51,9 +51,10 @@ node-minecraft-protocol is pluggable. create bots. * [flying-squid](https://github.com/PrismarineJS/flying-squid) - Create minecraft servers with a high level API, also a minecraft server by itself. - * [pakkit](https://github.com/Heath123/pakkit) - A GUI tool to monitor Minecraft packets in real time, allowing you to view their data and interactively edit and resend them - * [minecraft-packet-debugger](https://github.com/wvffle/minecraft-packet-debugger) - A tool to capture Minecraft packets in a buffer then view them in a browser - * [aresrpg](https://github.com/aresrpg/aresrpg) - An open-source mmorpg minecraft server + * [pakkit](https://github.com/Heath123/pakkit) - A GUI tool to monitor Minecraft packets in real time, allowing you to view their data and interactively edit and resend them. + * [minecraft-packet-debugger](https://github.com/wvffle/minecraft-packet-debugger) - A tool to capture Minecraft packets in a buffer then view them in a browser. + * [aresrpg](https://github.com/aresrpg/aresrpg) - An open-source mmorpg minecraft server. + * [SteveProxy](https://github.com/SteveProxy/proxy) - Proxy for Minecraft with the ability to change the gameplay using plugins. * and [several thousands others](https://github.com/PrismarineJS/node-minecraft-protocol/network/dependents?package_id=UGFja2FnZS0xODEzMDk0OQ%3D%3D) ## Installation From 3a1561b7c8ff5e2a877eadfe112b191b9bdf50fe Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sat, 22 Jan 2022 17:10:01 +0000 Subject: [PATCH 002/171] explicit 1.18.1 support --- .github/workflows/ci.yml | 2 +- docs/README.md | 2 +- package.json | 2 +- src/version.js | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d44c8714..dd17ea617 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - mcVersion: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17', '1.17.1', '1.18'] + mcVersion: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17', '1.17.1', '1.18.1'] steps: - uses: actions/checkout@v2 diff --git a/docs/README.md b/docs/README.md index 36389b533..74ef1acff 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18 and 1.18.1) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. diff --git a/package.json b/package.json index 81bad3984..1c540a177 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^2.98.0", + "minecraft-data": "^2.108.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", diff --git a/src/version.js b/src/version.js index 0afd42e12..4097059c1 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.18', - supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18'] + defaultVersion: '1.18.1', + supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.1'] } From c22682b9f1c2b5577481c53aa30b54828cc0254d Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sat, 22 Jan 2022 18:30:35 +0100 Subject: [PATCH 003/171] bump mcdata to include a fix for 1.18.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1c540a177..251e24953 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^2.108.0", + "minecraft-data": "^2.109.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", From ffeb7515e34fd9c53b8f76a885244d54e0cace3a Mon Sep 17 00:00:00 2001 From: usb <56985400+U5B@users.noreply.github.com> Date: Fri, 28 Jan 2022 07:02:48 -0800 Subject: [PATCH 004/171] require node 14 (#955) https://github.com/PrismarineJS/node-minecraft-protocol/issues/954 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 251e24953..4c9397fa6 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "author": "Andrew Kelley", "license": "BSD-3-Clause", "engines": { - "node": ">=6" + "node": ">=14" }, "browser": "src/browser.js", "devDependencies": { From 4691abd0365721d0c7981c4140c9009b434bc18f Mon Sep 17 00:00:00 2001 From: Richard Dorian <75615715+RichardDorian@users.noreply.github.com> Date: Tue, 1 Feb 2022 23:47:44 +0100 Subject: [PATCH 005/171] Wrong type definition for beforePing (#957) * `favicon` now include in types * fix `beforePing` type --- 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 37eff612f..6bf785ff3 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -136,7 +136,7 @@ declare module 'minecraft-protocol' { kickTimeout?: number checkTimeoutInterval?: number 'online-mode'?: boolean - beforePing?: (response: any, client: Client, callback?: (result: any) => any) => any + beforePing?: (response: any, client: Client, callback?: (error: any, result: any) => any) => any beforeLogin?: (client: Client) => void motd?: string maxPlayers?: number From 9e94d9d9019250b2f4bfa098f937464cccf7e4b8 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Tue, 1 Mar 2022 22:44:50 +0100 Subject: [PATCH 006/171] 1.18.2 (#967) * Update README.md * Update version.js * Update ci.yml --- .github/workflows/ci.yml | 2 +- docs/README.md | 2 +- src/version.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd17ea617..180a6f2d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - mcVersion: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17', '1.17.1', '1.18.1'] + mcVersion: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17', '1.17.1', '1.18.2'] steps: - uses: actions/checkout@v2 diff --git a/docs/README.md b/docs/README.md index 74ef1acff..c73c8fa32 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18 and 1.18.1) + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. diff --git a/src/version.js b/src/version.js index 4097059c1..638545104 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.18.1', - supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.1'] + defaultVersion: '1.18.2', + supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2'] } From ef7d0332d9c10ccccb3c584dd9d84c331830844e Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Tue, 1 Mar 2022 22:46:15 +0100 Subject: [PATCH 007/171] Release 1.31.0 (#968) * Update package.json * Update HISTORY.md --- docs/HISTORY.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 53f962e29..f9809056f 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,9 @@ # History +## 1.31.0 + +* 1.18.2 + ## 1.30.0 * add reasons for client.end() & fix issues (@U5B) diff --git a/package.json b/package.json index 4c9397fa6..72b60a6c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.30.0", + "version": "1.31.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From f7ef27dd70b788b7e8c002442ecb100e11145e4d Mon Sep 17 00:00:00 2001 From: Richard Dorian <75615715+RichardDorian@users.noreply.github.com> Date: Tue, 8 Mar 2022 15:40:57 +0100 Subject: [PATCH 008/171] add protocolValidation field to server and client options (#964) * add protocolValidation field to server and client options * add doc * change option name --- docs/API.md | 4 +++- src/client/pluginChannels.js | 2 +- src/index.d.ts | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/API.md b/docs/API.md index 787a129d6..8945e83c4 100644 --- a/docs/API.md +++ b/docs/API.md @@ -26,7 +26,8 @@ automatically logged in and validated against mojang's auth. * errorHandler : A way to override the default error handler for client errors. A function that takes a Client and an error. The default kicks the client. * hideErrors : do not display errors, default to false - * agent : a http agent that can be used to set proxy settings for yggdrasil authentication confirmation (see proxy-agent on npm) + * agent : a http agent that can be used to set proxy settings for yggdrasil authentication confirmation (see proxy-agent on npm) + * validateChannelProtocol (optional) : whether or not to enable protocol validation for custom protocols using plugin channels for the connected clients. Defaults to true ## mc.Server(version,[customPackets]) @@ -116,6 +117,7 @@ Returns a `Client` instance and perform login. * 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 ## mc.Client(isServer,version,[customPackets]) diff --git a/src/client/pluginChannels.js b/src/client/pluginChannels.js index eb8614176..b2e420bd7 100644 --- a/src/client/pluginChannels.js +++ b/src/client/pluginChannels.js @@ -5,7 +5,7 @@ const debug = require('debug')('minecraft-protocol') module.exports = function (client, options) { const mcdata = require('minecraft-data')(options.version || require('../version').defaultVersion) const channels = [] - const proto = new ProtoDef() + const proto = new ProtoDef(options.validateChannelProtocol ?? true) proto.addTypes(mcdata.protocol.types) proto.addTypes(minecraft) proto.addType('registerarr', [readDumbArr, writeDumbArr, sizeOfDumbArr]) diff --git a/src/index.d.ts b/src/index.d.ts index 6bf785ff3..500b8a9e0 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -104,6 +104,7 @@ declare module 'minecraft-protocol' { onMsaCode?: (data: MicrosoftDeviceAuthorizationResponse) => void id?: number session?: SessionOption + validateChannelProtocol?: boolean } export class Server extends EventEmitter { @@ -147,6 +148,7 @@ declare module 'minecraft-protocol' { errorHandler?: (client: Client, error: Error) => void hideErrors?: boolean agent?: Agent + validateChannelProtocol: boolean } export interface SerializerOptions { From e93d9de652eda3fd33c377ace1eb3c539f0ab7d8 Mon Sep 17 00:00:00 2001 From: Richard Dorian <75615715+RichardDorian@users.noreply.github.com> Date: Tue, 8 Mar 2022 15:45:52 +0100 Subject: [PATCH 009/171] fix plugin channel registration (#965) * fix plugin channel registration * add support for 1.13+ plugin channels * add missing null separator * use protodef instead --- src/client/pluginChannels.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client/pluginChannels.js b/src/client/pluginChannels.js index b2e420bd7..7988de356 100644 --- a/src/client/pluginChannels.js +++ b/src/client/pluginChannels.js @@ -17,13 +17,15 @@ module.exports = function (client, options) { client.registerChannel('REGISTER', ['registerarr', []]) client.registerChannel('UNREGISTER', ['registerarr', []]) - if (options.protocolVersion >= 385) { // 1.13-pre3 (385) added Added Login Plugin Message (https://wiki.vg/Protocol_History#1.13-pre3) + const above385 = options.protocolVersion >= 385 + if (above385) { // 1.13-pre3 (385) added Added Login Plugin Message (https://wiki.vg/Protocol_History#1.13-pre3) client.on('login_plugin_request', onLoginPluginRequest) } + const channelNames = above385 ? ['minecraft:register', 'minecraft:unregister'] : ['REGISTER', 'UNREGISTER'] function registerChannel (name, parser, custom) { if (custom) { - client.writeChannel('REGISTER', name) + client.writeChannel(channelNames[0], [name]) } if (parser) proto.addType(name, parser) channels.push(name) @@ -32,7 +34,7 @@ module.exports = function (client, options) { function unregisterChannel (channel, custom) { if (custom) { - client.writeChannel('UNREGISTER', channel) + client.writeChannel(channelNames[1], [channel]) } const index = channels.find(function (name) { return channel === name @@ -87,7 +89,7 @@ module.exports = function (client, options) { function writeDumbArr (value, buf, offset) { // TODO: Remove trailing \0 value.forEach(function (v) { - offset += this.write(v, buf, offset, 'cstring', {}) + offset += proto.write(v, buf, offset, 'cstring') }) return offset } From bcfeb712f913c5af0a6a7a809a412effdcc6c7b2 Mon Sep 17 00:00:00 2001 From: Robert Schuh Date: Tue, 8 Mar 2022 15:50:50 +0100 Subject: [PATCH 010/171] allows false value for profilesFolder (#961) --- 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 500b8a9e0..f71bb6102 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -100,7 +100,7 @@ declare module 'minecraft-protocol' { connect?: (client: Client) => void agent?: Agent fakeHost?: string - profilesFolder?: string + profilesFolder?: string | false onMsaCode?: (data: MicrosoftDeviceAuthorizationResponse) => void id?: number session?: SessionOption From f1f9f5c310626282b23c089d2d202090fa91d29f Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Fri, 11 Mar 2022 16:26:56 +0100 Subject: [PATCH 011/171] Release 1.32.0 (#971) * Update HISTORY.md * Update package.json --- docs/HISTORY.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index f9809056f..05c9a9f7a 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,11 @@ # History +## 1.32.0 + +* add protocolValidation field to server and client options (@RichardDorian) +* fix plugin channel registration (@RichardDorian) +* allows false value for profilesFolder (@Robbilie) + ## 1.31.0 * 1.18.2 diff --git a/package.json b/package.json index 72b60a6c6..4b9f17cb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.31.0", + "version": "1.32.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 11ad7130aa8ce8ad47ea9c859ced0d26101eba8f Mon Sep 17 00:00:00 2001 From: IceTank <61137113+IceTank@users.noreply.github.com> Date: Thu, 17 Mar 2022 09:22:42 +0100 Subject: [PATCH 012/171] Make `validateChannelProtocol` optional (#972) --- 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 f71bb6102..8a9adfe0e 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -148,7 +148,7 @@ declare module 'minecraft-protocol' { errorHandler?: (client: Client, error: Error) => void hideErrors?: boolean agent?: Agent - validateChannelProtocol: boolean + validateChannelProtocol?: boolean } export interface SerializerOptions { From 9cff34efc0ac35fb3693a766133ca988ce40b93c Mon Sep 17 00:00:00 2001 From: Rob9315 Date: Sun, 20 Mar 2022 15:59:09 +0100 Subject: [PATCH 013/171] Release 1.32.1 (#973) --- docs/HISTORY.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 05c9a9f7a..f7ed27893 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,9 @@ # History +## 1.32.1 + +* fix protocolValidation not being optional in .d.ts typings (@IceTank) + ## 1.32.0 * add protocolValidation field to server and client options (@RichardDorian) diff --git a/package.json b/package.json index 4b9f17cb7..78662095c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.32.0", + "version": "1.32.1", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 68e6400e308a776b164ce5258c55424d7ae8bdd2 Mon Sep 17 00:00:00 2001 From: jojomatik Date: Sun, 10 Apr 2022 14:45:04 +0200 Subject: [PATCH 014/171] fix: cross version ping (#976) * docs: explain version parameter Explain version parameter more explicitly. Remove reference to outdated versions. Describe dynamic cross version support with parameter value `false`. * fix(types): allow `false` for version parameter Allow value `false` for version parameter. This makes dynamic cross version support usable in typescript projects. * fix: enable cross version support for ping Enable dynamic cross version support for ping by responding with the client version and protocol version if dynamic cross version support is enabled. --- docs/API.md | 2 +- src/index.d.ts | 2 +- src/server/ping.js | 18 +++++++++++++----- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/API.md b/docs/API.md index 8945e83c4..1feccae56 100644 --- a/docs/API.md +++ b/docs/API.md @@ -20,7 +20,7 @@ automatically logged in and validated against mojang's auth. * motd : default to "A Minecraft server" * maxPlayers : default to 20 * keepAlive : send keep alive packets : default to true - * version : 1.8 or 1.9 : default to 1.8 + * version : the version of the server, defaults to the latest version. Set version to `false` to enable dynamic cross version support. * favicon (optional) : the favicon to set, base64 encoded * customPackets (optional) : an object index by version/state/direction/name, see client_custom_packet for an example * errorHandler : A way to override the default error handler for client errors. A function that takes a Client and an error. diff --git a/src/index.d.ts b/src/index.d.ts index 8a9adfe0e..77f60c1b2 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -142,7 +142,7 @@ declare module 'minecraft-protocol' { motd?: string maxPlayers?: number keepAlive?: boolean - version?: string + version?: string | false favicon?: string customPackets?: any errorHandler?: (client: Client, error: Error) => void diff --git a/src/server/ping.js b/src/server/ping.js index f72fca5f5..38205d49c 100644 --- a/src/server/ping.js +++ b/src/server/ping.js @@ -1,15 +1,23 @@ const endianToggle = require('endian-toggle') -module.exports = function (client, server, { beforePing = null }) { +module.exports = function (client, server, { beforePing = null, version }) { client.once('ping_start', onPing) client.once('legacy_server_list_ping', onLegacyPing) function onPing () { + // Use client version if dynamic cross version support is enabled. + const responseVersion = (version === false) + ? { + name: client.version, + protocol: client.protocolVersion + } + : { + name: server.mcversion.minecraftVersion, + protocol: server.mcversion.version + } + const response = { - version: { - name: server.mcversion.minecraftVersion, - protocol: server.mcversion.version - }, + version: responseVersion, players: { max: server.maxPlayers, online: server.playerCount, From 5e3196b2dee53b09c5b863dd2d92daa721e34dd0 Mon Sep 17 00:00:00 2001 From: IceTank <61137113+IceTank@users.noreply.github.com> Date: Sun, 10 Apr 2022 15:16:18 +0200 Subject: [PATCH 015/171] Add basic deserializer and serializer typings (#975) --- src/index.d.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/index.d.ts b/src/index.d.ts index 77f60c1b2..6f21f6065 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -4,6 +4,7 @@ import { EventEmitter } from 'events'; import { Socket } from 'net' import * as Stream from 'stream' import { Agent } from 'http' +import { Transform } from "readable-stream"; type PromiseLike = Promise | void @@ -17,6 +18,8 @@ declare module 'minecraft-protocol' { username: string session?: SessionOption profile?: any + deserializer: FullPacketParser + serializer: Serializer latency: number customPackets: any protocolVersion: number @@ -51,6 +54,24 @@ declare module 'minecraft-protocol' { once(event: `raw.${string}`, handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this } + class FullPacketParser extends Transform { + proto: any + mainType: any + noErrorLogging: boolean + constructor (proto: any, mainType: any, noErrorLogging?: boolean) + + parsePacketBuffer(buffer: Buffer): any + } + + class Serializer extends Transform { + proto: any + mainType: any + queue: Buffer + constructor(proto: any, mainType: any) + + createPacketBuffer(packet: any): any + } + export interface SessionOption { accessToken: string, /** My be needed for mojang auth. Is send by mojang on username + password auth */ From 3bb93b603570cc6e5180cadd471214e15875d370 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sun, 10 Apr 2022 15:18:09 +0200 Subject: [PATCH 016/171] Release 1.32.2 (#979) * Update package.json * Update HISTORY.md --- docs/HISTORY.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index f7ed27893..1e725c8dd 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,9 @@ # History +## 1.32.2 + +* fix: cross version ping + ## 1.32.1 * fix protocolValidation not being optional in .d.ts typings (@IceTank) diff --git a/package.json b/package.json index 78662095c..b9c1554cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.32.1", + "version": "1.32.2", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 6efbbcfe756fc06d38d902a309da96b9bea48515 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Apr 2022 13:29:00 +0200 Subject: [PATCH 017/171] Bump minecraft-data from 2.221.0 to 3.0.0 (#980) Bumps [minecraft-data](https://github.com/PrismarineJS/node-minecraft-data) from 2.221.0 to 3.0.0. - [Release notes](https://github.com/PrismarineJS/node-minecraft-data/releases) - [Changelog](https://github.com/PrismarineJS/node-minecraft-data/blob/master/doc/history.md) - [Commits](https://github.com/PrismarineJS/node-minecraft-data/compare/2.221.0...3.0.0) --- updated-dependencies: - dependency-name: minecraft-data dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b9c1554cf..d4709f002 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^2.109.0", + "minecraft-data": "^3.0.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", From 2f27ad40282e1fe7754eb586ad69bd006c69ce97 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Mon, 11 Apr 2022 13:31:22 +0200 Subject: [PATCH 018/171] Release 1.33.0 (#981) * Update package.json * Update HISTORY.md --- docs/HISTORY.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 1e725c8dd..195b8559a 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,9 @@ # History +## 1.33.0 + +* Bump mcdata + ## 1.32.2 * fix: cross version ping diff --git a/package.json b/package.json index d4709f002..226f3f0c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.32.2", + "version": "1.33.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From aac5494a12e2b2ea3fbe4147c2267935e107c657 Mon Sep 17 00:00:00 2001 From: IceTank <61137113+IceTank@users.noreply.github.com> Date: Tue, 12 Apr 2022 18:35:48 +0200 Subject: [PATCH 019/171] Fix missing readable-stream types (#982) * fix missing readable-stream types * move to normal dependencies --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 226f3f0c1..0f29b6b64 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "standard": "^16.0.1" }, "dependencies": { + "@types/readable-stream": "^2.3.13", "aes-js": "^3.1.2", "buffer-equal": "^1.0.0", "debug": "^4.3.2", From ffe912b2fe92f53e595615ad346a0237a9b1edf3 Mon Sep 17 00:00:00 2001 From: Matthias Neid Date: Wed, 13 Apr 2022 13:50:42 +0200 Subject: [PATCH 020/171] Export defaultVersion (#984) * export defaultVersion * added defaultVersion to api documentation --- docs/API.md | 4 ++++ src/index.d.ts | 3 ++- src/index.js | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/API.md b/docs/API.md index 1feccae56..7b983017e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -325,6 +325,10 @@ The minecraft protocol states. The supported minecraft versions. +## mc.defaultVersion + +The current default minecraft version. + ## mc.createSerializer({ state = states.HANDSHAKING, isServer = false , version}) Returns a minecraft protocol [serializer](https://github.com/roblabla/ProtoDef#serializerprotomaintype) for these parameters. diff --git a/src/index.d.ts b/src/index.d.ts index 6f21f6065..ac31075ee 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -245,7 +245,8 @@ declare module 'minecraft-protocol' { } export const states: typeof States - export const supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1'] + export const supportedVersions: string[] + export const defaultVersion: string export function createServer(options: ServerOptions): Server export function createClient(options: ClientOptions): Client diff --git a/src/index.js b/src/index.js index ab862afc1..345341033 100644 --- a/src/index.js +++ b/src/index.js @@ -15,5 +15,6 @@ module.exports = { createSerializer: serializer.createSerializer, createDeserializer: serializer.createDeserializer, ping: require('./ping'), - supportedVersions: require('./version').supportedVersions + supportedVersions: require('./version').supportedVersions, + defaultVersion: require('./version').defaultVersion } From d641634fe951716fa24e2afdcd891de8a704224c Mon Sep 17 00:00:00 2001 From: jojomatik Date: Sun, 17 Apr 2022 22:36:33 +0200 Subject: [PATCH 021/171] Release 1.34.0 (#988) * Update package.json * Update HISTORY.md --- .github/workflows/npm-publish.yml | 27 +++++++++++++++++---------- docs/HISTORY.md | 5 +++++ package.json | 2 +- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 7e90156fc..387db4010 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -13,13 +13,20 @@ jobs: - name: Set up Node.js uses: actions/setup-node@master with: - node-version: 10.0.0 - - name: Publish if version has been updated - uses: pascalgn/npm-publish-action@4f4bf159e299f65d21cd1cbd96fc5d53228036df - with: # All of theses inputs are optional - tag_name: "%s" - tag_message: "%s" - commit_pattern: "^Release (\\S+)" - env: # More info about the environment variables in the README - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Leave this as is, it's automatically generated - NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} # You need to set this in your repo settings + node-version: 14.0.0 + - id: publish + uses: JS-DevTools/npm-publish@v1 + with: + token: ${{ secrets.NPM_AUTH_TOKEN }} + - name: Create Release + if: steps.publish.outputs.type != 'none' + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.publish.outputs.version }} + release_name: Release ${{ steps.publish.outputs.version }} + body: ${{ steps.publish.outputs.version }} + draft: false + prerelease: false \ No newline at end of file diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 195b8559a..07613767e 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,10 @@ # History +## 1.34.0 + +* Export defaultVersion (@matthi4s) +* Fix missing readable-stream types (@IceTank) + ## 1.33.0 * Bump mcdata diff --git a/package.json b/package.json index 0f29b6b64..6ffc92acd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.33.0", + "version": "1.34.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 30053a402f1ddb7904cd961dee3152e543274676 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 May 2022 13:02:58 -0400 Subject: [PATCH 022/171] Bump mocha from 9.2.2 to 10.0.0 (#995) Bumps [mocha](https://github.com/mochajs/mocha) from 9.2.2 to 10.0.0. - [Release notes](https://github.com/mochajs/mocha/releases) - [Changelog](https://github.com/mochajs/mocha/blob/master/CHANGELOG.md) - [Commits](https://github.com/mochajs/mocha/compare/v9.2.2...v10.0.0) --- updated-dependencies: - dependency-name: mocha dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6ffc92acd..61353452a 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "minecraft-packets": "^1.1.5", "minecraft-protocol": "file:.", "minecraft-wrap": "^1.2.3", - "mocha": "^9.0.0", + "mocha": "^10.0.0", "power-assert": "^1.0.0", "standard": "^16.0.1" }, From 74e2ba84acae0b16cd431b22aaff68758286b91e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 May 2022 13:15:26 -0400 Subject: [PATCH 023/171] Bump standard from 16.0.4 to 17.0.0 (#990) Bumps [standard](https://github.com/standard/standard) from 16.0.4 to 17.0.0. - [Release notes](https://github.com/standard/standard/releases) - [Changelog](https://github.com/standard/standard/blob/master/CHANGELOG.md) - [Commits](https://github.com/standard/standard/compare/v16.0.4...v17.0.0) --- updated-dependencies: - dependency-name: standard dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 61353452a..e2de6232e 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "minecraft-wrap": "^1.2.3", "mocha": "^10.0.0", "power-assert": "^1.0.0", - "standard": "^16.0.1" + "standard": "^17.0.0" }, "dependencies": { "@types/readable-stream": "^2.3.13", From a9cc6cee85f896d98f891c3cda93516e6cea5cba Mon Sep 17 00:00:00 2001 From: IceTank <61137113+IceTank@users.noreply.github.com> Date: Sat, 14 May 2022 00:49:08 +0200 Subject: [PATCH 024/171] Add motdMsg to createServer (#996) * Add motd as MessageBuilder object * Fix chatMessageMotd type * Fix test (?) * Update API.md * Remove MessageBuilder dependency and type * Minor code refactor * Update docs/API.md Co-authored-by: u9g * chatMessageMotd => motdMsg Co-authored-by: u9g --- docs/API.md | 1 + src/createServer.js | 4 +++- src/index.d.ts | 2 ++ src/server/ping.js | 2 +- test/serverTest.js | 46 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/docs/API.md b/docs/API.md index 7b983017e..d324eec25 100644 --- a/docs/API.md +++ b/docs/API.md @@ -18,6 +18,7 @@ automatically logged in and validated against mojang's auth. * beforeLogin : allow customisation of client before the `success` packet is sent. It takes a function with argument client and should be synchronous for the server to wait for completion before continuing execution. * motd : default to "A Minecraft server" + * motdMsg : A json object of the chat message to use instead of `motd`. Can be build using [prismarine-chat](https://github.com/PrismarineJS/prismarine-chat) and calling .toJSON(). Not used with legacy pings. * maxPlayers : default to 20 * keepAlive : send keep alive packets : default to true * version : the version of the server, defaults to the latest version. Set version to `false` to enable dynamic cross version support. diff --git a/src/createServer.js b/src/createServer.js index 77620ad64..e1f5be543 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -22,7 +22,8 @@ function createServer (options = {}) { maxPlayers: maxPlayersNew = 20, version, favicon, - customPackets + customPackets, + motdMsg // This is when you want to send formated motd's from ChatMessage instances } = options const maxPlayers = options['max-players'] !== undefined ? maxPlayersOld : maxPlayersNew @@ -37,6 +38,7 @@ function createServer (options = {}) { const server = new Server(mcversion.minecraftVersion, customPackets, hideErrors) server.mcversion = mcversion server.motd = motd + server.motdMsg = motdMsg server.maxPlayers = maxPlayers server.playerCount = 0 server.onlineModeExceptions = Object.create(null) diff --git a/src/index.d.ts b/src/index.d.ts index ac31075ee..fd9efdca6 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -136,6 +136,7 @@ declare module 'minecraft-protocol' { playerCount: number maxPlayers: number motd: string + motdMsg?: Object favicon: string close(): void on(event: 'connection', handler: (client: ServerClient) => PromiseLike): this @@ -161,6 +162,7 @@ declare module 'minecraft-protocol' { beforePing?: (response: any, client: Client, callback?: (error: any, result: any) => any) => any beforeLogin?: (client: Client) => void motd?: string + motdMsg?: Object maxPlayers?: number keepAlive?: boolean version?: string | false diff --git a/src/server/ping.js b/src/server/ping.js index 38205d49c..8ca7839c4 100644 --- a/src/server/ping.js +++ b/src/server/ping.js @@ -23,7 +23,7 @@ module.exports = function (client, server, { beforePing = null, version }) { online: server.playerCount, sample: [] }, - description: { text: server.motd }, + description: server.motdMsg ?? { text: server.motd }, favicon: server.favicon } diff --git a/test/serverTest.js b/test/serverTest.js index 73a3a0c06..e66a776d0 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -157,6 +157,52 @@ for (const supportedVersion of mc.supportedVersions) { } }) it('responds to ping requests', function (done) { + const chatMotd = { // Generated with prismarine-chat MessageBuilder on version 1.16 may change in the future + extra: [{ color: 'red', text: 'Red text' }], + bold: true, + text: 'Example chat mesasge' + } + + const server = mc.createServer({ + 'online-mode': false, + motd: 'test1234', + motdMsg: chatMotd, + 'max-players': 120, + version: version.minecraftVersion, + port: PORT + }) + server.on('listening', function () { + mc.ping({ + host: '127.0.0.1', + version: version.minecraftVersion, + port: PORT + }, function (err, results) { + if (err) return done(err) + assert.ok(results.latency >= 0) + assert.ok(results.latency <= 1000) + delete results.latency + assert.deepEqual(results, { + version: { + name: version.minecraftVersion, + protocol: version.version + }, + players: { + max: 120, + online: 0, + sample: [] + }, + description: { + extra: [ { color: 'red', text: 'Red text' } ], + bold: true, + text: 'Example chat mesasge' + } + }) + server.close() + }) + }) + server.on('close', done) + }) + it('responds with chatMessage motd\'s', function (done) { const server = mc.createServer({ 'online-mode': false, motd: 'test1234', From 7a1d85760276a70ac6be5af4213c41c5e52efba7 Mon Sep 17 00:00:00 2001 From: Matthias Neid Date: Sun, 15 May 2022 00:39:20 +0200 Subject: [PATCH 025/171] Fallback version for dynamic versions (#983) * added fallbackVersion to dynamic version support * mark fallbackVersion as optional in docs * added fallbackVersion to ping responses * don't enable dynamic cross version support if version is undefined --- docs/API.md | 1 + src/index.d.ts | 1 + src/server/handshake.js | 14 +++++++++++--- src/server/ping.js | 28 ++++++++++++++++++++-------- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/docs/API.md b/docs/API.md index d324eec25..3516ea2f0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -22,6 +22,7 @@ automatically logged in and validated against mojang's auth. * maxPlayers : default to 20 * keepAlive : send keep alive packets : default to true * version : the version of the server, defaults to the latest version. Set version to `false` to enable dynamic cross version support. + * fallbackVersion (optional) : the version that should be used as a fallback, if the client version isn't supported, only works with dynamic cross version support. * favicon (optional) : the favicon to set, base64 encoded * customPackets (optional) : an object index by version/state/direction/name, see client_custom_packet for an example * errorHandler : A way to override the default error handler for client errors. A function that takes a Client and an error. diff --git a/src/index.d.ts b/src/index.d.ts index fd9efdca6..ac76a1d95 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -166,6 +166,7 @@ declare module 'minecraft-protocol' { maxPlayers?: number keepAlive?: boolean version?: string | false + fallbackVersion?: string favicon?: string customPackets?: any errorHandler?: (client: Client, error: Error) => void diff --git a/src/server/handshake.js b/src/server/handshake.js index 12d7fa3be..e3d5a75ec 100644 --- a/src/server/handshake.js +++ b/src/server/handshake.js @@ -1,6 +1,6 @@ const states = require('../states') -module.exports = function (client, server, { version }) { +module.exports = function (client, server, { version, fallbackVersion }) { client.once('set_protocol', onHandshake) function onHandshake (packet) { @@ -8,11 +8,19 @@ module.exports = function (client, server, { version }) { client.serverPort = packet.serverPort client.protocolVersion = packet.protocolVersion - if (version === false || version === undefined) { + if (version === false) { if (require('minecraft-data')(client.protocolVersion)) { client.version = client.protocolVersion } else { - client.end('Protocol version ' + client.protocolVersion + ' is not supported') + let fallback + if (fallbackVersion !== undefined) { + fallback = require('minecraft-data')(fallbackVersion) + } + if (fallback) { + client.version = fallback.version.version + } else { + client.end('Protocol version ' + client.protocolVersion + ' is not supported') + } } } else if (client.protocolVersion !== server.mcversion.version && packet.nextState !== 1) { client.end('Wrong protocol version, expected: ' + server.mcversion.version + ' and you are using: ' + client.protocolVersion) diff --git a/src/server/ping.js b/src/server/ping.js index 8ca7839c4..e03377a29 100644 --- a/src/server/ping.js +++ b/src/server/ping.js @@ -1,20 +1,32 @@ const endianToggle = require('endian-toggle') -module.exports = function (client, server, { beforePing = null, version }) { +module.exports = function (client, server, { beforePing = null, version, fallbackVersion }) { client.once('ping_start', onPing) client.once('legacy_server_list_ping', onLegacyPing) function onPing () { - // Use client version if dynamic cross version support is enabled. - const responseVersion = (version === false) - ? { + let responseVersion = { + name: server.mcversion.minecraftVersion, + protocol: server.mcversion.version + } + + if (version === false) { + let minecraftData = require('minecraft-data')(client.protocolVersion) + if (!minecraftData && fallbackVersion !== undefined) { + minecraftData = require('minecraft-data')(fallbackVersion) + } + if (minecraftData) { + responseVersion = { + name: minecraftData.version.minecraftVersion, + protocol: minecraftData.version.version + } + } else { + responseVersion = { name: client.version, protocol: client.protocolVersion } - : { - name: server.mcversion.minecraftVersion, - protocol: server.mcversion.version - } + } + } const response = { version: responseVersion, From 83f1e854800a64db46d100933054910edfb52a4f Mon Sep 17 00:00:00 2001 From: Matthias Neid Date: Sun, 15 May 2022 00:51:18 +0200 Subject: [PATCH 026/171] allow false as beforePing callback result to ignore pings and terminate the connection (#986) --- docs/API.md | 1 + src/server/ping.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/API.md b/docs/API.md index 3516ea2f0..38a87eb55 100644 --- a/docs/API.md +++ b/docs/API.md @@ -15,6 +15,7 @@ automatically logged in and validated against mojang's auth. * 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. * beforeLogin : allow customisation of client before the `success` packet is sent. It takes a function with argument client and should be synchronous for the server to wait for completion before continuing execution. * motd : default to "A Minecraft server" diff --git a/src/server/ping.js b/src/server/ping.js index e03377a29..07a73478b 100644 --- a/src/server/ping.js +++ b/src/server/ping.js @@ -41,7 +41,11 @@ module.exports = function (client, server, { beforePing = null, version, fallbac function answerToPing (err, response) { if (err) return - client.write('server_info', { response: JSON.stringify(response) }) + if (response === false) { + client.socket.destroy() + } else { + client.write('server_info', { response: JSON.stringify(response) }) + } } if (beforePing) { From 9cb16b8b129e16c09efc48701621d44024002e2e Mon Sep 17 00:00:00 2001 From: IceTank <61137113+IceTank@users.noreply.github.com> Date: Mon, 6 Jun 2022 17:51:28 +0200 Subject: [PATCH 027/171] Update doc add `listening` and `close` events (#1003) --- docs/API.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/API.md b/docs/API.md index 38a87eb55..091e15f39 100644 --- a/docs/API.md +++ b/docs/API.md @@ -77,6 +77,14 @@ Called when a client connects, but before any login has happened. Takes a Called when a client is logged in against server. Takes a `Client` parameter. +### `listening` event + +Called when the server is listening for connections. This means that the server is ready to accept incoming connections. + +### `close` event + +Called when the server is no longer listening to incoming connections. + ## mc.createClient(options) From 836675b5b2cec6b137f2ad6bb8e567bc0487e57e Mon Sep 17 00:00:00 2001 From: IceTank <61137113+IceTank@users.noreply.github.com> Date: Mon, 6 Jun 2022 17:53:14 +0200 Subject: [PATCH 028/171] Release 1.35.0 --- docs/HISTORY.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 07613767e..4420b89b1 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,13 @@ # History +## 1.35.0 + +* Add option to not answer to pings (@matthi4s) +* Add fallback version for dynamic version (@matthi4s) +* Add motdMsg to createServer (@IceTank & @U9G) +* Bump mocha to 10.x +* Bump standard to 17.x + ## 1.34.0 * Export defaultVersion (@matthi4s) diff --git a/package.json b/package.json index e2de6232e..07562002f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.34.0", + "version": "1.35.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 6cc3cfc08d19909c54d90f6f9e5f0866abbeb9f6 Mon Sep 17 00:00:00 2001 From: Rob9315 Date: Tue, 19 Jul 2022 22:47:18 +0200 Subject: [PATCH 029/171] add custom minecraft type `varlong` which aliases to varint (#1018) * add custom minecraft type varlong which aliases to varint for now or forever * semicolon (standard-js) --- src/datatypes/compiler-minecraft.js | 3 +++ src/datatypes/minecraft.js | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/datatypes/compiler-minecraft.js b/src/datatypes/compiler-minecraft.js index 508b2e8df..587530939 100644 --- a/src/datatypes/compiler-minecraft.js +++ b/src/datatypes/compiler-minecraft.js @@ -3,6 +3,7 @@ const minecraft = require('./minecraft') module.exports = { Read: { + varlong: ['native', minecraft.varlong[0]], UUID: ['native', (buffer, offset) => { return { value: UUID.stringify(buffer.slice(offset, 16 + offset)), @@ -44,6 +45,7 @@ module.exports = { }] }, Write: { + varlong: ['native', minecraft.varlong[1]], UUID: ['native', (value, buffer, offset) => { const buf = UUID.parse(value) buf.copy(buffer, offset) @@ -77,6 +79,7 @@ module.exports = { }] }, SizeOf: { + varlong: ['native', minecraft.varlong[2]], UUID: ['native', 16], restBuffer: ['native', (value) => { return value.length diff --git a/src/datatypes/minecraft.js b/src/datatypes/minecraft.js index b9b576145..6126674a9 100644 --- a/src/datatypes/minecraft.js +++ b/src/datatypes/minecraft.js @@ -3,8 +3,10 @@ const nbt = require('prismarine-nbt') const UUID = require('uuid-1345') const zlib = require('zlib') +const [readVarInt, writeVarInt, sizeOfVarInt] = require('protodef').types.varint module.exports = { + varlong: [readVarLong, writeVarLong, sizeOfVarLong], UUID: [readUUID, writeUUID, 16], nbt: [readNbt, writeNbt, sizeOfNbt], optionalNbt: [readOptionalNbt, writeOptionalNbt, sizeOfOptionalNbt], @@ -15,6 +17,18 @@ module.exports = { } const PartialReadError = require('protodef').utils.PartialReadError +function readVarLong (buffer, offset) { + return readVarInt(buffer, offset) +} + +function writeVarLong (value, buffer, offset) { + return writeVarInt(value, buffer, offset) +} + +function sizeOfVarLong (value) { + return sizeOfVarInt(value) +} + function readUUID (buffer, offset) { if (offset + 16 > buffer.length) { throw new PartialReadError() } return { From 50de598efefc926d5eb36362826bb5d076226dbe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Jul 2022 22:47:36 +0200 Subject: [PATCH 030/171] Bump @types/node from 17.0.45 to 18.0.6 (#1017) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 17.0.45 to 18.0.6. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 07562002f..a66b2975b 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ }, "browser": "src/browser.js", "devDependencies": { - "@types/node": "^17.0.4", + "@types/node": "^18.0.6", "espower-loader": "^1.0.0", "intelli-espower-loader": "^1.0.0", "minecraft-packets": "^1.1.5", From 446d1d9a85568e0af65c08318f3d41c1810f799b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Jul 2022 22:47:52 +0200 Subject: [PATCH 031/171] Bump readable-stream from 3.6.0 to 4.1.0 (#1014) Bumps [readable-stream](https://github.com/nodejs/readable-stream) from 3.6.0 to 4.1.0. - [Release notes](https://github.com/nodejs/readable-stream/releases) - [Commits](https://github.com/nodejs/readable-stream/compare/v3.6.0...v4.1.0) --- updated-dependencies: - dependency-name: readable-stream dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a66b2975b..3522a3785 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "prismarine-auth": "^1.1.0", "prismarine-nbt": "^2.0.0", "protodef": "^1.8.0", - "readable-stream": "^3.0.6", + "readable-stream": "^4.1.0", "uuid-1345": "^1.0.1", "yggdrasil": "^1.4.0" } From 92fd00d40d724f69333b63ea50bd45c8bba2e1cf Mon Sep 17 00:00:00 2001 From: Rob9315 Date: Thu, 21 Jul 2022 22:56:34 +0200 Subject: [PATCH 032/171] add type values to packetTest (#1023) * add varlong value to packetTest * add command_node type value in packetTest --- test/packetTest.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/test/packetTest.js b/test/packetTest.js index e70004f9f..7ffbf7bae 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -40,6 +40,7 @@ const values = { i16: -123, u16: 123, varint: 1, + varlong: -10, i8: -10, u8: 8, string: 'hi hi this is my client string', @@ -176,7 +177,30 @@ const values = { tags: [{ tagName: 'hi', entries: [1, 2, 3, 4, 5] }], ingredient: [slotValue], particleData: null, - chunkBlockEntity: { x: 10, y: 11, z: 12, type: 25 } + chunkBlockEntity: { x: 10, y: 11, z: 12, type: 25 }, + command_node: { + flags: { + has_custom_suggestions: 1, + has_redirect_node: 1, + has_command: 1, + command_node_type: 2 + }, + children: [23, 29], + redirectNode: 83, + extraNodeData: { + name: 'command_node name', + parser: 'brigadier:double', + properties: { + flags: { + max_present: 1, + min_present: 1 + }, + min: -5.0, + max: 256.0 + }, + suggestionType: 'minecraft:summonable_entities' + } + } } function getValue (_type, packet) { From e4c797d4d814865c510bfcf9251057e333a22daf Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Fri, 22 Jul 2022 22:20:55 +0200 Subject: [PATCH 033/171] Release 1.35.1 (#1024) * Update HISTORY.md * Update package.json --- docs/HISTORY.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 4420b89b1..2c1721e37 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,9 @@ # History +## 1.35.1 + +* add custom minecraft type varlong which aliases to varint @rob9315 + ## 1.35.0 * Add option to not answer to pings (@matthi4s) diff --git a/package.json b/package.json index 3522a3785..bc5fca011 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.35.0", + "version": "1.35.1", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From e079e9b0f6b5e01388ad3764403de4b9d511db78 Mon Sep 17 00:00:00 2001 From: Jordan Jones Date: Fri, 29 Jul 2022 09:03:20 -0700 Subject: [PATCH 034/171] Use offline mode as default authentication, fallback to offline mode if invalid option. (#998) * Default n-m-p to use offline unless specified. * Remove expression * actually make offline default * Revert examples * Revert all example changes * Add deprecation warning that mojang servers are no longer accepting mojang auth tokens. --- docs/API.md | 2 +- docs/README.md | 4 ++-- src/client/{auth.js => mojangAuth.js} | 2 +- src/createClient.js | 19 ++++++++++++++----- src/index.d.ts | 2 +- test/clientTest.js | 15 ++++++++++----- 6 files changed, 29 insertions(+), 15 deletions(-) rename src/client/{auth.js => mojangAuth.js} (98%) diff --git a/docs/API.md b/docs/API.md index 091e15f39..29abce3d7 100644 --- a/docs/API.md +++ b/docs/API.md @@ -93,7 +93,7 @@ Returns a `Client` instance and perform login. `options` is an object containing the properties : * username * port : default to 25565 - * auth : the type of account to use, either `microsoft` or `mojang`. default to 'mojang' + * auth : the type of account to use, either `microsoft`, `mojang`, or `offline`. default to 'offline' * password : can be omitted * (microsoft account) leave this blank to use device code auth. If you provide a password, we try to do username and password auth, but this does not always work. diff --git a/docs/README.md b/docs/README.md index c73c8fa32..3b9120509 100644 --- a/docs/README.md +++ b/docs/README.md @@ -78,7 +78,7 @@ var client = mc.createClient({ port: 25565, // optional username: "email@example.com", password: "12345678", - auth: 'mojang' // optional; by default uses mojang, if using a microsoft account, set to 'microsoft' + auth: 'microsoft' // optional; by default uses offline mode, if using a microsoft account, set to 'microsoft' }); client.on('chat', function(packet) { // Listen for chat messages and echo them back. @@ -92,7 +92,7 @@ client.on('chat', function(packet) { }); ``` -If the server is in offline mode, you may leave out the `password` option. +If the server is in offline mode, you may leave out the `password` option and switch auth to `offline`. You can also leave out `password` when using a Microsoft account. If provided, password based auth will be attempted first which may fail. *Note:* if using a Microsoft account, your account age must be >= 18 years old. ### Hello World server example diff --git a/src/client/auth.js b/src/client/mojangAuth.js similarity index 98% rename from src/client/auth.js rename to src/client/mojangAuth.js index ce74c7278..ea1a6d844 100644 --- a/src/client/auth.js +++ b/src/client/mojangAuth.js @@ -82,7 +82,7 @@ module.exports = async function (client, options) { remoteId: oldProfileObj?.remoteId ?? '', username: options.username, localId: profile, - type: (options.auth?.toLowerCase() === 'microsoft' ? 'Xbox' : 'Mojang'), + type: (options.auth?.toLowerCase() === 'mojang' ? 'Mojang' : 'Xbox'), persistent: true } auths.accounts[profile] = newProfileObj diff --git a/src/createClient.js b/src/createClient.js index ffdc5edc9..8a3ac2eaa 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -6,7 +6,7 @@ const assert = require('assert') const encrypt = require('./client/encrypt') const keepalive = require('./client/keepalive') const compress = require('./client/compress') -const auth = require('./client/auth') +const auth = require('./client/mojangAuth') const microsoftAuth = require('./client/microsoftAuth') const setProtocol = require('./client/setProtocol') const play = require('./client/play') @@ -34,10 +34,19 @@ function createClient (options) { const client = new Client(false, version.minecraftVersion, options.customPackets, hideErrors) tcpDns(client, options) - if (options.auth === 'microsoft') { - microsoftAuth.authenticate(client, options) - } else { - auth(client, options) + switch (options.auth) { + case 'mojang': + console.warn('[deprecated] mojang auth servers no longer accept mojang accounts to login. convert your account.\nhttps://help.minecraft.net/hc/en-us/articles/4403181904525-How-to-Migrate-Your-Mojang-Account-to-a-Microsoft-Account') + auth(client, options) + break + case 'microsoft': + microsoftAuth.authenticate(client, options) + break + case 'offline': + default: + client.username = options.username + options.connect(client) + break } if (options.version === false) autoVersion(client, options) setProtocol(client, options) diff --git a/src/index.d.ts b/src/index.d.ts index ac76a1d95..17bb00b06 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -101,7 +101,7 @@ declare module 'minecraft-protocol' { export interface ClientOptions { username: string port?: number - auth?: 'mojang' | 'microsoft' + auth?: 'mojang' | 'microsoft' | 'offline' password?: string host?: string clientToken?: string diff --git a/test/clientTest.js b/test/clientTest.js index 079b6e6fa..b9c219e7a 100644 --- a/test/clientTest.js +++ b/test/clientTest.js @@ -100,7 +100,8 @@ for (const supportedVersion of mc.supportedVersions) { const client = mc.createClient({ username: 'Player', version: version.minecraftVersion, - port: PORT + port: PORT, + auth: 'offline' }) client.on('error', err => done(err)) const lineListener = function (line) { @@ -149,7 +150,8 @@ for (const supportedVersion of mc.supportedVersions) { const client = mc.createClient({ username: 'Player', version: version.minecraftVersion, - port: PORT + port: PORT, + auth: 'offline' }) client.on('error', err => done(err)) client.on('login', function () { @@ -167,7 +169,8 @@ for (const supportedVersion of mc.supportedVersions) { const client = mc.createClient({ username: 'Player', version: version.minecraftVersion === '1.8.8' ? '1.11.2' : '1.8.8', - port: PORT + port: PORT, + auth: 'offline' }) client.once('error', function (err) { if (err.message.startsWith('This server is version')) { @@ -215,7 +218,8 @@ for (const supportedVersion of mc.supportedVersions) { username: process.env.MC_USERNAME, password: process.env.MC_PASSWORD, version: version.minecraftVersion, - port: PORT + port: PORT, + auth: 'offline' }) client.on('error', err => done(err)) const lineListener = function (line) { @@ -252,7 +256,8 @@ for (const supportedVersion of mc.supportedVersions) { const client = mc.createClient({ username: 'Player', version: version.minecraftVersion, - port: PORT + port: PORT, + auth: 'offline' }) client.on('error', err => done(err)) let gotKicked = false From 60379eb7d2e102c180b425178f8e525fe0417ad6 Mon Sep 17 00:00:00 2001 From: Jordan Jones Date: Fri, 29 Jul 2022 09:41:43 -0700 Subject: [PATCH 035/171] Provide interface for using not at all supported alternative accounts. (#1026) * Handle auth potentially being an function * "Simple" example of how in theroy it would work * Add package.json * Fix lint * add type declarations * Remove mcleaks example * Add doc changes * adjust warning * Fix lint from rebase manual edit. --- docs/API.md | 2 +- docs/FAQ.md | 25 +++++++---- .../client_custom_auth/client_custom_auth.js | 41 +++++++++++++++++++ examples/client_custom_auth/package.json | 10 +++++ src/createClient.js | 30 ++++++++------ src/index.d.ts | 2 +- 6 files changed, 87 insertions(+), 23 deletions(-) create mode 100644 examples/client_custom_auth/client_custom_auth.js create mode 100644 examples/client_custom_auth/package.json diff --git a/docs/API.md b/docs/API.md index 29abce3d7..ff3c78c65 100644 --- a/docs/API.md +++ b/docs/API.md @@ -93,7 +93,7 @@ Returns a `Client` instance and perform login. `options` is an object containing the properties : * username * port : default to 25565 - * auth : the type of account to use, either `microsoft`, `mojang`, or `offline`. default to 'offline' + * auth : the type of account to use, either `microsoft`, `mojang`, `offline` or `function (client, options) => void`. defaults to 'offline'. * password : can be omitted * (microsoft account) leave this blank to use device code auth. If you provide a password, we try to do username and password auth, but this does not always work. diff --git a/docs/FAQ.md b/docs/FAQ.md index eac88a4b3..7fa9e714e 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,20 +1,29 @@ -## FAQ +# FAQ This Frequently Asked Question document is meant to help people for the most common things. -### How to hide errors ? +## How to hide errors ? Use `hideErrors: true` in createClient options You may also choose to add these listeners : + ```js client.on('error', () => {}) client.on('end', () => {}) ``` -### How can I make a proxy with this ? +## How can I make a proxy with this ? + +* Check out our WIP proxy lib +* See this example +* Read this issue +* check out +* Check out this app + +## Can you support alternative auth methods? + +Supporting alternative authentcation methods has been a long standing issue with Prismarine for awhile. We do add support for using your own custom authentication method by providing a function to the `options.auth` property. In order to keep the legitimacy of the project, and to prevent bad attention from Mojang, we will not be supporting any custom authentication methods in the official repositories. + +It is up to the end user to support and maintain the authentication protocol if this is used as support in many of the official channels will be limited. -* Check out our WIP proxy lib https://github.com/PrismarineJS/prismarine-proxy -* See this example https://github.com/PrismarineJS/node-minecraft-protocol/tree/master/examples/proxy -* Read this issue https://github.com/PrismarineJS/node-minecraft-protocol/issues/712 -* check out https://github.com/Heath123/pakkit -* Check out this app https://github.com/wvffle/minecraft-packet-debugger +If you still wish to proceed, please make sure to throughly read and attempt to understand all implementations of the authentcation you wish to implement. Using an non-official authentication server can make you vulnerable to all different kinds of attacks which are not limited to insecure and/or malicious code! We will not be held responsible for anything you mess up. diff --git a/examples/client_custom_auth/client_custom_auth.js b/examples/client_custom_auth/client_custom_auth.js new file mode 100644 index 000000000..0544497ff --- /dev/null +++ b/examples/client_custom_auth/client_custom_auth.js @@ -0,0 +1,41 @@ +'use strict' + +const mc = require('minecraft-protocol') + +const [, , host, port, username, password] = process.argv +if (!username || !password) { + console.log('Usage : node client_custom_auth.js []') + process.exit(1) +} + +const client = mc.createClient({ + host, + port: parseInt(port), + username: username, + password: password, + sessionServer: '', // URL to your session server proxy that changes the expected result of mojang's seession server to mcleaks expected. + // For more information: https://github.com/PrismarineJS/node-yggdrasil/blob/master/src/Server.js#L19 + auth: async (client, options) => { + // handle custom authentication your way. + + // client.username = options.username + // options.accessToken = + return options.connect(client) + } +}) + +client.on('connect', function () { + console.info('connected') +}) +client.on('disconnect', function (packet) { + console.log('disconnected: ' + packet.reason) +}) +client.on('chat', function (packet) { + const jsonMsg = JSON.parse(packet.message) + if (jsonMsg.translate === 'chat.type.announcement' || jsonMsg.translate === 'chat.type.text') { + const username = jsonMsg.with[0].text + const msg = jsonMsg.with[1] + if (username === client.username) return + client.write('chat', { message: msg }) + } +}) diff --git a/examples/client_custom_auth/package.json b/examples/client_custom_auth/package.json new file mode 100644 index 000000000..3feec665b --- /dev/null +++ b/examples/client_custom_auth/package.json @@ -0,0 +1,10 @@ +{ + "name": "node-minecraft-protocol-example-client-custom-auth", + "version": "0.0.0", + "description": "A node-minecraft-protocol example", + "main": "client_custom_auth.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "" +} diff --git a/src/createClient.js b/src/createClient.js index 8a3ac2eaa..bee9ae1cd 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -34,19 +34,23 @@ function createClient (options) { const client = new Client(false, version.minecraftVersion, options.customPackets, hideErrors) tcpDns(client, options) - switch (options.auth) { - case 'mojang': - console.warn('[deprecated] mojang auth servers no longer accept mojang accounts to login. convert your account.\nhttps://help.minecraft.net/hc/en-us/articles/4403181904525-How-to-Migrate-Your-Mojang-Account-to-a-Microsoft-Account') - auth(client, options) - break - case 'microsoft': - microsoftAuth.authenticate(client, options) - break - case 'offline': - default: - client.username = options.username - options.connect(client) - break + if (options.auth instanceof Function) { + options.auth(client, options) + } else { + switch (options.auth) { + case 'mojang': + console.warn('[deprecated] mojang auth servers no longer accept mojang accounts to login. convert your account.\nhttps://help.minecraft.net/hc/en-us/articles/4403181904525-How-to-Migrate-Your-Mojang-Account-to-a-Microsoft-Account') + auth(client, options) + break + case 'microsoft': + microsoftAuth.authenticate(client, options) + break + case 'offline': + default: + client.username = options.username + options.connect(client) + break + } } if (options.version === false) autoVersion(client, options) setProtocol(client, options) diff --git a/src/index.d.ts b/src/index.d.ts index 17bb00b06..df915508c 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -101,7 +101,7 @@ declare module 'minecraft-protocol' { export interface ClientOptions { username: string port?: number - auth?: 'mojang' | 'microsoft' | 'offline' + auth?: 'mojang' | 'microsoft' | 'offline' | ((client: Client, options: ClientOptions) => void) password?: string host?: string clientToken?: string From d7c5053a13efec714bad1acb8d00b5a700cbb345 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Mon, 15 Aug 2022 18:57:26 -0400 Subject: [PATCH 036/171] 1.19.0 support (#1027) * Bump mcdata for 1.19 * 1.19 in version.js * 1.19.0 in ci yml * Update ci.yml * Update version.js * Update package.json * No fail fast * Update mcdata * Update package.json * Update ci.yml * [1.19] fix tests and library session code (#1020) * make tests work, add todo's * clean up, varlong test, additional todo * removed log statements, fix for older versions * Update mcdata * Update ci.yml * Update ci.yml * remove excessive version comments near supportFeature checks Co-authored-by: Romain Beaumont * chat signing implementation * Update ci.yml * move some boilerplate to pauth * update tests * update chat example * bump pauth, update doc * modify test nextMessage func * lint * update default version * add server player verifyMessage * update doc Co-authored-by: Romain Beaumont Co-authored-by: Rob9315 --- .github/workflows/ci.yml | 3 +- docs/API.md | 14 +- examples/client_chat/client_chat.js | 130 ++++++----- package.json | 4 +- src/client/encrypt.js | 29 ++- src/client/microsoftAuth.js | 4 +- src/client/play.js | 14 ++ src/client/setProtocol.js | 9 +- src/index.d.ts | 10 +- src/server/constants.js | 3 + src/server/login.js | 117 ++++++++-- src/transforms/binaryStream.js | 24 ++ src/version.js | 4 +- test/benchmark.js | 3 +- test/clientTest.js | 57 +++-- test/common/clientHelpers.js | 39 ++++ test/packetTest.js | 2 +- test/serverTest.js | 342 ++++++++++++---------------- 18 files changed, 494 insertions(+), 314 deletions(-) create mode 100644 src/server/constants.js create mode 100644 src/transforms/binaryStream.js create mode 100644 test/common/clientHelpers.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 180a6f2d1..7c4458740 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - mcVersion: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17', '1.17.1', '1.18.2'] + mcVersion: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17', '1.17.1', '1.18.2', '1.19'] + fail-fast: false steps: - uses: actions/checkout@v2 diff --git a/docs/API.md b/docs/API.md index ff3c78c65..fa4361af8 100644 --- a/docs/API.md +++ b/docs/API.md @@ -31,6 +31,7 @@ automatically logged in and validated against mojang's auth. * hideErrors : do not display errors, default to false * agent : a http agent that can be used to set proxy settings for yggdrasil authentication confirmation (see proxy-agent on npm) * validateChannelProtocol (optional) : whether or not to enable protocol validation for custom protocols using plugin channels for the connected clients. Defaults to true + * enforceSecureProfile (optional) : Kick clients that do not have chat signing keys from Mojang (1.19+) ## mc.Server(version,[customPackets]) @@ -40,6 +41,10 @@ Create a server instance for `version` of minecraft. Write a packet to all `clients` but encode it only once. +### client.verifyMessage(packet) : boolean + +Verifies if player's chat message packet was signed with their Mojang provided key + ### server.onlineModeExceptions This is a plain old JavaScript object. Add a key with the username you want to @@ -129,7 +134,7 @@ Returns a `Client` instance and perform login. 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+) ## mc.Client(isServer,version,[customPackets]) @@ -268,6 +273,13 @@ Start emitting channel events of the given name on the client object. Unregister a channel `name` and send the unregister packet if `custom` is true. +### client.signMessage(message: string, timestamp: BigInt, salt?: number) : Buffer + +Generate a signature for a chat message to be sent to server + +### client.verifyMessage(publicKey: Buffer | KeyObject, packet) : boolean + +Verifies a player chat packet sent by another player against their public key ## Not Immediately Obvious Data Type Formats diff --git a/examples/client_chat/client_chat.js b/examples/client_chat/client_chat.js index 362a9e0e1..8ba7c15a7 100644 --- a/examples/client_chat/client_chat.js +++ b/examples/client_chat/client_chat.js @@ -1,66 +1,28 @@ -const readline = require('readline') const mc = require('minecraft-protocol') -const states = mc.states - +const readline = require('readline') const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false }) -function printHelp () { - console.log('usage: node client_chat.js []') -} - -if (process.argv.length < 5) { - console.log('Too few arguments!') - printHelp() +const [,, host, port, username] = process.argv +if (!host || !port) { + console.error('Usage: node client_chat.js ') + console.error('Usage (offline mode): node client_chat.js offline') process.exit(1) } -process.argv.forEach(function (val) { - if (val === '-h') { - printHelp() - process.exit(0) - } -}) - -let host = process.argv[2] -let port = parseInt(process.argv[3]) -const user = process.argv[4] -const passwd = process.argv[5] - -let ChatMessage - -if (host.indexOf(':') !== -1) { - port = host.substring(host.indexOf(':') + 1) - host = host.substring(0, host.indexOf(':')) -} - -console.log('connecting to ' + host + ':' + port) -console.log('user: ' + user) - const client = mc.createClient({ - host: host, - port: port, - username: user, - password: passwd -}) - -client.on('kick_disconnect', function (packet) { - console.info('Kicked for ' + packet.reason) - process.exit(1) -}) - -const chats = [] - -client.on('connect', function () { - ChatMessage = require('prismarine-chat')(client.version) - console.info('Successfully connected to ' + host + ':' + port) + host, + port, + username, + auth: username === 'offline' ? 'offline' : 'microsoft' }) +// Boilerplate client.on('disconnect', function (packet) { - console.log('disconnected: ' + packet.reason) + console.log('Disconnected from server : ' + packet.reason) }) client.on('end', function () { @@ -69,19 +31,66 @@ client.on('end', function () { }) client.on('error', function (err) { - console.log('Error occured') + console.log('Error occurred') console.log(err) process.exit(1) }) +client.on('connect', () => { + const mcData = require('minecraft-data')(client.version) + const ChatMessage = require('prismarine-chat')(client.version) + const players = {} // 1.19+ + + console.log('Connected to server') + + client.chat = (message) => { + if (mcData.supportFeature('signedChat')) { + const timestamp = BigInt(Date.now()) + client.write('chat_message', { + message, + timestamp, + salt: 0, + signature: client.signMessage(message, timestamp) + }) + } else { + client.write('chat', { message }) + } + } + + function onChat (packet) { + const message = packet.message || packet.unsignedChatContent || packet.signedChatContent + const j = JSON.parse(message) + const chat = new ChatMessage(j) + + if (packet.signature) { + const verified = client.verifyMessage(players[packet.senderUuid].publicKey, packet) + console.info(verified ? 'Verified: ' : 'UNVERIFIED: ', chat.toAnsi()) + } else { + console.info(chat.toAnsi()) + } + } + + client.on('chat', onChat) + client.on('player_chat', onChat) + client.on('player_info', (packet) => { + if (packet.action === 0) { // add player + for (const player of packet.data) { + players[player.UUID] = player.crypto + } + } + }) +}) + +// Send the queued messages +const queuedChatMessages = [] client.on('state', function (newState) { - if (newState === states.PLAY) { - chats.forEach(function (chat) { - client.write('chat', { message: chat }) - }) + if (newState === mc.states.PLAY) { + queuedChatMessages.forEach(message => client.chat(message)) + queuedChatMessages.length = 0 } }) +// Listen for messages written to the console, send them to game chat rl.on('line', function (line) { if (line === '') { return @@ -93,14 +102,9 @@ rl.on('line', function (line) { console.info('Forcibly ended client') process.exit(0) } - if (!client.write('chat', { message: line })) { - chats.push(line) + if (!client.chat) { + queuedChatMessages.push(line) + } else { + client.chat(line) } }) - -client.on('chat', function (packet) { - if (!ChatMessage) return // Return if ChatMessage is not loaded yet. - const j = JSON.parse(packet.message) - const chat = new ChatMessage(j) - console.info(chat.toAnsi()) -}) diff --git a/package.json b/package.json index bc5fca011..dc915b7c9 100644 --- a/package.json +++ b/package.json @@ -51,11 +51,11 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^3.0.0", + "minecraft-data": "^3.8.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", - "prismarine-auth": "^1.1.0", + "prismarine-auth": "^1.7.0", "prismarine-nbt": "^2.0.0", "protodef": "^1.8.0", "readable-stream": "^4.1.0", diff --git a/src/client/encrypt.js b/src/client/encrypt.js index d9ff607c8..b9d21bab9 100644 --- a/src/client/encrypt.js +++ b/src/client/encrypt.js @@ -3,6 +3,7 @@ const crypto = require('crypto') const debug = require('debug')('minecraft-protocol') const yggdrasil = require('yggdrasil') +const { concat } = require('../transforms/binaryStream') module.exports = function (client, options) { const yggdrasilServer = yggdrasil.server({ agent: options.agent, host: options.sessionServer || 'https://sessionserver.mojang.com' }) @@ -42,13 +43,33 @@ module.exports = function (client, options) { } function sendEncryptionKeyResponse () { + const mcData = require('minecraft-data')(client.version) + const pubKey = mcPubKeyToPem(packet.publicKey) const encryptedSharedSecretBuffer = crypto.publicEncrypt({ key: pubKey, padding: crypto.constants.RSA_PKCS1_PADDING }, sharedSecret) const encryptedVerifyTokenBuffer = crypto.publicEncrypt({ key: pubKey, padding: crypto.constants.RSA_PKCS1_PADDING }, packet.verifyToken) - client.write('encryption_begin', { - sharedSecret: encryptedSharedSecretBuffer, - verifyToken: encryptedVerifyTokenBuffer - }) + + if (mcData.supportFeature('signatureEncryption')) { + const salt = BigInt(Date.now()) + client.write('encryption_begin', { + sharedSecret: encryptedSharedSecretBuffer, + hasVerifyToken: client.profileKeys == null, + crypto: client.profileKeys + ? { + salt, + messageSignature: crypto.sign('sha256WithRSAEncryption', + concat('buffer', packet.verifyToken, 'i64', salt), client.profileKeys.private) + } + : { + verifyToken: encryptedVerifyTokenBuffer + } + }) + } else { + client.write('encryption_begin', { + sharedSecret: encryptedSharedSecretBuffer, + verifyToken: encryptedVerifyTokenBuffer + }) + } client.setEncryption(sharedSecret) } } diff --git a/src/client/microsoftAuth.js b/src/client/microsoftAuth.js index 7f88cf119..200e2428d 100644 --- a/src/client/microsoftAuth.js +++ b/src/client/microsoftAuth.js @@ -14,7 +14,7 @@ async function authenticate (client, options) { } const Authflow = new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode) - const { token, entitlements, profile } = await Authflow.getMinecraftJavaToken({ fetchProfile: true }).catch(e => { + const { token, entitlements, profile, certificates } = await Authflow.getMinecraftJavaToken({ fetchProfile: true, fetchCertificates: !options.disableChatSigning }).catch(e => { if (options.password) console.warn('Sign in failed, try removing the password field\n') if (e.toString().includes('Not Found')) console.warn(`Please verify that the account ${options.username} owns Minecraft\n`) throw e @@ -32,8 +32,10 @@ async function authenticate (client, options) { selectedProfile: profile, availableProfile: [profile] } + Object.assign(client, certificates) client.session = session client.username = profile.name + options.accessToken = token client.emit('session', session) options.connect(client) diff --git a/src/client/play.js b/src/client/play.js index a5efb73cc..1274adff9 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -1,4 +1,6 @@ const states = require('../states') +const crypto = require('crypto') +const concat = require('../transforms/binaryStream').concat module.exports = function (client, options) { client.once('success', onLogin) @@ -7,5 +9,17 @@ module.exports = function (client, options) { client.state = states.PLAY client.uuid = packet.uuid client.username = packet.username + client.signMessage = (message, timestamp, salt = 0) => { + if (!client.profileKeys) throw Error("Can't sign message without profile keys, please set valid auth mode") + const signable = concat('i64', salt, 'UUID', client.uuid, 'i64', + timestamp / 1000n, 'pstring', JSON.stringify({ text: message })) + return crypto.sign('RSA-SHA256', signable, client.profileKeys.private) + } + client.verifyMessage = (pubKey, packet) => { + if (pubKey instanceof Buffer) pubKey = crypto.createPublicKey({ key: pubKey, format: 'der', type: 'spki' }) + const signable = concat('i64', packet.salt, 'UUID', packet.senderUuid, + 'i64', packet.timestamp / 1000n, 'pstring', packet.signedChatContent) + return crypto.verify('RSA-SHA256', signable, pubKey, packet.signature) + } } } diff --git a/src/client/setProtocol.js b/src/client/setProtocol.js index 5eea6527a..fe835c782 100644 --- a/src/client/setProtocol.js +++ b/src/client/setProtocol.js @@ -25,7 +25,14 @@ module.exports = function (client, options) { }) client.state = states.LOGIN client.write('login_start', { - username: client.username + username: client.username, + signature: client.profileKeys + ? { + timestamp: BigInt(client.profileKeys.expiresOn.getTime()), // should probably be called "expireTime" + publicKey: client.profileKeys.publicDER, + signature: client.profileKeys.signature + } + : null }) } } diff --git a/src/index.d.ts b/src/index.d.ts index df915508c..2725b8114 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -5,6 +5,7 @@ import { Socket } from 'net' import * as Stream from 'stream' import { Agent } from 'http' import { Transform } from "readable-stream"; +import { KeyObject } from 'crypto'; type PromiseLike = Promise | void @@ -34,6 +35,8 @@ declare module 'minecraft-protocol' { registerChannel(name: string, typeDefinition: any, custom?: boolean): void unregisterChannel(name: string): void writeChannel(channel: any, params: any): void + signMessage(message: string, timestamp: BigInt, salt?: number): Buffer + verifyMessage(publicKey: Buffer | KeyObject, packet: object): boolean on(event: 'error', listener: (error: Error) => PromiseLike): this on(event: 'packet', handler: (data: any, packetMeta: PacketMeta, buffer: Buffer, fullBuffer: Buffer) => PromiseLike): this on(event: 'raw', handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this @@ -125,7 +128,9 @@ declare module 'minecraft-protocol' { onMsaCode?: (data: MicrosoftDeviceAuthorizationResponse) => void id?: number session?: SessionOption - validateChannelProtocol?: boolean + validateChannelProtocol?: boolean, + // 1.19+ + disableChatSigning: boolean } export class Server extends EventEmitter { @@ -173,6 +178,9 @@ declare module 'minecraft-protocol' { hideErrors?: boolean agent?: Agent validateChannelProtocol?: boolean + // 1.19+ + // Require connecting clients to have chat signing support enabled + enforceSecureProfile: boolean } export interface SerializerOptions { diff --git a/src/server/constants.js b/src/server/constants.js new file mode 100644 index 000000000..be719cad3 --- /dev/null +++ b/src/server/constants.js @@ -0,0 +1,3 @@ +module.exports = { + mojangPublicKeyPem: '-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAylB4B6m5lz7jwrcFz6Fd\n/fnfUhcvlxsTSn5kIK/2aGG1C3kMy4VjhwlxF6BFUSnfxhNswPjh3ZitkBxEAFY2\n5uzkJFRwHwVA9mdwjashXILtR6OqdLXXFVyUPIURLOSWqGNBtb08EN5fMnG8iFLg\nEJIBMxs9BvF3s3/FhuHyPKiVTZmXY0WY4ZyYqvoKR+XjaTRPPvBsDa4WI2u1zxXM\neHlodT3lnCzVvyOYBLXL6CJgByuOxccJ8hnXfF9yY4F0aeL080Jz/3+EBNG8RO4B\nyhtBf4Ny8NQ6stWsjfeUIvH7bU/4zCYcYOq4WrInXHqS8qruDmIl7P5XXGcabuzQ\nstPf/h2CRAUpP/PlHXcMlvewjmGU6MfDK+lifScNYwjPxRo4nKTGFZf/0aqHCh/E\nAsQyLKrOIYRE0lDG3bzBh8ogIMLAugsAfBb6M3mqCqKaTMAf/VAjh5FFJnjS+7bE\n+bZEV0qwax1CEoPPJL1fIQjOS8zj086gjpGRCtSy9+bTPTfTR/SJ+VUB5G2IeCIt\nkNHpJX2ygojFZ9n5Fnj7R9ZnOM+L8nyIjPu3aePvtcrXlyLhH/hvOfIOjPxOlqW+\nO5QwSFP4OEcyLAUgDdUgyW36Z5mB285uKW/ighzZsOTevVUG2QwDItObIV6i8RCx\nFbN2oDHyPaO5j1tTaBNyVt8CAwEAAQ==\n-----END PUBLIC KEY-----' +} diff --git a/src/server/login.js b/src/server/login.js index e9f0b58a8..12c7d858d 100644 --- a/src/server/login.js +++ b/src/server/login.js @@ -1,11 +1,14 @@ const UUID = require('uuid-1345') -const bufferEqual = require('buffer-equal') const crypto = require('crypto') const pluginChannels = require('../client/pluginChannels') const states = require('../states') const yggdrasil = require('yggdrasil') +const { concat } = require('../transforms/binaryStream') +const { mojangPublicKeyPem } = require('./constants') module.exports = function (client, server, options) { + const mojangPubKey = crypto.createPublicKey(mojangPublicKeyPem) + const raise = (translatableError) => client.end(translatableError, JSON.stringify({ translate: translatableError })) const yggdrasilServer = yggdrasil.server({ agent: options.agent }) const { 'online-mode': onlineMode = true, @@ -21,15 +24,49 @@ module.exports = function (client, server, options) { client.on('end', () => { clearTimeout(loginKickTimer) }) - client.once('login_start', onLogin) + function kickForNotLoggingIn () { + client.end('LoginTimeout') + } let loginKickTimer = setTimeout(kickForNotLoggingIn, kickTimeout) function onLogin (packet) { + const mcData = require('minecraft-data')(client.version) + client.username = packet.username const isException = !!server.onlineModeExceptions[client.username.toLowerCase()] const needToVerify = (onlineMode && !isException) || (!onlineMode && isException) + + if (mcData.supportFeature('signatureEncryption')) { + if (options.enforceSecureProfile && !packet.signature) { + raise('multiplayer.disconnect.missing_public_key') + return + } + } + + if (packet.signature) { + if (packet.signature.timestamp < BigInt(Date.now())) { + raise('multiplayer.disconnect.invalid_public_key_signature') + return // expired tokens, client needs to restart game + } + + try { + const publicKey = crypto.createPublicKey({ key: packet.signature.publicKey, format: 'der', type: 'spki' }) + const publicPEM = mcPubKeyToPem(packet.signature.publicKey) + const signable = packet.signature.timestamp + publicPEM // (expires at + publicKey) + + if (!crypto.verify('RSA-SHA1', Buffer.from(signable, 'utf8'), mojangPubKey, packet.signature.signature)) { + raise('multiplayer.disconnect.invalid_public_key_signature') + return + } + client.profileKeys = { public: publicKey, publicPEM } + } catch (err) { + raise('multiplayer.disconnect.invalid_public_key') + return + } + } + if (needToVerify) { serverId = crypto.randomBytes(4).toString('hex') client.verifyToken = crypto.randomBytes(4) @@ -41,7 +78,7 @@ module.exports = function (client, server, options) { client.publicKey = Buffer.from(publicKeyStr, 'base64') client.once('encryption_begin', onEncryptionKeyResponse) client.write('encryption_begin', { - serverId: serverId, + serverId, publicKey: client.publicKey, verifyToken: client.verifyToken }) @@ -50,23 +87,51 @@ module.exports = function (client, server, options) { } } - function kickForNotLoggingIn () { - client.end('LoginTimeout') - } - function onEncryptionKeyResponse (packet) { - let sharedSecret - try { - const verifyToken = crypto.privateDecrypt({ key: server.serverKey.exportKey(), padding: crypto.constants.RSA_PKCS1_PADDING }, packet.verifyToken) - if (!bufferEqual(client.verifyToken, verifyToken)) { + if (client.profileKeys) { + if (options.enforceSecureProfile && packet.hasVerifyToken) { + raise('multiplayer.disconnect.missing_public_key') + return // Unexpected - client has profile keys, and we expect secure profile + } + } + + if (packet.hasVerifyToken === false) { + // 1.19, hasVerifyToken is set and equal to false IF chat signing is enabled + // This is the default action starting in 1.19.1. + const signable = concat('buffer', client.verifyToken, 'i64', packet.crypto.salt) + if (!crypto.verify('sha256WithRSAEncryption', signable, client.profileKeys.public, packet.crypto.messageSignature)) { + raise('multiplayer.disconnect.invalid_public_key_signature') + return + } + } else { + const encryptedToken = packet.hasVerifyToken ? packet.crypto.verifyToken : packet.verifyToken + try { + const decryptedToken = crypto.privateDecrypt({ + key: server.serverKey.exportKey(), + padding: crypto.constants.RSA_PKCS1_PADDING + }, encryptedToken) + + if (!client.verifyToken.equals(decryptedToken)) { + client.end('DidNotEncryptVerifyTokenProperly') + return + } + } catch { client.end('DidNotEncryptVerifyTokenProperly') return } - sharedSecret = crypto.privateDecrypt({ key: server.serverKey.exportKey(), padding: crypto.constants.RSA_PKCS1_PADDING }, packet.sharedSecret) + } + + let sharedSecret + try { + sharedSecret = crypto.privateDecrypt({ + key: server.serverKey.exportKey(), + padding: crypto.constants.RSA_PKCS1_PADDING + }, packet.sharedSecret) } catch (e) { client.end('DidNotEncryptVerifyTokenProperly') return } + client.setEncryption(sharedSecret) const isException = !!server.onlineModeExceptions[client.username.toLowerCase()] @@ -114,7 +179,12 @@ module.exports = function (client, server, options) { client.write('compress', { threshold: 256 }) // Default threshold is 256 client.compressionThreshold = 256 } - client.write('success', { uuid: client.uuid, username: client.username }) + client.write('success', { + uuid: client.uuid, + username: client.username, + properties: [] + }) + // TODO: find out what properties are on 'success' packet client.state = states.PLAY clearTimeout(loginKickTimer) @@ -125,6 +195,27 @@ module.exports = function (client, server, options) { server.playerCount -= 1 }) pluginChannels(client, options) + + if (client.profileKeys) { + client.verifyMessage = (packet) => { + const signable = concat('i64', packet.salt, 'UUID', client.uuid, 'i64', + packet.timestamp, 'pstring', packet.message) + + return crypto.verify('sha256WithRSAEncryption', signable, client.profileKeys.public, packet.crypto.signature) + } + } server.emit('login', client) } } + +function mcPubKeyToPem (mcPubKeyBuffer) { + let pem = '-----BEGIN RSA PUBLIC KEY-----\n' + let base64PubKey = mcPubKeyBuffer.toString('base64') + const maxLineLength = 76 + while (base64PubKey.length > 0) { + pem += base64PubKey.substring(0, maxLineLength) + '\n' + base64PubKey = base64PubKey.substring(maxLineLength) + } + pem += '-----END RSA PUBLIC KEY-----\n' + return pem +} diff --git a/src/transforms/binaryStream.js b/src/transforms/binaryStream.js new file mode 100644 index 000000000..6f0f53ed5 --- /dev/null +++ b/src/transforms/binaryStream.js @@ -0,0 +1,24 @@ +const types = {} +Object.assign(types, require('protodef').types) +Object.assign(types, require('../datatypes/minecraft')) + +function concat (...args) { + let allocLen = 0 + for (let i = 0; i < args.length; i += 2) { + const type = args[i] + const value = args[i + 1] + const [,, s] = types[type] + allocLen += typeof s === 'number' ? s : s(value, {}) + } + const buffer = Buffer.alloc(allocLen) + let offset = 0 + for (let i = 0; i < args.length; i += 2) { + const type = args[i] + const value = args[i + 1] + offset = types[type][1](value, buffer, offset, {}) + } + return buffer +} + +// concat('i32', 22, 'i64', 2n) => +module.exports = { concat } diff --git a/src/version.js b/src/version.js index 638545104..ce23f51ef 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.18.2', - supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2'] + defaultVersion: '1.19', + supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19'] } diff --git a/test/benchmark.js b/test/benchmark.js index e31c901f0..4dc87f68b 100644 --- a/test/benchmark.js +++ b/test/benchmark.js @@ -7,7 +7,8 @@ const states = mc.states const testDataWrite = [ { name: 'keep_alive', params: { keepAliveId: 957759560 } }, - { name: 'chat', params: { message: ' Hello World!' } }, + // TODO: 1.19+ `chat` -> `player_chat` feature toggle + // { name: 'chat', params: { message: ' Hello World!' } }, { name: 'position_look', params: { x: 6.5, y: 65.62, stance: 67.24, z: 7.5, yaw: 0, pitch: 0, onGround: true } } // TODO: add more packets for better quality data ] diff --git a/test/clientTest.js b/test/clientTest.js index b9c219e7a..01d0b2494 100644 --- a/test/clientTest.js +++ b/test/clientTest.js @@ -4,16 +4,16 @@ const mc = require('../') const os = require('os') const path = require('path') const assert = require('power-assert') -const SURVIVE_TIME = 10000 const util = require('util') +const applyClientHelpers = require('./common/clientHelpers') +const download = util.promisify(require('minecraft-wrap').download) +const { getPort } = require('./common/util') + +const SURVIVE_TIME = 10000 const MC_SERVER_PATH = path.join(__dirname, 'server') const Wrap = require('minecraft-wrap').Wrap -const download = util.promisify(require('minecraft-wrap').download) - -const { getPort } = require('./common/util') - for (const supportedVersion of mc.supportedVersions) { let PORT = null const mcData = require('minecraft-data')(supportedVersion) @@ -97,12 +97,12 @@ for (const supportedVersion of mc.supportedVersions) { }) it('connects successfully - offline mode', function (done) { - const client = mc.createClient({ + const client = applyClientHelpers(mc.createClient({ username: 'Player', version: version.minecraftVersion, port: PORT, auth: 'offline' - }) + })) client.on('error', err => done(err)) const lineListener = function (line) { const match = line.match(/\[Server thread\/INFO\]: <(.+?)> (.+)/) @@ -110,15 +110,16 @@ for (const supportedVersion of mc.supportedVersions) { assert.strictEqual(match[1], 'Player') assert.strictEqual(match[2], 'hello everyone; I have logged in.') wrap.writeServer('say hello\n') + wrap.off('line', lineListener) } wrap.on('line', lineListener) let chatCount = 0 client.on('login', function (packet) { assert.strictEqual(packet.gameMode, 0) - client.write('chat', { - message: 'hello everyone; I have logged in.' - }) + client.chat('hello everyone; I have logged in.') }) + + // 1.18 and below client.on('chat', function (packet) { chatCount += 1 assert.ok(chatCount <= 2) @@ -144,20 +145,34 @@ for (const supportedVersion of mc.supportedVersions) { done() } }) + + // 1.19 and above + let gotClientMessage, gotServerMessage + client.on('player_chat', (packet) => { + const message = JSON.parse(packet.unsignedChatContent || packet.signedChatContent) + // const sender = JSON.parse(packet.senderName) + + if (message.text === 'hello everyone; I have logged in.') gotClientMessage = true + if (message.text === 'hello') gotServerMessage = true + + if (gotClientMessage && gotServerMessage) { + wrap.removeListener('line', lineListener) + client.end() + done() + } + }) }) it('does not crash for ' + SURVIVE_TIME + 'ms', function (done) { - const client = mc.createClient({ + const client = applyClientHelpers(mc.createClient({ username: 'Player', version: version.minecraftVersion, port: PORT, auth: 'offline' - }) + })) client.on('error', err => done(err)) client.on('login', function () { - client.write('chat', { - message: 'hello everyone; I have logged in.' - }) + client.chat('hello everyone; I have logged in.') setTimeout(function () { client.end() done() @@ -166,6 +181,7 @@ for (const supportedVersion of mc.supportedVersions) { }) it('produce a decent error when connecting with the wrong version', function (done) { + if (process.platform === 'win32') return done() const client = mc.createClient({ username: 'Player', version: version.minecraftVersion === '1.8.8' ? '1.11.2' : '1.8.8', @@ -214,13 +230,12 @@ for (const supportedVersion of mc.supportedVersions) { }) it('connects successfully - online mode', function (done) { - const client = mc.createClient({ + const client = applyClientHelpers(mc.createClient({ username: process.env.MC_USERNAME, password: process.env.MC_PASSWORD, version: version.minecraftVersion, - port: PORT, - auth: 'offline' - }) + port: PORT + })) client.on('error', err => done(err)) const lineListener = function (line) { const match = line.match(/\[Server thread\/INFO\]: <(.+?)> (.+)/) @@ -235,9 +250,7 @@ for (const supportedVersion of mc.supportedVersions) { assert.strictEqual(packet.difficulty, 1) assert.strictEqual(packet.dimension, 0) assert.strictEqual(packet.gameMode, 0) - client.write('chat', { - message: 'hello everyone; I have logged in.' - }) + client.chat('hello everyone; I have logged in.') }) let chatCount = 0 client.on('chat', function (packet) { diff --git a/test/common/clientHelpers.js b/test/common/clientHelpers.js new file mode 100644 index 000000000..7ebe0d386 --- /dev/null +++ b/test/common/clientHelpers.js @@ -0,0 +1,39 @@ +module.exports = client => { + const mcData = require('minecraft-data')(client.version) + const hasSignedChat = mcData.supportFeature('signedChat') + + client.chat = (message) => { + if (hasSignedChat) { + const timestamp = BigInt(Date.now()) + client.write('chat_message', { + message, + timestamp, + salt: 0, + signature: Buffer.alloc(0) + }) + } else { + client.write('chat', { message }) + } + } + + client.nextMessage = (containing) => { + return new Promise((resolve) => { + function onChat (packet) { + const m = packet.message || packet.unsignedChatContent || packet.signedChatContent + if (containing) { + if (m.includes(containing)) return finish(m) + else return + } + return finish(m) + } + client.on(hasSignedChat ? 'player_chat' : 'chat', onChat) + + function finish (m) { + client.off(hasSignedChat ? 'player_chat' : 'chat', onChat) + resolve(m) + } + }) + } + + return client +} diff --git a/test/packetTest.js b/test/packetTest.js index 7ffbf7bae..05682525d 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -40,7 +40,7 @@ const values = { i16: -123, u16: 123, varint: 1, - varlong: -10, + varlong: -20, i8: -10, u8: 8, string: 'hi hi this is my client string', diff --git a/test/serverTest.js b/test/serverTest.js index e66a776d0..d1215620c 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -3,77 +3,100 @@ const mc = require('../') const assert = require('power-assert') const { once } = require('events') +const nbt = require('prismarine-nbt') +const applyClientHelpers = require('./common/clientHelpers') const { getPort } = require('./common/util') -const w = { - piglin_safe: { - type: 'byte', - value: 0 - }, - natural: { - type: 'byte', - value: 1 - }, - ambient_light: { - type: 'float', - value: 0 - }, - infiniburn: { - type: 'string', - value: 'minecraft:infiniburn_overworld' - }, - respawn_anchor_works: { - type: 'byte', - value: 0 - }, - has_skylight: { - type: 'byte', - value: 1 - }, - bed_works: { - type: 'byte', - value: 1 - }, - has_raids: { - type: 'byte', - value: 1 - }, - name: { - type: 'string', - value: 'minecraft:overworld' - }, - logical_height: { - type: 'int', - value: 256 - }, - shrunk: { - type: 'byte', - value: 0 - }, - ultrawarm: { - type: 'byte', - value: 0 - }, - has_ceiling: { - type: 'byte', - value: 0 - } -} +const w = nbt.comp({ + piglin_safe: nbt.byte(0), + natural: nbt.byte(1), + ambient_light: nbt.float(0), + infiniburn: nbt.string('minecraft:infiniburn_overworld'), + respawn_anchor_works: nbt.byte(0), + has_skylight: nbt.byte(1), + bed_works: nbt.byte(1), + has_raids: nbt.byte(1), + name: nbt.string('minecraft:overworld'), + logical_height: nbt.int(256), + shrunk: nbt.byte(0), + ultrawarm: nbt.byte(0), + has_ceiling: nbt.byte(0) +}) for (const supportedVersion of mc.supportedVersions) { let PORT const mcData = require('minecraft-data')(supportedVersion) const version = mcData.version - describe('mc-server ' + version.minecraftVersion, function () { + const loginPacket = (client, server) => { + return { + // 1.7 + entityId: client.id, + gameMode: 1, + dimension: (version.version >= 735 ? mcData.loginPacket.dimension : 0), + difficulty: 2, + maxPlayers: server.maxPlayers, + levelType: 'default', + // 1.8 + reducedDebugInfo: (version.version >= 735 ? false : 0), + // 1.14 + // removes `difficulty` + viewDistance: 10, + // 1.15 + hashedSeed: [0, 0], + enableRespawnScreen: true, + // 1.16 + // removed levelType + previousGameMode: version.version >= 755 ? 0 : 255, + worldNames: ['minecraft:overworld'], + dimensionCodec: version.version >= 755 ? mcData.loginPacket.dimensionCodec : (version.version >= 735 ? mcData.loginPacket.dimension : { name: '', type: 'compound', value: { dimension: { type: 'list', value: { type: 'compound', value: [w] } } } }), + worldName: 'minecraft:overworld', + isDebug: false, + isFlat: false, + // 1.16.2 + isHardcore: false, + // 1.18 + simulationDistance: 10, + // 1.19 + // removed `dimension` + // removed `dimensionCodec` + registryCodec: { + "type": "compound", + "name": "", + "value": {} + }, + worldType: "minecraft:overworld", + death: undefined + // more to be added + } + } + function sendBroadcastMessage(server, clients, message, sender) { + if (mcData.supportFeature('signedChat')) { + server.writeToClients(clients, 'player_chat', { + signedChatContent: '', + unsignedChatContent: JSON.stringify({ text: message }), + type: 0, + senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random + senderName: JSON.stringify({ text: sender }), + senderTeam: undefined, + timestamp: Date.now(), + salt: 0n, + signature: Buffer.alloc(0) + }) + } else { + server.writeToClients(clients, 'chat', { message: JSON.stringify({ text: message }), position: 0, sender: sender || '0' }) + } + } + + describe('mc-server ' + version.minecraftVersion, function () { + this.timeout(5000) this.beforeAll(async function() { PORT = await getPort() console.log(`Using port for tests: ${PORT}`) }) - this.timeout(5000) it('starts listening and shuts down cleanly', function (done) { const server = mc.createServer({ 'online-mode': false, @@ -90,6 +113,7 @@ for (const supportedVersion of mc.supportedVersions) { done() }) }) + it('kicks clients that do not log in', function (done) { const server = mc.createServer({ 'online-mode': false, @@ -105,14 +129,10 @@ for (const supportedVersion of mc.supportedVersions) { server.close() }) }) - server.on('close', function () { - resolve() - }) + server.on('close', resolve) server.on('listening', function () { const client = new mc.Client(false, version.minecraftVersion) - client.on('end', function () { - resolve() - }) + client.on('end', resolve) client.connect(PORT, '127.0.0.1') }) @@ -121,6 +141,7 @@ for (const supportedVersion of mc.supportedVersions) { if (count <= 0) done() } }) + it('kicks clients that do not send keepalive packets', function (done) { const server = mc.createServer({ 'online-mode': false, @@ -136,9 +157,7 @@ for (const supportedVersion of mc.supportedVersions) { server.close() }) }) - server.on('close', function () { - resolve() - }) + server.on('close', resolve) server.on('listening', function () { const client = mc.createClient({ username: 'superpants', @@ -147,15 +166,14 @@ for (const supportedVersion of mc.supportedVersions) { keepAlive: false, version: version.minecraftVersion }) - client.on('end', function () { - resolve() - }) + client.on('end', resolve) }) function resolve () { count -= 1 if (count <= 0) done() } }) + it('responds to ping requests', function (done) { const chatMotd = { // Generated with prismarine-chat MessageBuilder on version 1.16 may change in the future extra: [{ color: 'red', text: 'Red text' }], @@ -191,7 +209,7 @@ for (const supportedVersion of mc.supportedVersions) { online: 0, sample: [] }, - description: { + description: { extra: [ { color: 'red', text: 'Red text' } ], bold: true, text: 'Example chat mesasge' @@ -202,6 +220,7 @@ for (const supportedVersion of mc.supportedVersions) { }) server.on('close', done) }) + it('responds with chatMessage motd\'s', function (done) { const server = mc.createServer({ 'online-mode': false, @@ -237,6 +256,7 @@ for (const supportedVersion of mc.supportedVersions) { }) server.on('close', done) }) + it('clients can be changed by beforeLogin', function (done) { const notchUUID = '069a79f4-44e9-4726-a5be-fca90e38aaf5' const server = mc.createServer({ @@ -263,12 +283,16 @@ for (const supportedVersion of mc.supportedVersions) { }) server.on('close', done) }) + it('clients can log in and chat', function (done) { const server = mc.createServer({ 'online-mode': false, version: version.minecraftVersion, port: PORT }) + const broadcast = (message, exclude) => sendBroadcastMessage(server, + Object.values(server.clients).filter(client => client !== exclude), message) + const username = ['player1', 'player2'] let index = 0 server.on('login', function (client) { @@ -279,86 +303,49 @@ for (const supportedVersion of mc.supportedVersions) { broadcast(client.username + ' left the game.', client) if (client.username === 'player2') server.close() }) - const loginPacket = { - entityId: client.id, - levelType: 'default', - gameMode: 1, - previousGameMode: version.version >= 755 ? 0 : 255, - worldNames: ['minecraft:overworld'], - dimensionCodec: version.version >= 755 ? mcData.loginPacket.dimensionCodec : (version.version >= 735 ? mcData.loginPacket.dimension : { name: '', type: 'compound', value: { dimension: { type: 'list', value: { type: 'compound', value: [w] } } } }), - dimension: (version.version >= 735 ? mcData.loginPacket.dimension : 0), - worldName: 'minecraft:overworld', - hashedSeed: [0, 0], - difficulty: 2, - maxPlayers: server.maxPlayers, - reducedDebugInfo: (version.version >= 735 ? false : 0), - enableRespawnScreen: true - } - if (version.version >= 735) { // 1.16x - loginPacket.isDebug = false - loginPacket.isFlat = false - loginPacket.isHardcore = false - loginPacket.viewDistance = 10 - delete loginPacket.levelType - delete loginPacket.difficulty - } - client.write('login', loginPacket) - client.on('chat', function (packet) { - const message = '<' + client.username + '>' + ' ' + packet.message - broadcast(message) - }) + client.write('login', loginPacket(client, server)) + + const handleChat = (packet) => broadcast(`<${client.username}> ${packet.message}`) + client.on('chat', handleChat) + client.on('chat_message', handleChat) }) server.on('close', done) + server.on('listening', function () { - const player1 = mc.createClient({ + const player1 = applyClientHelpers(mc.createClient({ username: 'player1', host: '127.0.0.1', version: version.minecraftVersion, port: PORT - }) - player1.on('login', function (packet) { + })) + player1.on('login', async function (packet) { assert.strictEqual(packet.gameMode, 1) - player1.once('chat', function (packet) { - assert.strictEqual(packet.message, '{"text":"player2 joined the game."}') - player1.once('chat', function (packet) { - assert.strictEqual(packet.message, '{"text":" hi"}') - player2.once('chat', fn) - function fn (packet) { - if (//.test(packet.message)) { - player2.once('chat', fn) - return - } - assert.strictEqual(packet.message, '{"text":" hello"}') - player1.once('chat', function (packet) { - assert.strictEqual(packet.message, '{"text":"player2 left the game."}') - player1.end() - }) - player2.end() - } - - player1.write('chat', { message: 'hello' }) - }) - player2.write('chat', { message: 'hi' }) - }) - const player2 = mc.createClient({ + const player2 = applyClientHelpers(mc.createClient({ username: 'player2', host: '127.0.0.1', version: version.minecraftVersion, port: PORT - }) - }) - }) + })) - function broadcast (message, exclude) { - let client - for (const clientId in server.clients) { - if (server.clients[clientId] === undefined) continue + const p1Join = await player1.nextMessage('player2') + assert.strictEqual(p1Join, '{"text":"player2 joined the game."}') - client = server.clients[clientId] - if (client !== exclude) client.write('chat', { message: JSON.stringify({ text: message }), position: 0, sender: '0' }) - } - } + player2.chat('hi') + const p2hi = await player1.nextMessage('player2') + assert.strictEqual(p2hi, '{"text":" hi"}') + + player1.chat('hello') + const p1hello = await player2.nextMessage('player1') + assert.strictEqual(p1hello, '{"text":" hello"}') + + player2.end() + const p2leaving = await player1.nextMessage('player2') + assert.strictEqual(p2leaving, '{"text":"player2 left the game."}') + player1.end() + }) + }) }) + it('kicks clients when invalid credentials', function (done) { this.timeout(10000) const server = mc.createServer({ @@ -372,9 +359,7 @@ for (const supportedVersion of mc.supportedVersions) { server.close() }) }) - server.on('close', function () { - resolve() - }) + server.on('close', resolve) server.on('listening', function () { resolve() const client = mc.createClient({ @@ -383,15 +368,14 @@ for (const supportedVersion of mc.supportedVersions) { version: version.minecraftVersion, port: PORT }) - client.on('end', function () { - resolve() - }) + client.on('end', resolve) }) function resolve () { count -= 1 if (count <= 0) done() } }) + it('gives correct reason for kicking clients when shutting down', function (done) { const server = mc.createServer({ 'online-mode': false, @@ -404,34 +388,9 @@ for (const supportedVersion of mc.supportedVersions) { assert.strictEqual(reason, 'ServerShutdown') resolve() }) - const loginPacket = { - entityId: client.id, - levelType: 'default', - gameMode: 1, - previousGameMode: version.version >= 755 ? 0 : 255, - worldNames: ['minecraft:overworld'], - dimensionCodec: version.version >= 755 ? mcData.loginPacket.dimensionCodec : (version.version >= 735 ? mcData.loginPacket.dimension : { name: '', type: 'compound', value: { dimension: { type: 'list', value: { type: 'compound', value: [w] } } } }), - dimension: (version.version >= 735 ? mcData.loginPacket.dimension : 0), - worldName: 'minecraft:overworld', - hashedSeed: [0, 0], - difficulty: 2, - maxPlayers: server.maxPlayers, - reducedDebugInfo: (version.version >= 735 ? false : 0), - enableRespawnScreen: true - } - if (version.version >= 735) { // 1.16x - loginPacket.isDebug = false - loginPacket.isFlat = false - loginPacket.isHardcore = false - loginPacket.viewDistance = 10 - delete loginPacket.levelType - delete loginPacket.difficulty - } - client.write('login', loginPacket) - }) - server.on('close', function () { - resolve() + client.write('login', loginPacket(client, server)) }) + server.on('close', resolve) server.on('listening', function () { const client = mc.createClient({ username: 'lalalal', @@ -448,6 +407,7 @@ for (const supportedVersion of mc.supportedVersions) { if (count <= 0) done() } }) + it('encodes chat packet once and send it to two clients', function (done) { const server = mc.createServer({ 'online-mode': false, @@ -455,51 +415,31 @@ for (const supportedVersion of mc.supportedVersions) { port: PORT }) server.on('login', function (client) { - const loginPacket = { - entityId: client.id, - levelType: 'default', - gameMode: 1, - previousGameMode: version.version >= 755 ? 0 : 255, - worldNames: ['minecraft:overworld'], - dimensionCodec: version.version >= 755 ? mcData.loginPacket.dimensionCodec : (version.version >= 735 ? mcData.loginPacket.dimension : { name: '', type: 'compound', value: { dimension: { type: 'list', value: { type: 'compound', value: [w] } } } }), - dimension: (version.version >= 735 ? mcData.loginPacket.dimension : 0), - worldName: 'minecraft:overworld', - hashedSeed: [0, 0], - difficulty: 2, - maxPlayers: server.maxPlayers, - reducedDebugInfo: (version.version >= 735 ? false : 0), - enableRespawnScreen: true - } - if (version.version >= 735) { // 1.16x - loginPacket.isDebug = false - loginPacket.isFlat = false - loginPacket.isHardcore = false - loginPacket.viewDistance = 10 - delete loginPacket.levelType - delete loginPacket.difficulty - } - client.write('login', loginPacket) + client.write('login', loginPacket(client, server)) }) server.on('close', done) server.on('listening', async function () { - const player1 = mc.createClient({ + const player1 = applyClientHelpers(mc.createClient({ username: 'player1', host: '127.0.0.1', version: version.minecraftVersion, port: PORT - }) - const player2 = mc.createClient({ + })) + const player2 = applyClientHelpers(mc.createClient({ username: 'player2', host: '127.0.0.1', version: version.minecraftVersion, port: PORT - }) - await Promise.all([once(player1, 'login'), once(player2, 'login')]) - server.writeToClients(Object.values(server.clients), 'chat', { message: JSON.stringify({ text: 'A message from the server.' }), position: 1, sender: '00000000-0000-0000-0000-000000000000' }) - - let results = await Promise.all([ once(player1, 'chat'), once(player2, 'chat') ]) - results.forEach(res => assert.strictEqual(res[0].message, '{"text":"A message from the server."}')) - + })) + await Promise.all([once(player1, 'login'), once(player2, 'login')]) + + sendBroadcastMessage(server, Object.values(server.clients), 'A message from the server.') + + let results = await Promise.all([player1.nextMessage(), player2.nextMessage()]) + for (const msg of results) { + assert.strictEqual(msg, '{"text":"A message from the server."}') + } + player1.end() player2.end() await Promise.all([once(player1, 'end'), once(player2, 'end')]) From 79afd277d8dd294a5e819453c9c3effb3aba715d Mon Sep 17 00:00:00 2001 From: IceTank <61137113+IceTank@users.noreply.github.com> Date: Tue, 16 Aug 2022 00:57:47 +0200 Subject: [PATCH 037/171] Fix unhandled promise rejection on ms auth (#1022) --- src/createClient.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/createClient.js b/src/createClient.js index bee9ae1cd..aeea6ac58 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -43,7 +43,7 @@ function createClient (options) { auth(client, options) break case 'microsoft': - microsoftAuth.authenticate(client, options) + microsoftAuth.authenticate(client, options).catch((err) => client.emit('error', err)) break case 'offline': default: From af31ece024d643408a6fae6e07a8f20a28b5b673 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Tue, 16 Aug 2022 01:00:53 +0200 Subject: [PATCH 038/171] Release 1.36.0 --- docs/HISTORY.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 2c1721e37..b440ca045 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,12 @@ # History +## 1.36.0 + +* Use offline mode as default authentication, fallback to offline mode if invalid option. (@Kashalls) +* Provide interface for using not at all supported alternative accounts. (@Kashalls) +* 1.19 support (@extremeheat) +* Fix unhandled promise rejection on ms auth (@IceTank) + ## 1.35.1 * add custom minecraft type varlong which aliases to varint @rob9315 diff --git a/package.json b/package.json index dc915b7c9..027c3dabb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.35.1", + "version": "1.36.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 6cbdbb85f8787b281491699cf514b752097d55df Mon Sep 17 00:00:00 2001 From: IceTank <61137113+IceTank@users.noreply.github.com> Date: Sun, 21 Aug 2022 23:04:58 +0200 Subject: [PATCH 039/171] Fix type definition breaking typescript projects (#1033) * Fix type definition breaking typescript projects * Fix createClient types --- src/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 2725b8114..b74b7c8a3 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -130,7 +130,7 @@ declare module 'minecraft-protocol' { session?: SessionOption validateChannelProtocol?: boolean, // 1.19+ - disableChatSigning: boolean + disableChatSigning?: boolean } export class Server extends EventEmitter { @@ -180,7 +180,7 @@ declare module 'minecraft-protocol' { validateChannelProtocol?: boolean // 1.19+ // Require connecting clients to have chat signing support enabled - enforceSecureProfile: boolean + enforceSecureProfile?: boolean } export interface SerializerOptions { From 1f4eb8655b779f23c27bac134d1deec7ac7d858b Mon Sep 17 00:00:00 2001 From: IceTank <61137113+IceTank@users.noreply.github.com> Date: Mon, 22 Aug 2022 18:47:07 +0200 Subject: [PATCH 040/171] Release 1.36.1 (#1036) --- docs/HISTORY.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index b440ca045..578632528 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,7 @@ # History +* Fix new types not being optional. (@IceTank) [#1033](https://github.com/PrismarineJS/node-minecraft-protocol/pull/1033) + ## 1.36.0 * Use offline mode as default authentication, fallback to offline mode if invalid option. (@Kashalls) diff --git a/package.json b/package.json index 027c3dabb..84f575926 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.36.0", + "version": "1.36.1", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 00949a02e653fccccfb8ae73238c3bec34743b2d Mon Sep 17 00:00:00 2001 From: Spencer Burgess <40410913+SpencerBurgess@users.noreply.github.com> Date: Fri, 30 Sep 2022 11:25:52 -0600 Subject: [PATCH 041/171] Update README.md (#1034) --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 3b9120509..a1c29c191 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2) + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. From cbf663a3eb0161e0fb53e0423b4a286ec4a8bbca Mon Sep 17 00:00:00 2001 From: IceTank <61137113+IceTank@users.noreply.github.com> Date: Fri, 30 Sep 2022 19:27:16 +0200 Subject: [PATCH 042/171] Fix connect event typo (#1035) --- 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 b74b7c8a3..c522d7e41 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -52,7 +52,7 @@ declare module 'minecraft-protocol' { once(event: 'sessionce', handler: (sessionce: any) => PromiseLike): this once(event: 'state', handler: (newState: States, oldState: States) => PromiseLike): this once(event: 'end', handler: (reasonce: string) => PromiseLike): this - once(event: 'concenect', handler: () => PromiseLike): this + once(event: 'connect', handler: () => PromiseLike): this once(event: string, handler: (data: any, packetMeta: PacketMeta) => PromiseLike): this once(event: `raw.${string}`, handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this } From 4ca8523b88b0280cafe04ea52c2f6139592bb308 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Fri, 11 Nov 2022 06:35:25 -0500 Subject: [PATCH 043/171] Update microsoftAuth to set default flow option (#1041) * Update microsoftAuth to set default flow option Explicitly specify to login through live.com * Update package.json --- package.json | 2 +- src/client/microsoftAuth.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 84f575926..511ff72c5 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", - "prismarine-auth": "^1.7.0", + "prismarine-auth": "^2.0.0", "prismarine-nbt": "^2.0.0", "protodef": "^1.8.0", "readable-stream": "^4.1.0", diff --git a/src/client/microsoftAuth.js b/src/client/microsoftAuth.js index 200e2428d..531b26f66 100644 --- a/src/client/microsoftAuth.js +++ b/src/client/microsoftAuth.js @@ -11,6 +11,7 @@ async function authenticate (client, options) { if (options.authTitle === undefined) { options.authTitle = Titles.MinecraftNintendoSwitch options.deviceType = 'Nintendo' + options.flow = 'live' } const Authflow = new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode) From 85264bcb95f7fe20172eb535269697ecb5c08b87 Mon Sep 17 00:00:00 2001 From: makin <9150636+makindotcc@users.noreply.github.com> Date: Fri, 11 Nov 2022 12:37:09 +0100 Subject: [PATCH 044/171] Make "fakeHost" option working (#1040) --- src/client/setProtocol.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/setProtocol.js b/src/client/setProtocol.js index fe835c782..41ad12368 100644 --- a/src/client/setProtocol.js +++ b/src/client/setProtocol.js @@ -15,7 +15,7 @@ module.exports = function (client, options) { function next () { let taggedHost = options.host if (client.tagHost) taggedHost += client.tagHost - if (client.fakeHost) taggedHost = options.fakeHost + if (options.fakeHost) taggedHost = options.fakeHost client.write('set_protocol', { protocolVersion: options.protocolVersion, From 061176d377c0d4055f02c0f5228f427586220872 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Fri, 11 Nov 2022 22:24:15 +0000 Subject: [PATCH 045/171] run standard --- examples/client_custom_auth/client_custom_auth.js | 4 ++-- examples/client_custom_packets/client_custom_packets.js | 2 +- examples/client_microsoft_auth/client_microsoft_auth.js | 2 +- examples/proxy/proxy.js | 8 ++++---- examples/server/server.js | 2 +- examples/server_ping/ping.js | 2 +- src/browser.js | 4 ++-- src/client.js | 4 ++-- src/client/pluginChannels.js | 2 +- src/index.js | 8 ++++---- src/transforms/encryption.js | 4 ++-- src/transforms/serializer.js | 4 ++-- 12 files changed, 23 insertions(+), 23 deletions(-) diff --git a/examples/client_custom_auth/client_custom_auth.js b/examples/client_custom_auth/client_custom_auth.js index 0544497ff..263da0938 100644 --- a/examples/client_custom_auth/client_custom_auth.js +++ b/examples/client_custom_auth/client_custom_auth.js @@ -11,8 +11,8 @@ if (!username || !password) { const client = mc.createClient({ host, port: parseInt(port), - username: username, - password: password, + username, + password, sessionServer: '', // URL to your session server proxy that changes the expected result of mojang's seession server to mcleaks expected. // For more information: https://github.com/PrismarineJS/node-yggdrasil/blob/master/src/Server.js#L19 auth: async (client, options) => { diff --git a/examples/client_custom_packets/client_custom_packets.js b/examples/client_custom_packets/client_custom_packets.js index 3c29d6516..998dd9959 100644 --- a/examples/client_custom_packets/client_custom_packets.js +++ b/examples/client_custom_packets/client_custom_packets.js @@ -62,7 +62,7 @@ const client = mc.createClient({ port: parseInt(process.argv[3]), username: process.argv[4] ? process.argv[4] : 'echo', password: process.argv[5], - customPackets: customPackets + customPackets }) client.on('connect', function () { diff --git a/examples/client_microsoft_auth/client_microsoft_auth.js b/examples/client_microsoft_auth/client_microsoft_auth.js index 01eebe898..a96d5915e 100644 --- a/examples/client_microsoft_auth/client_microsoft_auth.js +++ b/examples/client_microsoft_auth/client_microsoft_auth.js @@ -12,7 +12,7 @@ const client = mc.createClient({ host, port: parseInt(port), username: userOrEmail, // your microsoft account email - password: password, // your microsoft account password + password, // your microsoft account password auth: 'microsoft' // This option must be present and set to 'microsoft' to use Microsoft Account Authentication. Failure to do so will result in yggdrasil throwing invalid account information. }) diff --git a/examples/proxy/proxy.js b/examples/proxy/proxy.js index 499c4a5fa..c6835559e 100644 --- a/examples/proxy/proxy.js +++ b/examples/proxy/proxy.js @@ -73,7 +73,7 @@ const srv = mc.createServer({ 'online-mode': false, port: 25566, keepAlive: false, - version: version + version }) srv.on('login', function (client) { const addr = client.socket.remoteAddress @@ -92,11 +92,11 @@ srv.on('login', function (client) { if (!endedTargetClient) { targetClient.end('Error') } }) const targetClient = mc.createClient({ - host: host, - port: port, + host, + port, username: client.username, keepAlive: false, - version: version + version }) client.on('packet', function (data, meta) { if (targetClient.state === states.PLAY && meta.state === states.PLAY) { diff --git a/examples/server/server.js b/examples/server/server.js index 59a6bc864..4604c87a2 100644 --- a/examples/server/server.js +++ b/examples/server/server.js @@ -73,7 +73,7 @@ function broadcast (message, exclude, username) { client = server.clients[clientId] if (client !== exclude) { const msg = { - translate: translate, + translate, with: [ username, message diff --git a/examples/server_ping/ping.js b/examples/server_ping/ping.js index df68e89f0..d0973fcd1 100644 --- a/examples/server_ping/ping.js +++ b/examples/server_ping/ping.js @@ -20,7 +20,7 @@ if (!process.argv[2].includes(':')) { // Spliting ip and port if available. port = parseInt(port) } -protocol.ping({ host: host, port: port }, (err, pingResults) => { // Pinging server and getting result +protocol.ping({ host, port }, (err, pingResults) => { // Pinging server and getting result if (err) throw err console.log(`${removeColorsFromString(JSON.stringify(pingResults.description.text))}`) // Printing motd to console // Printing some infos to console diff --git a/src/browser.js b/src/browser.js index 4c01b24fe..d51f98bb1 100644 --- a/src/browser.js +++ b/src/browser.js @@ -5,8 +5,8 @@ const Server = require('./server') const serializer = require('./transforms/serializer') module.exports = { - Client: Client, - Server: Server, + Client, + Server, states: require('./states'), createSerializer: serializer.createSerializer, createDeserializer: serializer.createDeserializer, diff --git a/src/client.js b/src/client.js index 03e2c9bf1..4146d6bbf 100644 --- a/src/client.js +++ b/src/client.js @@ -39,11 +39,11 @@ class Client extends EventEmitter { } setSerializer (state) { - this.serializer = createSerializer({ isServer: this.isServer, version: this.version, state: state, customPackets: this.customPackets }) + this.serializer = createSerializer({ isServer: this.isServer, version: this.version, state, customPackets: this.customPackets }) this.deserializer = createDeserializer({ isServer: this.isServer, version: this.version, - state: state, + state, packetsToParse: this.packetsToParse, customPackets: this.customPackets, diff --git a/src/client/pluginChannels.js b/src/client/pluginChannels.js index 7988de356..fde8a9694 100644 --- a/src/client/pluginChannels.js +++ b/src/client/pluginChannels.js @@ -66,7 +66,7 @@ module.exports = function (client, options) { function writeChannel (channel, params) { debug('write custom payload ' + channel + ' ' + params) client.write('custom_payload', { - channel: channel, + channel, data: proto.createPacketBuffer(channel, params) }) } diff --git a/src/index.js b/src/index.js index 345341033..2e861a550 100644 --- a/src/index.js +++ b/src/index.js @@ -7,10 +7,10 @@ const createClient = require('./createClient') const createServer = require('./createServer') module.exports = { - createClient: createClient, - createServer: createServer, - Client: Client, - Server: Server, + createClient, + createServer, + Client, + Server, states: require('./states'), createSerializer: serializer.createSerializer, createDeserializer: serializer.createDeserializer, diff --git a/src/transforms/encryption.js b/src/transforms/encryption.js index 00a82013d..75383df80 100644 --- a/src/transforms/encryption.js +++ b/src/transforms/encryption.js @@ -49,6 +49,6 @@ class Decipher extends Transform { } module.exports = { - createCipher: createCipher, - createDecipher: createDecipher + createCipher, + createDecipher } diff --git a/src/transforms/serializer.js b/src/transforms/serializer.js index fb154baea..4985bb0a0 100644 --- a/src/transforms/serializer.js +++ b/src/transforms/serializer.js @@ -46,6 +46,6 @@ function createDeserializer ({ state = states.HANDSHAKING, isServer = false, ver } module.exports = { - createSerializer: createSerializer, - createDeserializer: createDeserializer + createSerializer, + createDeserializer } From 7fb293fba2527e4878df2d56ba296b4fa47e21bc Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sat, 19 Nov 2022 19:07:48 -0500 Subject: [PATCH 046/171] Throw error on minecraft-data protocol version mismatch (#1044) require('minecraft-data')('1.19.1') can redirect to require('minecraft-data')('1.19') (the major version) per node-minecraft-data constructor's version handling. This can cause issues if the protocol version changed between patch updates even on the same major, so throw error to prevent. --- src/transforms/serializer.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/transforms/serializer.js b/src/transforms/serializer.js index 4985bb0a0..22a6b7447 100644 --- a/src/transforms/serializer.js +++ b/src/transforms/serializer.js @@ -10,15 +10,20 @@ const states = require('../states') const merge = require('lodash.merge') const get = require('lodash.get') +const minecraftData = require('minecraft-data') const protocols = {} function createProtocol (state, direction, version, customPackets, compiled = true) { const key = state + ';' + direction + ';' + version + (compiled ? ';c' : '') if (protocols[key]) { return protocols[key] } - const mcData = require('minecraft-data')(version) + const mcData = minecraftData(version) + const versionInfo = minecraftData.versionsByMinecraftVersion.pc[version] if (mcData === null) { throw new Error(`No data available for version ${version}`) + } else if (versionInfo && versionInfo.version !== mcData.version.version) { + // The protocol version returned by node-minecraft-data constructor does not match the data in minecraft-data's protocolVersions.json + throw new Error(`Do not have protocol data for protocol version ${versionInfo.version} (attempted to use ${mcData.version.version} data)`) } if (compiled) { From 1efbde1ef7b581797a70a3cb1f1e0d0f7df5bfad Mon Sep 17 00:00:00 2001 From: extremeheat Date: Mon, 28 Nov 2022 20:46:11 -0500 Subject: [PATCH 047/171] Release 1.36.2 (#1045) * Update package.json * Update HISTORY.md * Update HISTORY.md Co-authored-by: Romain Beaumont --- docs/HISTORY.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 578632528..5bb24e003 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,12 @@ # History +## 1.36.2 +* Throw error on minecraft-data protocol version mismatch (#1044) +* Make "fakeHost" option working +* Update microsoftAuth to set default flow option + +## 1.36.1 + * Fix new types not being optional. (@IceTank) [#1033](https://github.com/PrismarineJS/node-minecraft-protocol/pull/1033) ## 1.36.0 diff --git a/package.json b/package.json index 511ff72c5..a4911854c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.36.1", + "version": "1.36.2", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 367c01567ce89771abc6fd9c643c9466b6b612e8 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sat, 14 Jan 2023 14:33:04 -0500 Subject: [PATCH 048/171] Initial 1.19.1/2 signed chat support (#1050) * Initial 1.19.1/2 signed chat impl * lint * remove node 15 nullish operators * fix undefined uuid error * handle player left * fix * add some feature flags * fix * Fix test to use new client.chat() wrapper method * refactoring * corrections, working client example * refactoring * message expiry checking * Fix UUID write serialization * Remove padding from client login to match vanilla client * Fix server verification * update packet field terminology * Add some tech docs Rename `map` field in Pending to not conflict with Array.map method * update tech doc * lint * Bump mcdata and pauth * add doc on playerChat event, .chat function * update doc * use supportFeature, update doc Co-authored-by: Romain Beaumont --- docs/API.md | 23 ++- docs/chat.md | 54 ++++++ examples/client_chat/client_chat.js | 44 +---- package.json | 4 +- src/client/chat.js | 287 ++++++++++++++++++++++++++++ src/client/microsoftAuth.js | 4 +- src/client/play.js | 42 ++-- src/client/setProtocol.js | 12 +- src/createServer.js | 1 + src/datatypes/compiler-minecraft.js | 2 +- src/datatypes/minecraft.js | 2 +- src/index.d.ts | 14 +- src/server/chat.js | 166 ++++++++++++++++ src/server/login.js | 39 ++-- src/version.js | 2 +- test/common/clientHelpers.js | 14 -- 16 files changed, 615 insertions(+), 95 deletions(-) create mode 100644 docs/chat.md create mode 100644 src/client/chat.js create mode 100644 src/server/chat.js diff --git a/docs/API.md b/docs/API.md index fa4361af8..375583b86 100644 --- a/docs/API.md +++ b/docs/API.md @@ -45,6 +45,10 @@ Write a packet to all `clients` but encode it only once. Verifies if player's chat message packet was signed with their Mojang provided key +### client.logSentMessageFromPeer(packet) +(1.19.1+) 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. For more information, see [chat.md](chat.md). + ### server.onlineModeExceptions This is a plain old JavaScript object. Add a key with the username you want to @@ -253,6 +257,18 @@ parameters. Called when an error occurs within the client. Takes an Error as parameter. +### `playerChat` event + +Called when a chat message from another player arrives. The emitted object contains: +* formattedMessage -- the chat message preformatted, if done on server side +* message -- the chat message without formatting (for example no ` message` ; instead `message`), on version 1.19+ +* 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) +* verified -- true if message is signed, false if not signed, undefined on versions prior to 1.19 + +See the [chat example](https://github.com/PrismarineJS/node-minecraft-protocol/blob/master/examples/client_chat/client_chat.js#L1) for usage. + ### per-packet events Check out the [minecraft-data docs](https://prismarinejs.github.io/minecraft-data/?v=1.8&d=protocol) to know the event names and data field names. @@ -273,13 +289,16 @@ Start emitting channel events of the given name on the client object. Unregister a channel `name` and send the unregister packet if `custom` is true. +### client.chat(message) +Send a chat message to the server, with signing on 1.19+. + ### client.signMessage(message: string, timestamp: BigInt, salt?: number) : Buffer -Generate a signature for a chat message to be sent to server +(1.19) Generate a signature for a chat message to be sent to server ### client.verifyMessage(publicKey: Buffer | KeyObject, packet) : boolean -Verifies a player chat packet sent by another player against their public key +(1.19) Verifies a player chat packet sent by another player against their public key ## Not Immediately Obvious Data Type Formats diff --git a/docs/chat.md b/docs/chat.md new file mode 100644 index 000000000..f8cff1f6c --- /dev/null +++ b/docs/chat.md @@ -0,0 +1,54 @@ +## About chat signing + +Starting in Minecraft 1.19, client messages sent to the server are signed and then broadcasted to other players. +Other clients receiving a signed message can verify that a message was written by a particular player as opposed +to being modified by the server. The way this is achieved is by the client asking Mojang's servers for signing keys, +and the server responding with a private key that can be used to sign messages, and a public key that can be used to +verify the messages. + +When a client connects to the server, it sends its public key to the server, which then sends that to other players +that are on the server. The server also does some checks during the login procedure to authenticate the validity of +the public key, to ensure it came from Mojang. This is achieved by the client sending along a signature from Mojang's +servers in the login step which is the output of concatenating and signing the public key, player UUID and timestamp +with a special Mojang private key specifically for signature validation. The public key used to verify this +signature is public and is stored statically inside node-minecraft-protocol (src/server/constants.js). + +Back to the client, when other players join the server they also get a copy of the players' public key for chat verification. +The clients can then verify that a message came from a client as well as do secondary checks like verifying timestamps. +This feature is designed to allow players to report chat messages from other players to Mojang. When the client reports a +message the contents, the sender UUID, timestamp, and signature are all sent so the Mojang server can verify the message +and send it for moderator review. + +Note: Since the server sends the public key, it's possible that the server can spoof the key and return a fake one, so +only Mojang can truly know if a message came from a client (as it stores its own copy of the clients' chat key pair). + +## 1.19.1 + +Starting with 1.19.1, instead of signing the message itself, a SHA256 hash of the message and last seen messages are +signed instead. In addition, the payload of the hash is prepended with the signature of the previous message sent by the same client, +creating a signed chain of chat messages. See publicly available documentation for more detailed information on this. + +Since chat verification happens on the client-side (as well as server side), all clients need to be kept up to date +on messages from other users. Since not all messages are public (for example, a player may send a signed private message), +the server can send a `chat_header` packet containing the aforementioned SHA256 hash of the message which the client +can generate a signature from, and store as the last signature for that player (maintaining chain integrity). + +In the client, inbound player chat history is now stored in chat logs (in a 1000 length array). This allows players +to search through last seen messages when reporting messages. + +When reporting chat messages, the chained chat functionality and chat history also securely lets Mojang get +authentic message context before and after a reported message. + +## Extra details + +### 1.19.1 + +When a server sends a player a message from another player, the server saves the outbound message and expects +that the client will acknowledge that message, either in a outbound `chat_message` packet's lastSeen field, +or in a `message_acknowledgement` packet. (If the client doesn't seen any chat_message's to the server and +lots of messages pending ACK queue up, a serverbound `message_acknowledgement` packet will be sent to flush the queue.) + +In the server, upon reviewal of the ACK, those messages removed from the servers' pending array. If too many +pending messages pile up, the client will get kicked. + +In nmp server, you must call `client.logSentMessageFromPeer(packet)` 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 (as described above). \ No newline at end of file diff --git a/examples/client_chat/client_chat.js b/examples/client_chat/client_chat.js index 8ba7c15a7..2bbfb79bc 100644 --- a/examples/client_chat/client_chat.js +++ b/examples/client_chat/client_chat.js @@ -3,7 +3,8 @@ const readline = require('readline') const rl = readline.createInterface({ input: process.stdin, output: process.stdout, - terminal: false + terminal: false, + prompt: 'Enter a message> ' }) const [,, host, port, username] = process.argv @@ -37,47 +38,14 @@ client.on('error', function (err) { }) client.on('connect', () => { - const mcData = require('minecraft-data')(client.version) const ChatMessage = require('prismarine-chat')(client.version) - const players = {} // 1.19+ console.log('Connected to server') + rl.prompt() - client.chat = (message) => { - if (mcData.supportFeature('signedChat')) { - const timestamp = BigInt(Date.now()) - client.write('chat_message', { - message, - timestamp, - salt: 0, - signature: client.signMessage(message, timestamp) - }) - } else { - client.write('chat', { message }) - } - } - - function onChat (packet) { - const message = packet.message || packet.unsignedChatContent || packet.signedChatContent - const j = JSON.parse(message) - const chat = new ChatMessage(j) - - if (packet.signature) { - const verified = client.verifyMessage(players[packet.senderUuid].publicKey, packet) - console.info(verified ? 'Verified: ' : 'UNVERIFIED: ', chat.toAnsi()) - } else { - console.info(chat.toAnsi()) - } - } - - client.on('chat', onChat) - client.on('player_chat', onChat) - client.on('player_info', (packet) => { - if (packet.action === 0) { // add player - for (const player of packet.data) { - players[player.UUID] = player.crypto - } - } + client.on('playerChat', function ({ senderName, message, formattedMessage, verified }) { + const chat = new ChatMessage(formattedMessage ? JSON.parse(formattedMessage) : message) + console.log(senderName, { true: 'Verified:', false: 'UNVERIFIED:' }[verified] || '', chat.toAnsi()) }) }) diff --git a/package.json b/package.json index a4911854c..29920d35a 100644 --- a/package.json +++ b/package.json @@ -51,11 +51,11 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^3.8.0", + "minecraft-data": "^3.21.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", - "prismarine-auth": "^2.0.0", + "prismarine-auth": "^2.2.0", "prismarine-nbt": "^2.0.0", "protodef": "^1.8.0", "readable-stream": "^4.1.0", diff --git a/src/client/chat.js b/src/client/chat.js new file mode 100644 index 000000000..51a798dc1 --- /dev/null +++ b/src/client/chat.js @@ -0,0 +1,287 @@ +const crypto = require('crypto') +const concat = require('../transforms/binaryStream').concat +const messageExpireTime = 420000 // 7 minutes (ms) + +module.exports = function (client, options) { + const mcData = require('minecraft-data')(client.version) + client._players = {} + client._lastChatSignature = null + client._lastRejectedMessage = null + + // This stores the last 5 messages that the player has seen, from unique players + client._lastSeenMessages = new LastSeenMessages() + // This stores last 1024 inbound messages for report lookup + client._lastChatHistory = new class extends Array { + capacity = 1024 + push (e) { + super.push(e) + if (this.length > this.capacity) { + this.shift() + } + } + }() + + function updateAndValidateChat (uuid, previousSignature, currentSignature, payload) { + // Get the player information + const player = client._players[uuid] + if (player && player.hasChainIntegrity) { + if (!player.lastSignature) { + // First time client is handling a chat message from this player, allow + player.lastSignature = currentSignature + } else if (player.lastSignature.equals(previousSignature)) { + player.lastSignature = currentSignature + } else { + // Not valid, client can no longer authenticate messages until player quits and reconnects + player.hasChainIntegrity = false + } + + if (player.hasChainIntegrity) { + const verifier = crypto.createVerify('RSA-SHA256') + if (previousSignature) verifier.update(previousSignature) + verifier.update(concat('UUID', uuid)) + verifier.update(payload) + player.hasChainIntegrity = verifier.verify(player.publicKey, currentSignature) + } + + return player.hasChainIntegrity + } + + return false + } + + client.on('player_info', (packet) => { + if (packet.action === 0) { // add player + for (const player of packet.data) { + if (player.crypto) { + client._players[player.UUID] = { + publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }), + publicKeyDER: player.crypto.publicKey, + signature: player.crypto.signature, + displayName: player.displayName || player.name + } + client._players[player.UUID].hasChainIntegrity = true + } + } + } else if (packet.action === 4) { // remove player + for (const player of packet.data) { + delete client._players[player.UUID] + } + } + }) + + client.on('message_header', (packet) => { + updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, packet.messageHash) + + client._lastChatHistory.push({ + previousSignature: packet.previousSignature, + signature: packet.signature, + messageHash: packet.messageHash + }) + }) + + client.on('player_chat', (packet) => { + if (!mcData.supportFeature('chainedChatWithHashing')) { // 1.19.0 + const pubKey = client._players[packet.senderUuid]?.publicKey + client.emit('playerChat', { + formattedMessage: packet.signedChatContent || packet.unsignedChatContent, + type: packet.type, + sender: packet.senderUuid, + senderName: packet.senderName, + senderTeam: packet.senderTeam, + verified: pubKey ? client.verifyMessage(pubKey, packet) : false + }) + return + } + + const hash = crypto.createHash('sha256') + hash.update(concat('i64', packet.salt, 'i64', packet.timestamp / 1000n, 'pstring', packet.plainMessage, 'i8', 70)) + if (packet.formattedMessage) hash.update(packet.formattedMessage) + for (const previousMessage of packet.previousMessages) { + hash.update(concat('i8', 70, 'UUID', previousMessage.messageSender)) + hash.update(previousMessage.messageSignature) + } + + // Chain integrity remains even if message is considered unverified due to expiry + const tsDelta = Date.now() - packet.timestamp + const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0 + const verified = updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, hash.digest()) && !expired + client.emit('playerChat', { + message: packet.plainMessage || packet.unsignedChatContent, + formattedMessage: packet.formattedMessage, + type: packet.type, + sender: packet.senderUuid, + senderName: client._players[packet.senderUuid]?.displayName, + senderTeam: packet.senderTeam, + verified + }) + + // We still accept a message (by pushing to seenMessages) even if the chain is broken. A vanilla client + // will reject a message if the client sets secure chat to be required and the message from the server + // isn't signed, or the client has blocked the sender. + // client1.19.1/client/net/minecraft/client/multiplayer/ClientPacketListener.java#L768 + client._lastSeenMessages.push({ sender: packet.senderUuid, signature: packet.signature }) + client._lastChatHistory.push({ + previousSignature: packet.previousSignature, + signature: packet.signature, + message: { + plain: packet.plainMessage, + decorated: packet.formattedMessage + }, + messageHash: packet.bodyDigest, + timestamp: packet.timestamp, + salt: packet.salt, + lastSeen: packet.previousMessages + }) + + if (client._lastSeenMessages.pending++ > 64) { + client.write('message_acknowledgement', { + previousMessages: client._lastSeenMessages.map((e) => ({ + messageSender: e.sender, + messageSignature: e.signature + })), + lastRejectedMessage: client._lastRejectedMessage + }) + client._lastSeenMessages.pending = 0 + } + }) + + // Chat Sending + let pendingChatRequest + let lastPreviewRequestId = 0 + + client._signedChat = (message, options = {}) => { + options.timestamp = options.timestamp || BigInt(Date.now()) + options.salt = options.salt || 0 + + if (options.skipPreview || !client.serverFeatures.chatPreview) { + client.write('chat_message', { + message, + timestamp: options.timestamp, + salt: options.salt, + signature: client.profileKeys ? client.signMessage(message, options.timestamp, options.salt) : Buffer.alloc(0), + signedPreview: options.didPreview, + previousMessages: client._lastSeenMessages.map((e) => ({ + messageSender: e.sender, + messageSignature: e.signature + })), + lastRejectedMessage: client._lastRejectedMessage + }) + client._lastSeenMessages.pending = 0 + } else { + client.write('chat_preview', { + query: lastPreviewRequestId, + message + }) + pendingChatRequest = { id: lastPreviewRequestId, message, options } + lastPreviewRequestId++ + } + } + + client.on('chat_preview', (packet) => { + if (pendingChatRequest && pendingChatRequest.id === packet.query) { + client._signedChat(packet.message, { ...pendingChatRequest.options, skipPreview: true, didPreview: true }) + pendingChatRequest = null + } + }) + + // Signing methods + client.signMessage = (message, timestamp, salt = 0) => { + if (!client.profileKeys) throw Error("Can't sign message without profile keys, please set valid auth mode") + + if (mcData.supportFeature('chainedChatWithHashing')) { + // 1.19.2 + const signer = crypto.createSign('RSA-SHA256') + if (client._lastChatSignature) signer.update(client._lastChatSignature) + signer.update(concat('UUID', client.uuid)) + + // Hash of chat body now opposed to signing plaintext. This lets server give us hashes for chat + // chain without needing to reveal message contents + if (message instanceof Buffer) { + signer.update(message) + } else { + const hash = crypto.createHash('sha256') + hash.update(concat('i64', salt, 'i64', timestamp / 1000n, 'pstring', message, 'i8', 70)) + for (const previousMessage of client._lastSeenMessages) { + hash.update(concat('i8', 70, 'UUID', previousMessage.sender)) + hash.update(previousMessage.signature) + } + // Feed hash back into signing payload + signer.update(hash.digest()) + } + + client._lastChatSignature = signer.sign(client.profileKeys.private) + } else { + // 1.19 + const signable = concat('i64', salt, 'UUID', client.uuid, 'i64', timestamp / 1000n, 'pstring', JSON.stringify({ text: message })) + client._lastChatSignature = crypto.sign('RSA-SHA256', signable, client.profileKeys.private) + } + + return client._lastChatSignature + } + + client.verifyMessage = (pubKey, packet) => { + if (!mcData.supportFeature('chainedChatWithHashing')) { // 1.19.0 + // Verification handled internally in 1.19.1+ as previous messages must be stored to verify future messages + throw new Error("Please listen to the 'playerChat' event instead to check message validity. client.verifyMessage is deprecated and only works on version 1.19.") + } + + if (pubKey instanceof Buffer) pubKey = crypto.createPublicKey({ key: pubKey, format: 'der', type: 'spki' }) + const signable = concat('i64', packet.salt, 'UUID', packet.senderUuid, 'i64', packet.timestamp / 1000n, 'pstring', packet.signedChatContent) + return crypto.verify('RSA-SHA256', signable, pubKey, packet.signature) + } + + // Report a chat message. + client.reportPlayer = (uuid, reason, reportedSignatures, comments) => { + const evidence = [] + + function addEvidence (entry) { + evidence.push({ + previousHeaderSignature: entry.previousSignature, + uuid: entry.senderUuid, + message: entry.message, + messageHash: entry.messageHash, + signature: entry.signature, + timestamp: entry.timestamp, + salt: entry.salt, + lastSeen: entry.lastSeen + }) + } + + for (let i = 0; i < client._lastChatHistory.capacity; i++) { + const entry = client._lastChatHistory[i] + for (const reportSig of reportedSignatures) { + if (reportSig.equals(entry.signature)) addEvidence(entry) + } + } + + return client.authflow.mca.reportPlayerChat({ + reason, + comments, + messages: evidence, + reportedPlayer: uuid, + createdTime: Date.now(), + clientVersion: client.version, + serverAddress: options.host + ':' + options.port, + realmInfo: undefined // { realmId, slotId } + }) + } +} + +class LastSeenMessages extends Array { + capacity = 5 + pending = 0 + push (e) { + // Insert a new entry at the top and shift everything to the right + let last = this[0] + this[0] = e + if (last && last.sender !== e.sender) { + for (let i = 1; i < this.capacity; i++) { + const current = this[i] + this[i] = last + last = current + // If we found an existing entry for the sender ID, we can stop shifting + if (!current || (current.sender === e.sender)) break + } + } + } +} diff --git a/src/client/microsoftAuth.js b/src/client/microsoftAuth.js index 531b26f66..e438fb284 100644 --- a/src/client/microsoftAuth.js +++ b/src/client/microsoftAuth.js @@ -14,8 +14,8 @@ async function authenticate (client, options) { options.flow = 'live' } - const Authflow = new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode) - const { token, entitlements, profile, certificates } = await Authflow.getMinecraftJavaToken({ fetchProfile: true, fetchCertificates: !options.disableChatSigning }).catch(e => { + client.authflow = new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode) + const { token, entitlements, profile, certificates } = await client.authflow.getMinecraftJavaToken({ fetchProfile: true, fetchCertificates: !options.disableChatSigning }).catch(e => { if (options.password) console.warn('Sign in failed, try removing the password field\n') if (e.toString().includes('Not Found')) console.warn(`Please verify that the account ${options.username} owns Minecraft\n`) throw e diff --git a/src/client/play.js b/src/client/play.js index 1274adff9..aab3f3999 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -1,25 +1,43 @@ const states = require('../states') -const crypto = require('crypto') -const concat = require('../transforms/binaryStream').concat +const signedChatPlugin = require('./chat') module.exports = function (client, options) { + client.serverFeatures = {} + client.on('server_data', (packet) => { + client.serverFeatures = { + chatPreview: packet.previewsChat, + enforcesSecureChat: packet.enforcesSecureChat + } + }) + client.once('success', onLogin) function onLogin (packet) { + const mcData = require('minecraft-data')(client.version) client.state = states.PLAY client.uuid = packet.uuid client.username = packet.username - client.signMessage = (message, timestamp, salt = 0) => { - if (!client.profileKeys) throw Error("Can't sign message without profile keys, please set valid auth mode") - const signable = concat('i64', salt, 'UUID', client.uuid, 'i64', - timestamp / 1000n, 'pstring', JSON.stringify({ text: message })) - return crypto.sign('RSA-SHA256', signable, client.profileKeys.private) + + if (mcData.supportFeature('signedChat')) { + if (!options.disableChatSigning && client.serverFeatures.enforcesSecureChat) { + throw new Error('"disableChatSigning" was enabled in client options, but server is enforcing secure chat') + } + signedChatPlugin(client, options) + } else { + client.on('chat', (packet) => { + client.emit(packet.position === 0 ? 'playerChat' : 'systemChat', { + formattedMessage: packet.message, + sender: packet.sender, + positionId: packet.position, + verified: false + }) + }) } - client.verifyMessage = (pubKey, packet) => { - if (pubKey instanceof Buffer) pubKey = crypto.createPublicKey({ key: pubKey, format: 'der', type: 'spki' }) - const signable = concat('i64', packet.salt, 'UUID', packet.senderUuid, - 'i64', packet.timestamp / 1000n, 'pstring', packet.signedChatContent) - return crypto.verify('RSA-SHA256', signable, pubKey, packet.signature) + + function unsignedChat (message) { + client.write('chat', { message }) } + + client.chat = client._signedChat || unsignedChat } } diff --git a/src/client/setProtocol.js b/src/client/setProtocol.js index 41ad12368..cb3ec8090 100644 --- a/src/client/setProtocol.js +++ b/src/client/setProtocol.js @@ -13,6 +13,7 @@ module.exports = function (client, options) { } function next () { + const mcData = require('minecraft-data')(client.version) let taggedHost = options.host if (client.tagHost) taggedHost += client.tagHost if (options.fakeHost) taggedHost = options.fakeHost @@ -24,15 +25,20 @@ module.exports = function (client, options) { nextState: 2 }) client.state = states.LOGIN + client.write('login_start', { username: client.username, signature: client.profileKeys ? { timestamp: BigInt(client.profileKeys.expiresOn.getTime()), // should probably be called "expireTime" - publicKey: client.profileKeys.publicDER, - signature: client.profileKeys.signature + // Remove padding on the public key: not needed in vanilla server but matches how vanilla client looks + publicKey: client.profileKeys.public.export({ type: 'spki', format: 'der' }), + signature: mcData.supportFeature('profileKeySignatureV2') + ? client.profileKeys.signatureV2 + : client.profileKeys.signature } - : null + : null, + playerUUID: client.session?.selectedProfile?.id }) } } diff --git a/src/createServer.js b/src/createServer.js index e1f5be543..a7e7372b1 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -43,6 +43,7 @@ function createServer (options = {}) { server.playerCount = 0 server.onlineModeExceptions = Object.create(null) server.favicon = favicon + server.options = options // The RSA keypair can take some time to generate // and is only needed for online-mode diff --git a/src/datatypes/compiler-minecraft.js b/src/datatypes/compiler-minecraft.js index 587530939..cac252609 100644 --- a/src/datatypes/compiler-minecraft.js +++ b/src/datatypes/compiler-minecraft.js @@ -47,7 +47,7 @@ module.exports = { Write: { varlong: ['native', minecraft.varlong[1]], UUID: ['native', (value, buffer, offset) => { - const buf = UUID.parse(value) + const buf = value.length === 32 ? Buffer.from(value, 'hex') : UUID.parse(value) buf.copy(buffer, offset) return offset + 16 }], diff --git a/src/datatypes/minecraft.js b/src/datatypes/minecraft.js index 6126674a9..eb9ef2498 100644 --- a/src/datatypes/minecraft.js +++ b/src/datatypes/minecraft.js @@ -38,7 +38,7 @@ function readUUID (buffer, offset) { } function writeUUID (value, buffer, offset) { - const buf = UUID.parse(value) + const buf = value.length === 32 ? Buffer.from(value, 'hex') : UUID.parse(value) buf.copy(buffer, offset) return offset + 16 } diff --git a/src/index.d.ts b/src/index.d.ts index c522d7e41..3319f3647 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -37,6 +37,7 @@ declare module 'minecraft-protocol' { writeChannel(channel: any, params: any): void signMessage(message: string, timestamp: BigInt, salt?: number): Buffer verifyMessage(publicKey: Buffer | KeyObject, packet: object): boolean + reportPlayer(uuid: string, reason: 'FALSE_REPORTING' | 'HATE_SPEECH' | 'TERRORISM_OR_VIOLENT_EXTREMISM' | 'CHILD_SEXUAL_EXPLOITATION_OR_ABUSE' | 'IMMINENT_HARM' | 'NON_CONSENSUAL_INTIMATE_IMAGERY' | 'HARASSMENT_OR_BULLYING' | 'DEFAMATION_IMPERSONATION_FALSE_INFORMATION' | 'SELF_HARM_OR_SUICIDE' | 'ALCOHOL_TOBACCO_DRUGS', signatures: Buffer[], comment?: string) on(event: 'error', listener: (error: Error) => PromiseLike): this on(event: 'packet', handler: (data: any, packetMeta: PacketMeta, buffer: Buffer, fullBuffer: Buffer) => PromiseLike): this on(event: 'raw', handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this @@ -46,6 +47,7 @@ declare module 'minecraft-protocol' { on(event: 'connect', handler: () => PromiseLike): this on(event: string, handler: (data: any, packetMeta: PacketMeta) => PromiseLike): this on(event: `raw.${string}`, handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this + on(event: 'playerChat', handler: ({ formattedMessage, message, type, sender, senderName, senderTeam, verified })) once(event: 'error', listener: (error: Error) => PromiseLike): this once(event: 'packet', handler: (data: any, packetMeta: PacketMeta, buffer: Buffer, fullBuffer: Buffer) => PromiseLike): this once(event: 'raw', handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this @@ -137,7 +139,7 @@ declare module 'minecraft-protocol' { constructor(version: string, customPackets?: any) writeToClients(clients: Client[], name: string, params: any): void onlineModeExceptions: object - clients: ClientsMap + clients: { [key: number]: ServerClient } playerCount: number maxPlayers: number motd: string @@ -156,6 +158,10 @@ 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. + logSentMessageFromPeer(packet: object): boolean } export interface ServerOptions { @@ -181,6 +187,8 @@ declare module 'minecraft-protocol' { // 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 + enableChatPreview?: boolean } export interface SerializerOptions { @@ -211,10 +219,6 @@ declare module 'minecraft-protocol' { state: States } - interface ClientsMap { - [key: number]: Client - } - export interface PingOptions { host?: string majorVersion?: string diff --git a/src/server/chat.js b/src/server/chat.js new file mode 100644 index 000000000..3785d77e0 --- /dev/null +++ b/src/server/chat.js @@ -0,0 +1,166 @@ +const crypto = require('crypto') +const concat = require('../transforms/binaryStream').concat +const debug = require('debug')('minecraft-protocol') +const messageExpireTime = 300000 // 5 min (ms) + +class VerificationError extends Error {} +function validateLastMessages (pending, lastSeen, lastRejected) { + if (lastRejected) { + const rejectedTs = pending.get(lastRejected.sender, lastRejected.signature) + if (!rejectedTs) { + throw new VerificationError(`Client rejected a message we never sent from '${lastRejected.sender}'`) + } else { + pending.acknowledge(lastRejected.sender, lastRejected.signature) + } + } + + let lastTimestamp + const seenSenders = new Set() + + for (const { messageSender, messageSignature } of lastSeen) { + if (pending.previouslyAcknowledged(messageSender, messageSignature)) continue + + const ts = pending.get(messageSender)(messageSignature) + if (!ts) { + throw new VerificationError(`Client saw a message that we never sent from '${messageSender}'`) + } else if (lastTimestamp && (ts < lastTimestamp)) { + throw new VerificationError(`Received messages out of order: Last acknowledged timestamp was at ${lastTimestamp}, now reading older message at ${ts}`) + } else if (seenSenders.has(messageSender)) { + // in the lastSeen array, last 5 messages from different players are stored, not just last 5 messages + throw new VerificationError(`Two last seen entries from same player not allowed: ${messageSender}`) + } else { + lastTimestamp = ts + seenSenders.add(messageSender) + pending.acknowledgePrior(messageSender, messageSignature) + } + } + + pending.setPreviouslyAcknowledged(lastSeen, lastRejected) +} + +module.exports = function (client, server, options) { + const raise = (translatableError) => client.end(translatableError, JSON.stringify({ translate: translatableError })) + const pending = new Pending() + + function validateMessageChain (packet) { + try { + validateLastMessages(pending, packet.previousMessages, packet.lastRejectedMessage) + } catch (e) { + if (e instanceof VerificationError) { + raise('multiplayer.disconnect.chat_validation_failed') + if (!options.hideErrors) console.error(client.address, 'disconnected because', e) + } else { + client.emit('error', e) + } + } + } + + // Listen to chat messages and verify the `lastSeen` and `lastRejected` messages chain + let lastTimestamp + client.on('chat_message', (packet) => { + if (!options.enforceSecureProfile) return // nothing signable + + if ((lastTimestamp && packet.timestamp < lastTimestamp) || (packet.timestamp > Date.now())) { + return raise('multiplayer.disconnect.out_of_order_chat') + } + lastTimestamp = packet.timestamp + + // Checks here: 1) make sure client can chat, 2) chain is OK, 3) signature is OK, 4) log if expired + if (client.settings.disabledChat) return raise('chat.disabled.options') + if (client.supportFeature('chainedChatWithHashing')) validateMessageChain(packet) // 1.19.1 + if (!client.verifyMessage(packet)) raise('multiplayer.disconnect.unsigned_chat') + if ((BigInt(Date.now()) - packet.timestamp) > messageExpireTime) debug(client.socket.address(), 'sent expired message TS', packet.timestamp) + }) + + // Client will occasionally send a list of seen messages to the server, here we listen & check chain validity + client.on('message_acknowledgement', validateMessageChain) + + client.verifyMessage = (packet) => { + if (!client.profileKeys) return null + if (client.supportFeature('chainedChatWithHashing')) { // 1.19.1+ + if (client._lastChatSignature === packet.signature) return true // Called twice + const verifier = crypto.createVerify('RSA-SHA256') + if (client._lastChatSignature) verifier.update(client._lastChatSignature) + verifier.update(concat('UUID', client.uuid)) + + // Hash of chat body now opposed to signing plaintext. This lets server give us hashes for chat + // chain without needing to reveal message contents + if (packet.bodyDigest) { + // Header + verifier.update(packet.bodyDigest) + } else { + // Player Chat + const hash = crypto.createHash('sha256') + hash.update(concat('i64', packet.salt, 'i64', packet.timestamp / 1000n, 'pstring', packet.message, 'i8', 70)) + for (const { messageSender, messageSignature } of packet.previousMessages) { + hash.update(concat('i8', 70, 'UUID', messageSender)) + hash.update(messageSignature) + } + // Feed hash back into signing payload + verifier.update(hash.digest()) + } + client._lastChatSignature = packet.signature + return verifier.verify(client.profileKeys.public, packet.signature) + } else { // 1.19 + const signable = concat('i64', packet.salt, 'UUID', client.uuid, 'i64', packet.timestamp, 'pstring', packet.message) + return crypto.verify('sha256WithRSAEncryption', signable, client.profileKeys.public, packet.signature) + } + } + + // On 1.19.1+, outbound messages from server (client->SERVER->players) are logged so we can verify + // the last seen message field in inbound chat packets + client.logSentMessageFromPeer = (chatPacket) => { + if (!options.enforceSecureProfile || !server.features.signedChat) return // nothing signable + + pending.add(chatPacket.senderUuid, chatPacket.signature, chatPacket.timestamp) + if (pending.length > 4096) { + raise('multiplayer.disconnect.too_many_pending_chats') + return false + } + return true + } +} + +class Pending extends Array { + m = {} + lastSeen = [] + + get (sender, signature) { + return this.m[sender]?.[signature] + } + + add (sender, signature, ts) { + this.m[sender] = this.m[sender] || {} + this.m[sender][signature] = ts + this.push([sender, signature]) + } + + acknowledge (sender, username) { + delete this.m[sender][username] + this.splice(this.findIndex(([a, b]) => a === sender && b === username), 1) + } + + acknowledgePrior (sender, signature) { + for (let i = 0; i < this.length; i++) { + const [a, b] = this[i] + delete this.m[a] + if (a === sender && b === signature) { + this.splice(0, i) + break + } + } + } + + // Once we've acknowledged that the client has saw the messages we sent, + // we delete it from our map & pending list. However, the client may keep it in + // their 5-length lastSeen list anyway. Once we verify/ack the client's lastSeen array, + // we need to store it in memory to allow those entries to be approved again without + // erroring about a message we never sent in the next serverbound message packet we get. + setPreviouslyAcknowledged (lastSeen, lastRejected = {}) { + this.lastSeen = lastSeen.map(e => Object.values(e)).push(Object.values(lastRejected)) + } + + previouslyAcknowledged (sender, signature) { + return this.lastSeen.some(([a, b]) => a === sender && b === signature) + } +} diff --git a/src/server/login.js b/src/server/login.js index 12c7d858d..bfa9d408d 100644 --- a/src/server/login.js +++ b/src/server/login.js @@ -3,8 +3,10 @@ const crypto = require('crypto') const pluginChannels = require('../client/pluginChannels') const states = require('../states') const yggdrasil = require('yggdrasil') +const chatPlugin = require('./chat') const { concat } = require('../transforms/binaryStream') const { mojangPublicKeyPem } = require('./constants') +const debug = require('debug')('minecraft-protocol') module.exports = function (client, server, options) { const mojangPubKey = crypto.createPublicKey(mojangPublicKeyPem) @@ -13,7 +15,10 @@ module.exports = function (client, server, options) { const { 'online-mode': onlineMode = true, kickTimeout = 30 * 1000, - errorHandler: clientErrorHandler = (client, err) => client.end(err) + errorHandler: clientErrorHandler = function (client, err) { + if (!options.hideErrors) console.debug('Disconnecting client because error', err) + client.end(err) + } } = options let serverId @@ -33,6 +38,7 @@ module.exports = function (client, server, options) { function onLogin (packet) { const mcData = require('minecraft-data')(client.version) + client.supportFeature = mcData.supportFeature client.username = packet.username const isException = !!server.onlineModeExceptions[client.username.toLowerCase()] @@ -47,21 +53,26 @@ module.exports = function (client, server, options) { if (packet.signature) { if (packet.signature.timestamp < BigInt(Date.now())) { + debug('Client sent expired tokens') raise('multiplayer.disconnect.invalid_public_key_signature') return // expired tokens, client needs to restart game } try { const publicKey = crypto.createPublicKey({ key: packet.signature.publicKey, format: 'der', type: 'spki' }) - const publicPEM = mcPubKeyToPem(packet.signature.publicKey) - const signable = packet.signature.timestamp + publicPEM // (expires at + publicKey) + const signable = mcData.supportFeature('chainedChatWithHashing') + ? concat('UUID', packet.playerUUID, 'i64', packet.signature.timestamp, 'buffer', publicKey.export({ type: 'spki', format: 'der' })) + : Buffer.from(packet.signature.timestamp + mcPubKeyToPem(packet.signature.publicKey), 'utf8') // (expires at + publicKey) - if (!crypto.verify('RSA-SHA1', Buffer.from(signable, 'utf8'), mojangPubKey, packet.signature.signature)) { + // This makes sure 'signable' when signed with the mojang private key equals signature in this packet + if (!crypto.verify('RSA-SHA1', signable, mojangPubKey, packet.signature.signature)) { + debug('Signature mismatch') raise('multiplayer.disconnect.invalid_public_key_signature') return } - client.profileKeys = { public: publicKey, publicPEM } + client.profileKeys = { public: publicKey } } catch (err) { + debug(err) raise('multiplayer.disconnect.invalid_public_key') return } @@ -186,6 +197,14 @@ module.exports = function (client, server, options) { }) // TODO: find out what properties are on 'success' packet client.state = states.PLAY + client.settings = {} + + if (client.supportFeature('chainedChatWithHashing')) { // 1.19.1+ + client.write('server_data', { + previewsChat: options.enableChatPreview, + enforceSecureProfile: options.enforceSecureProfile + }) + } clearTimeout(loginKickTimer) loginKickTimer = null @@ -195,15 +214,7 @@ module.exports = function (client, server, options) { server.playerCount -= 1 }) pluginChannels(client, options) - - if (client.profileKeys) { - client.verifyMessage = (packet) => { - const signable = concat('i64', packet.salt, 'UUID', client.uuid, 'i64', - packet.timestamp, 'pstring', packet.message) - - return crypto.verify('sha256WithRSAEncryption', signable, client.profileKeys.public, packet.crypto.signature) - } - } + if (client.supportFeature('signedChat')) chatPlugin(client, server, options) server.emit('login', client) } } diff --git a/src/version.js b/src/version.js index ce23f51ef..04fb38199 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.19', + defaultVersion: '1.19.2', supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19'] } diff --git a/test/common/clientHelpers.js b/test/common/clientHelpers.js index 7ebe0d386..8847d1739 100644 --- a/test/common/clientHelpers.js +++ b/test/common/clientHelpers.js @@ -2,20 +2,6 @@ module.exports = client => { const mcData = require('minecraft-data')(client.version) const hasSignedChat = mcData.supportFeature('signedChat') - client.chat = (message) => { - if (hasSignedChat) { - const timestamp = BigInt(Date.now()) - client.write('chat_message', { - message, - timestamp, - salt: 0, - signature: Buffer.alloc(0) - }) - } else { - client.write('chat', { message }) - } - } - client.nextMessage = (containing) => { return new Promise((resolve) => { function onChat (packet) { From ca654c1770cc475ec0c999d520c1d3e662e3cf7d Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sat, 14 Jan 2023 20:40:34 +0100 Subject: [PATCH 049/171] update version support in readme --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index a1c29c191..2017ad3ba 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. From 6aaef7bbe55dc0864aa584a54ddd58de6c41dd24 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sat, 14 Jan 2023 20:41:32 +0100 Subject: [PATCH 050/171] Release 1.37.0 (#1051) * Update HISTORY.md * Update package.json --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 5bb24e003..8bf20e984 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.37.0 +* 1.19.1/2 signed chat support + ## 1.36.2 * Throw error on minecraft-data protocol version mismatch (#1044) * Make "fakeHost" option working diff --git a/package.json b/package.json index 29920d35a..1849a7bf9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.36.2", + "version": "1.37.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 38eecb5aa19d0c145ba306bc4bc690bfdc537494 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Sat, 14 Jan 2023 22:12:39 +0100 Subject: [PATCH 051/171] Fixes type mismatch (#1052) --- src/client/chat.js | 2 +- src/client/play.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/chat.js b/src/client/chat.js index 51a798dc1..3840b32ad 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -102,7 +102,7 @@ module.exports = function (client, options) { } // Chain integrity remains even if message is considered unverified due to expiry - const tsDelta = Date.now() - packet.timestamp + const tsDelta = BigInt(Date.now()) - packet.timestamp const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0 const verified = updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, hash.digest()) && !expired client.emit('playerChat', { diff --git a/src/client/play.js b/src/client/play.js index aab3f3999..5981726c9 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -19,7 +19,7 @@ module.exports = function (client, options) { client.username = packet.username if (mcData.supportFeature('signedChat')) { - if (!options.disableChatSigning && client.serverFeatures.enforcesSecureChat) { + if (options.disableChatSigning && client.serverFeatures.enforcesSecureChat) { throw new Error('"disableChatSigning" was enabled in client options, but server is enforcing secure chat') } signedChatPlugin(client, options) From 2fc4b917d911d1b48b9cc4b57fcc8952d3cfc361 Mon Sep 17 00:00:00 2001 From: jojomatik Date: Sun, 15 Jan 2023 14:37:32 +0100 Subject: [PATCH 052/171] fix(types): complete type definitions (#1053) Complete the type definitions to prevent errors. --- src/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 3319f3647..24aa15b7d 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -37,7 +37,7 @@ declare module 'minecraft-protocol' { writeChannel(channel: any, params: any): void signMessage(message: string, timestamp: BigInt, salt?: number): Buffer verifyMessage(publicKey: Buffer | KeyObject, packet: object): boolean - reportPlayer(uuid: string, reason: 'FALSE_REPORTING' | 'HATE_SPEECH' | 'TERRORISM_OR_VIOLENT_EXTREMISM' | 'CHILD_SEXUAL_EXPLOITATION_OR_ABUSE' | 'IMMINENT_HARM' | 'NON_CONSENSUAL_INTIMATE_IMAGERY' | 'HARASSMENT_OR_BULLYING' | 'DEFAMATION_IMPERSONATION_FALSE_INFORMATION' | 'SELF_HARM_OR_SUICIDE' | 'ALCOHOL_TOBACCO_DRUGS', signatures: Buffer[], comment?: string) + reportPlayer(uuid: string, reason: 'FALSE_REPORTING' | 'HATE_SPEECH' | 'TERRORISM_OR_VIOLENT_EXTREMISM' | 'CHILD_SEXUAL_EXPLOITATION_OR_ABUSE' | 'IMMINENT_HARM' | 'NON_CONSENSUAL_INTIMATE_IMAGERY' | 'HARASSMENT_OR_BULLYING' | 'DEFAMATION_IMPERSONATION_FALSE_INFORMATION' | 'SELF_HARM_OR_SUICIDE' | 'ALCOHOL_TOBACCO_DRUGS', signatures: Buffer[], comment?: string): Promise on(event: 'error', listener: (error: Error) => PromiseLike): this on(event: 'packet', handler: (data: any, packetMeta: PacketMeta, buffer: Buffer, fullBuffer: Buffer) => PromiseLike): this on(event: 'raw', handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this @@ -47,7 +47,7 @@ declare module 'minecraft-protocol' { on(event: 'connect', handler: () => PromiseLike): this on(event: string, handler: (data: any, packetMeta: PacketMeta) => PromiseLike): this on(event: `raw.${string}`, handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this - on(event: 'playerChat', handler: ({ formattedMessage, message, type, sender, senderName, senderTeam, verified })) + on(event: 'playerChat', handler: ({ formattedMessage: string, message: string, type: string, sender: string, senderName: string, senderTeam: string, verified?: boolean })): this once(event: 'error', listener: (error: Error) => PromiseLike): this once(event: 'packet', handler: (data: any, packetMeta: PacketMeta, buffer: Buffer, fullBuffer: Buffer) => PromiseLike): this once(event: 'raw', handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this From 0526edf5f33f18e6991909b849668b63a817d504 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Sun, 15 Jan 2023 18:28:46 +0100 Subject: [PATCH 053/171] Update convenience chat events so higher-level clients can properly parse chat (#1055) * Emit systemChat event in 1.19+ * Pass both plain message and unsigned content * Rename property for clarity * Fix verifyMessage throwing in 1.19 * Update docs * Document systemChat event --- docs/API.md | 9 ++++++++- src/client/chat.js | 12 ++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/API.md b/docs/API.md index 375583b86..5719d5545 100644 --- a/docs/API.md +++ b/docs/API.md @@ -261,12 +261,19 @@ Called when an error occurs within the client. Takes an Error as parameter. Called when a chat message from another player arrives. The emitted object contains: * formattedMessage -- the chat message preformatted, if done on server side -* message -- the chat message without formatting (for example no ` message` ; instead `message`), on version 1.19+ +* plainMessage -- the chat message without formatting (for example no ` message` ; instead `message`), on version 1.19+ +* unsignedContent -- unsigned formatted chat contents ; should only be present when the message is modified and server has chat previews disabled - only on version 1.19 - 1.19.2 * 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) * verified -- true if message is signed, false if not signed, undefined on versions prior to 1.19 +### `systemChat` event + +Called when a system chat message arrives. A system chat message is any message not sent by a player. The emitted object contains: +* formattedMessage -- the chat message preformatted +* positionid -- the chat type of the message. 1 for system chat and 2 for actionbar + See the [chat example](https://github.com/PrismarineJS/node-minecraft-protocol/blob/master/examples/client_chat/client_chat.js#L1) for usage. ### per-packet events diff --git a/src/client/chat.js b/src/client/chat.js index 3840b32ad..306029f1b 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -69,6 +69,13 @@ module.exports = function (client, options) { } }) + client.on('system_chat', (packet) => { + client.emit('systemChat', { + positionid: packet.isActionBar ? 2 : 1, + formattedMessage: packet.content + }) + }) + client.on('message_header', (packet) => { updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, packet.messageHash) @@ -106,7 +113,8 @@ module.exports = function (client, options) { const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0 const verified = updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, hash.digest()) && !expired client.emit('playerChat', { - message: packet.plainMessage || packet.unsignedChatContent, + plainMessage: packet.plainMessage, + unsignedContent: packet.unsignedChatContent, formattedMessage: packet.formattedMessage, type: packet.type, sender: packet.senderUuid, @@ -220,7 +228,7 @@ module.exports = function (client, options) { } client.verifyMessage = (pubKey, packet) => { - if (!mcData.supportFeature('chainedChatWithHashing')) { // 1.19.0 + if (mcData.supportFeature('chainedChatWithHashing')) { // 1.19.1+ // Verification handled internally in 1.19.1+ as previous messages must be stored to verify future messages throw new Error("Please listen to the 'playerChat' event instead to check message validity. client.verifyMessage is deprecated and only works on version 1.19.") } From 28093fb1fb88280b182e7dd6a524191eda962f58 Mon Sep 17 00:00:00 2001 From: LucienHH <66429271+LucienHH@users.noreply.github.com> Date: Sun, 15 Jan 2023 17:30:33 +0000 Subject: [PATCH 054/171] Realm Joining (#1056) * Add docs * Implement Realm joining * Check for Microsoft auth * Remove on chat event * Add realmName option to example * Passthrough client to `realmAuthenticate` * Fix overwriting existing authflow * Don't use `??=` * Lint --- docs/API.md | 4 +++ docs/README.md | 14 ++++++++ examples/client_realms/client_realms.js | 25 ++++++++++++++ examples/client_realms/package.json | 8 +++++ package.json | 1 + src/client/microsoftAuth.js | 44 ++++++++++++++++++++++--- src/createClient.js | 9 +++-- src/index.d.ts | 7 ++++ 8 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 examples/client_realms/client_realms.js create mode 100644 examples/client_realms/package.json diff --git a/docs/API.md b/docs/API.md index 5719d5545..ac5c0e807 100644 --- a/docs/API.md +++ b/docs/API.md @@ -139,6 +139,10 @@ Returns a `Client` instance and perform login. * 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. + ## mc.Client(isServer,version,[customPackets]) diff --git a/docs/README.md b/docs/README.md index 2017ad3ba..25a542ec4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -95,6 +95,20 @@ client.on('chat', function(packet) { If the server is in offline mode, you may leave out the `password` option and switch auth to `offline`. You can also leave out `password` when using a Microsoft account. If provided, password based auth will be attempted first which may fail. *Note:* if using a Microsoft account, your account age must be >= 18 years old. +### Client example joining a Realm + +Example to connect to a Realm that the authenticating account is owner of or has been invited to: + +```js +var mc = require('minecraft-protocol'); +var client = mc.createClient({ + realms: { + pickRealm: (realms) => realms[0] // Function which recieves an array of joined/owned Realms and must return a single Realm. Can be async + }, + auth: 'microsoft' +}) +``` + ### Hello World server example ```js diff --git a/examples/client_realms/client_realms.js b/examples/client_realms/client_realms.js new file mode 100644 index 000000000..14985f8e9 --- /dev/null +++ b/examples/client_realms/client_realms.js @@ -0,0 +1,25 @@ +'use strict' + +const mc = require('minecraft-protocol') + +const [,, username, realmName] = process.argv +if (!realmName) { + console.log('Usage : node client_realms.js ') + process.exit(1) +} + +const client = mc.createClient({ + realms: { + // realmId: '1234567', // Connect the client to a Realm using the Realms ID + pickRealm: (realms) => realms.find(e => e.name === realmName) // Connect the client to a Realm using a function that returns a Realm + }, + username, + auth: 'microsoft' // This option must be present and set to 'microsoft' to join a Realm. +}) + +client.on('connect', function () { + console.info('connected') +}) +client.on('disconnect', function (packet) { + console.log('disconnected: ' + packet.reason) +}) diff --git a/examples/client_realms/package.json b/examples/client_realms/package.json new file mode 100644 index 000000000..56fcdf265 --- /dev/null +++ b/examples/client_realms/package.json @@ -0,0 +1,8 @@ +{ + "name": "node-minecraft-protocol-example", + "version": "0.0.0", + "private": true, + "dependencies": { + }, + "description": "A node-minecraft-protocol example" +} diff --git a/package.json b/package.json index 1849a7bf9..9f7f6c20d 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "node-rsa": "^0.4.2", "prismarine-auth": "^2.2.0", "prismarine-nbt": "^2.0.0", + "prismarine-realms": "^1.2.0", "protodef": "^1.8.0", "readable-stream": "^4.1.0", "uuid-1345": "^1.0.1", diff --git a/src/client/microsoftAuth.js b/src/client/microsoftAuth.js index e438fb284..8aea9890f 100644 --- a/src/client/microsoftAuth.js +++ b/src/client/microsoftAuth.js @@ -2,19 +2,23 @@ const path = require('path') const { Authflow: PrismarineAuth, Titles } = require('prismarine-auth') const minecraftFolderPath = require('minecraft-folder-path') const debug = require('debug')('minecraft-protocol') +const { RealmAPI } = require('prismarine-realms') -async function authenticate (client, options) { +function validateOptions (options) { if (!options.profilesFolder) { options.profilesFolder = path.join(minecraftFolderPath, 'nmp-cache') } - if (options.authTitle === undefined) { options.authTitle = Titles.MinecraftNintendoSwitch options.deviceType = 'Nintendo' options.flow = 'live' } +} - client.authflow = new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode) +async function authenticate (client, options) { + validateOptions(options) + + if (!client.authflow) client.authflow = new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode) const { token, entitlements, profile, certificates } = await client.authflow.getMinecraftJavaToken({ fetchProfile: true, fetchCertificates: !options.disableChatSigning }).catch(e => { if (options.password) console.warn('Sign in failed, try removing the password field\n') if (e.toString().includes('Not Found')) console.warn(`Please verify that the account ${options.username} owns Minecraft\n`) @@ -42,6 +46,38 @@ async function authenticate (client, options) { options.connect(client) } +async function realmAuthenticate (client, options) { + validateOptions(options) + + client.authflow = new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode) + + const api = RealmAPI.from(client.authflow, 'java') + const realms = await api.getRealms() + + debug('realms', realms) + + if (!realms || !realms.length) throw Error('Couldn\'t find any Realms for the authenticated account') + + let realm + + if (options.realms.realmId) { + realm = realms.find(e => e.id === Number(options.realms.realmId)) + } else if (options.realms.pickRealm) { + if (typeof options.realms.pickRealm !== 'function') throw Error('realms.pickRealm must be a function') + realm = await options.realms.pickRealm(realms) + } + + if (!realm) throw Error('Couldn\'t find a Realm to connect to. Authenticated account must be the owner or has been invited to the Realm.') + + const { host, port } = await realm.getAddress() + + debug('realms connection', { host, port }) + + options.host = host + options.port = port +} + module.exports = { - authenticate + authenticate, + realmAuthenticate } diff --git a/src/createClient.js b/src/createClient.js index aeea6ac58..1c4f0330a 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -20,7 +20,8 @@ module.exports = createClient function createClient (options) { assert.ok(options, 'options is required') assert.ok(options.username, 'username is required') - if (!options.version) { options.version = false } + if (!options.version && !options.realms) { options.version = false } + if (options.realms && options.auth !== 'microsoft') throw new Error('Currently Realms can only be joined with auth: "microsoft"') // TODO: avoid setting default version if autoVersion is enabled const optVersion = options.version || require('./version').defaultVersion @@ -43,7 +44,11 @@ function createClient (options) { auth(client, options) break case 'microsoft': - microsoftAuth.authenticate(client, options).catch((err) => client.emit('error', err)) + if (options.realms) { + microsoftAuth.realmAuthenticate(client, options).then(() => microsoftAuth.authenticate(client, options)).catch((err) => client.emit('error', err)) + } else { + microsoftAuth.authenticate(client, options).catch((err) => client.emit('error', err)) + } break case 'offline': default: diff --git a/src/index.d.ts b/src/index.d.ts index 24aa15b7d..0bfef3d1c 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -6,6 +6,7 @@ import * as Stream from 'stream' import { Agent } from 'http' import { Transform } from "readable-stream"; import { KeyObject } from 'crypto'; +import { Realm } from "prismarine-realms" type PromiseLike = Promise | void @@ -131,6 +132,7 @@ declare module 'minecraft-protocol' { id?: number session?: SessionOption validateChannelProtocol?: boolean, + realms?: RealmsOptions // 1.19+ disableChatSigning?: boolean } @@ -259,6 +261,11 @@ declare module 'minecraft-protocol' { latency: number } + export interface RealmsOptions { + realmId?: string + pickRealm?: (realms: Realm[]) => Realm + } + export const states: typeof States export const supportedVersions: string[] export const defaultVersion: string From 2a5ec378c4c3f398d0d7cb10e64831a561f8cd91 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Sun, 15 Jan 2023 18:39:48 +0100 Subject: [PATCH 055/171] Fix chat previews not working (#1054) * Fix chat previews not working * Update signMessage to account for chat previews * Fix lint * Fix signMessage function * Server side chat preview verification * Fix default value overriding provided value * Only sign previews when they are formatted * Sign undecorated messages when text is changed * Update docs --- docs/API.md | 1 + src/client/chat.js | 23 +++++++++++++++++++---- src/index.d.ts | 2 +- src/server/chat.js | 3 +++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/docs/API.md b/docs/API.md index ac5c0e807..06e609621 100644 --- a/docs/API.md +++ b/docs/API.md @@ -32,6 +32,7 @@ automatically logged in and validated against mojang's auth. * agent : a http agent that can be used to set proxy settings for yggdrasil authentication confirmation (see proxy-agent on npm) * validateChannelProtocol (optional) : whether or not to enable protocol validation for custom protocols using plugin channels for the connected clients. Defaults to true * 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) ## mc.Server(version,[customPackets]) diff --git a/src/client/chat.js b/src/client/chat.js index 306029f1b..b9a32abe2 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -2,6 +2,20 @@ const crypto = require('crypto') const concat = require('../transforms/binaryStream').concat const messageExpireTime = 420000 // 7 minutes (ms) +function isFormatted (message) { + // This should match the ChatComponent.isDecorated function from Vanilla + try { + const comp = JSON.parse(message) + for (const key in comp) { + if (key !== 'text') return true + } + if (comp.text && comp.text !== message) return true + return false + } catch { + return false + } +} + module.exports = function (client, options) { const mcData = require('minecraft-data')(client.version) client._players = {} @@ -166,7 +180,7 @@ module.exports = function (client, options) { message, timestamp: options.timestamp, salt: options.salt, - signature: client.profileKeys ? client.signMessage(message, options.timestamp, options.salt) : Buffer.alloc(0), + signature: client.profileKeys ? client.signMessage(message, options.timestamp, options.salt, options.preview) : Buffer.alloc(0), signedPreview: options.didPreview, previousMessages: client._lastSeenMessages.map((e) => ({ messageSender: e.sender, @@ -186,14 +200,14 @@ module.exports = function (client, options) { } client.on('chat_preview', (packet) => { - if (pendingChatRequest && pendingChatRequest.id === packet.query) { - client._signedChat(packet.message, { ...pendingChatRequest.options, skipPreview: true, didPreview: true }) + if (pendingChatRequest && pendingChatRequest.id === packet.queryId) { + client._signedChat(pendingChatRequest.message, { ...pendingChatRequest.options, skipPreview: true, didPreview: true, preview: isFormatted(packet.message) ? packet.message : undefined }) pendingChatRequest = null } }) // Signing methods - client.signMessage = (message, timestamp, salt = 0) => { + client.signMessage = (message, timestamp, salt = 0, preview) => { if (!client.profileKeys) throw Error("Can't sign message without profile keys, please set valid auth mode") if (mcData.supportFeature('chainedChatWithHashing')) { @@ -209,6 +223,7 @@ module.exports = function (client, options) { } else { const hash = crypto.createHash('sha256') hash.update(concat('i64', salt, 'i64', timestamp / 1000n, 'pstring', message, 'i8', 70)) + if (preview) hash.update(preview) for (const previousMessage of client._lastSeenMessages) { hash.update(concat('i8', 70, 'UUID', previousMessage.sender)) hash.update(previousMessage.signature) diff --git a/src/index.d.ts b/src/index.d.ts index 0bfef3d1c..bb5c5f467 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -36,7 +36,7 @@ declare module 'minecraft-protocol' { registerChannel(name: string, typeDefinition: any, custom?: boolean): void unregisterChannel(name: string): void writeChannel(channel: any, params: any): void - signMessage(message: string, timestamp: BigInt, salt?: number): Buffer + signMessage(message: string, timestamp: BigInt, salt?: number, preview?: string): Buffer verifyMessage(publicKey: Buffer | KeyObject, packet: object): boolean reportPlayer(uuid: string, reason: 'FALSE_REPORTING' | 'HATE_SPEECH' | 'TERRORISM_OR_VIOLENT_EXTREMISM' | 'CHILD_SEXUAL_EXPLOITATION_OR_ABUSE' | 'IMMINENT_HARM' | 'NON_CONSENSUAL_INTIMATE_IMAGERY' | 'HARASSMENT_OR_BULLYING' | 'DEFAMATION_IMPERSONATION_FALSE_INFORMATION' | 'SELF_HARM_OR_SUICIDE' | 'ALCOHOL_TOBACCO_DRUGS', signatures: Buffer[], comment?: string): Promise on(event: 'error', listener: (error: Error) => PromiseLike): this diff --git a/src/server/chat.js b/src/server/chat.js index 3785d77e0..4e52314bb 100644 --- a/src/server/chat.js +++ b/src/server/chat.js @@ -42,6 +42,8 @@ module.exports = function (client, server, options) { const raise = (translatableError) => client.end(translatableError, JSON.stringify({ translate: translatableError })) const pending = new Pending() + if (!options.generatePreview) options.generatePreview = message => message + function validateMessageChain (packet) { try { validateLastMessages(pending, packet.previousMessages, packet.lastRejectedMessage) @@ -92,6 +94,7 @@ module.exports = function (client, server, options) { // Player Chat const hash = crypto.createHash('sha256') hash.update(concat('i64', packet.salt, 'i64', packet.timestamp / 1000n, 'pstring', packet.message, 'i8', 70)) + if (packet.signedPreview) hash.update(options.generatePreview(packet.message)) for (const { messageSender, messageSignature } of packet.previousMessages) { hash.update(concat('i8', 70, 'UUID', messageSender)) hash.update(messageSignature) From 4f9341e6547801fae14ea390b8dd658b0bf7cd50 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sun, 15 Jan 2023 18:41:53 +0100 Subject: [PATCH 056/171] Release 1.38.0 (#1058) * Update package.json * Update HISTORY.md --- docs/HISTORY.md | 7 ++++++- package.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 8bf20e984..f8665e850 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,7 +1,12 @@ # History +## 1.38.0 +* Update convenience chat events (@frej4189) +* Realm Joining (@LucienHH ) +* Fix chat previews not working (@frej4189) + ## 1.37.0 -* 1.19.1/2 signed chat support +* 1.19.1/2 signed chat support (@frej4189 @extremeheat) ## 1.36.2 * Throw error on minecraft-data protocol version mismatch (#1044) diff --git a/package.json b/package.json index 9f7f6c20d..0f76236a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.37.0", + "version": "1.38.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From f2875a5f0350956b6af4afd4bfca36772709d6b7 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Mon, 16 Jan 2023 20:35:26 +0100 Subject: [PATCH 057/171] Update chat example for 1.19 (#1059) * Update example * Fix lint --- examples/client_chat/client_chat.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/examples/client_chat/client_chat.js b/examples/client_chat/client_chat.js index 2bbfb79bc..5dba86b00 100644 --- a/examples/client_chat/client_chat.js +++ b/examples/client_chat/client_chat.js @@ -43,9 +43,17 @@ client.on('connect', () => { console.log('Connected to server') rl.prompt() - client.on('playerChat', function ({ senderName, message, formattedMessage, verified }) { - const chat = new ChatMessage(formattedMessage ? JSON.parse(formattedMessage) : message) - console.log(senderName, { true: 'Verified:', false: 'UNVERIFIED:' }[verified] || '', chat.toAnsi()) + client.on('playerChat', function ({ senderName, plainMessage, unsignedContent, formattedMessage, verified }) { + let content + + const allowInsecureChat = true + + if (formattedMessage) content = JSON.parse(formattedMessage) + else if (allowInsecureChat && unsignedContent) content = JSON.parse(unsignedContent) + else content = { text: plainMessage } + + const chat = new ChatMessage(content) + console.log(senderName, { trugie: 'Verified:', false: 'UNVERIFIED:' }[verified] || '', chat.toAnsi()) }) }) From 4201b94ab8966e3c340a4deb454429b4bb5f80a2 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Mon, 16 Jan 2023 20:36:42 +0100 Subject: [PATCH 058/171] Fix chat not working on offline servers (#1060) * Do not acknowledge unsigned messages * Fix lint * Parse player data in offline mode * Fix lint --- src/client/chat.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/client/chat.js b/src/client/chat.js index b9a32abe2..76dddf632 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -71,11 +71,22 @@ module.exports = function (client, options) { publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }), publicKeyDER: player.crypto.publicKey, signature: player.crypto.signature, - displayName: player.displayName || player.name + displayName: player.displayName || player.name, + name: player.name } client._players[player.UUID].hasChainIntegrity = true + } else { + client._players[player.UUID] = { + displayName: player.displayName || player.name, + name: player.name + } } } + } else if (packet.action === 3) { + for (const player of packet.data) { + if (!client._players[player.UUID]) continue + client._players[player.UUID].displayName = player.displayName || client._players[player.UUID].name + } } else if (packet.action === 4) { // remove player for (const player of packet.data) { delete client._players[player.UUID] @@ -294,6 +305,8 @@ class LastSeenMessages extends Array { capacity = 5 pending = 0 push (e) { + if (e.signature.length === 0) return // We do not acknowledge unsigned messages + // Insert a new entry at the top and shift everything to the right let last = this[0] this[0] = e From 14b138cef852524e98ea17c8e4fb41bce6b06db8 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Tue, 17 Jan 2023 04:15:53 -0500 Subject: [PATCH 059/171] Release 1.38.1 (#1063) --- docs/HISTORY.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index f8665e850..158009c95 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,9 @@ # History +## 1.38.1 +* Update chat example for 1.19 (#1059) (@frej4189) +* Fix chat not working on offline servers (#1060) (@frej4189) + ## 1.38.0 * Update convenience chat events (@frej4189) * Realm Joining (@LucienHH ) diff --git a/package.json b/package.json index 0f76236a9..716adf301 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.38.0", + "version": "1.38.1", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 5e52e782ddeef3293588763a96e3b9b39d8da8f0 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Wed, 18 Jan 2023 21:10:22 +0100 Subject: [PATCH 060/171] Mark message as insecure if unsigned content is present (#1065) --- src/client/chat.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/chat.js b/src/client/chat.js index 76dddf632..f3f927b06 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -120,7 +120,7 @@ module.exports = function (client, options) { sender: packet.senderUuid, senderName: packet.senderName, senderTeam: packet.senderTeam, - verified: pubKey ? client.verifyMessage(pubKey, packet) : false + verified: (pubKey && !packet.unsignedChatContent) ? client.verifyMessage(pubKey, packet) : false }) return } @@ -136,7 +136,7 @@ module.exports = function (client, options) { // Chain integrity remains even if message is considered unverified due to expiry const tsDelta = BigInt(Date.now()) - packet.timestamp const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0 - const verified = updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, hash.digest()) && !expired + const verified = !packet.unsignedChatContent && updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, hash.digest()) && !expired client.emit('playerChat', { plainMessage: packet.plainMessage, unsignedContent: packet.unsignedChatContent, From 2164732844328a259927a57091c9bad27f507eb8 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Wed, 18 Jan 2023 21:11:15 +0100 Subject: [PATCH 061/171] Use non-zero salt (#1064) --- src/client/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/chat.js b/src/client/chat.js index f3f927b06..d46ad3d9c 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -184,7 +184,7 @@ module.exports = function (client, options) { client._signedChat = (message, options = {}) => { options.timestamp = options.timestamp || BigInt(Date.now()) - options.salt = options.salt || 0 + options.salt = options.salt || 1n if (options.skipPreview || !client.serverFeatures.chatPreview) { client.write('chat_message', { From c9e900d442a3085f414550e51c431eff3d7969fc Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Thu, 19 Jan 2023 01:28:25 +0100 Subject: [PATCH 062/171] Release 1.39.0 (#1066) * Update HISTORY.md * Update package.json --- docs/HISTORY.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 158009c95..1d7970faf 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,9 @@ # History +## 1.39.0 +* Use non-zero salt (@frej4189) +* Mark message as insecure if unsigned content is present (@frej4189) + ## 1.38.1 * Update chat example for 1.19 (#1059) (@frej4189) * Fix chat not working on offline servers (#1060) (@frej4189) diff --git a/package.json b/package.json index 716adf301..54b612bb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.38.1", + "version": "1.39.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 92a52199158bcac3fca0553992dc6081c4dd20d7 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Fri, 20 Jan 2023 22:09:35 +0100 Subject: [PATCH 063/171] Add more fields to playerChat event (#1068) * Add sender and target name to playerChat event * Update docs * Update docs * Remove unnecessary displayName parsing --- docs/API.md | 2 ++ src/client/chat.js | 17 +++-------------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/docs/API.md b/docs/API.md index 06e609621..229566e82 100644 --- a/docs/API.md +++ b/docs/API.md @@ -271,6 +271,8 @@ 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 +* 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 ### `systemChat` event diff --git a/src/client/chat.js b/src/client/chat.js index d46ad3d9c..95800b230 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -71,22 +71,11 @@ module.exports = function (client, options) { publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }), publicKeyDER: player.crypto.publicKey, signature: player.crypto.signature, - displayName: player.displayName || player.name, - name: player.name + displayName: player.displayName || player.name } client._players[player.UUID].hasChainIntegrity = true - } else { - client._players[player.UUID] = { - displayName: player.displayName || player.name, - name: player.name - } } } - } else if (packet.action === 3) { - for (const player of packet.data) { - if (!client._players[player.UUID]) continue - client._players[player.UUID].displayName = player.displayName || client._players[player.UUID].name - } } else if (packet.action === 4) { // remove player for (const player of packet.data) { delete client._players[player.UUID] @@ -143,8 +132,8 @@ module.exports = function (client, options) { formattedMessage: packet.formattedMessage, type: packet.type, sender: packet.senderUuid, - senderName: client._players[packet.senderUuid]?.displayName, - senderTeam: packet.senderTeam, + senderName: packet.networkName, + targetName: packet.networkTargetName, verified }) From cf1f67117d586b5e6e21f0d9602da12e9fcf46b6 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Sat, 21 Jan 2023 20:31:17 +0100 Subject: [PATCH 064/171] Update to 1.19.3 (#1069) * Add 1.19.3 player_info parsing * player_remove packet parsing * 1.19.3 chat parsing * Outgoing chat for 1.19.3 * Fix lint * Server chat validation * add 1.19.2 and 1.19.3 in version.js * Add 1.19.2 and 1.19.3 in ci.yml * Deprecated client.verifyMessage for server clients * Update docs * Deprecate client.verifyMessage for server clients * Fix tests * Fix lint * Fix packetTest * Fix test * Remove unneeded log statement * Update types/docs * Remove unnecessary feature check * Remove _session from docs Co-authored-by: Romain Beaumont --- .github/workflows/ci.yml | 2 +- docs/API.md | 4 +- src/client/chat.js | 309 ++++++++++++++++++++++++++++------- src/client/play.js | 15 ++ src/client/setProtocol.js | 3 +- src/index.d.ts | 2 +- src/server/chat.js | 108 +++++++++++- src/server/login.js | 2 +- src/version.js | 4 +- test/clientTest.js | 75 ++++++--- test/common/clientHelpers.js | 11 +- test/packetTest.js | 16 +- test/serverTest.js | 8 +- 13 files changed, 461 insertions(+), 98 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c4458740..4de44f807 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - mcVersion: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17', '1.17.1', '1.18.2', '1.19'] + mcVersion: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3'] fail-fast: false steps: diff --git a/docs/API.md b/docs/API.md index 229566e82..70108370a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -44,7 +44,7 @@ Write a packet to all `clients` but encode it only once. ### client.verifyMessage(packet) : boolean -Verifies if player's chat message packet was signed with their Mojang provided key +(1.19-1.19.2) Verifies if player's chat message packet was signed with their Mojang provided key. Handled internally (and thus deprecated) in 1.19.3 and above ### client.logSentMessageFromPeer(packet) (1.19.1+) You must call this function when the server receives a message from a player and that message gets @@ -306,7 +306,7 @@ Unregister a channel `name` and send the unregister packet if `custom` is true. ### client.chat(message) Send a chat message to the server, with signing on 1.19+. -### client.signMessage(message: string, timestamp: BigInt, salt?: number) : Buffer +### client.signMessage(message: string, timestamp: BigInt, salt?: number, preview?: string, acknowledgements?: Buffer[]) : Buffer (1.19) Generate a signature for a chat message to be sent to server diff --git a/src/client/chat.js b/src/client/chat.js index 95800b230..33323763d 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -22,8 +22,13 @@ module.exports = function (client, options) { client._lastChatSignature = null client._lastRejectedMessage = null - // This stores the last 5 messages that the player has seen, from unique players - client._lastSeenMessages = new LastSeenMessages() + // This stores the last n (5 or 20) messages that the player has seen, from unique players + if (mcData.supportFeature('chainedChatWithHashing')) client._lastSeenMessages = new LastSeenMessages() + else client._lastSeenMessages = new LastSeenMessagesWithInvalidation() + + // This stores the last 128 inbound (signed) messages for 1.19.3 chat validation + client._signatureCache = new SignatureCache() + // This stores last 1024 inbound messages for report lookup client._lastChatHistory = new class extends Array { capacity = 1024 @@ -35,6 +40,31 @@ module.exports = function (client, options) { } }() + function updateAndValidateSession (uuid, message, currentSignature, index, previousMessages, salt, timestamp) { + const player = client._players[uuid] + + if (player && player.hasChainIntegrity) { + if (!player.lastSignature || player.lastSignature.equals(currentSignature) || index > player.sessionIndex) { + player.lastSignature = currentSignature + } else { + player.hasChainIntegrity = false + } + + if (player.hasChainIntegrity) { + const length = Buffer.byteLength(message, 'utf8') + const acknowledgements = previousMessages.length > 0 ? ['i32', previousMessages.length, 'buffer', Buffer.concat(...previousMessages.map(msg => msg.signature || client._signatureCache[msg.id]))] : ['i32', 0] + + const signable = concat('i32', 1, 'UUID', uuid, 'UUID', player.sessionUuid, 'i32', index, 'i64', salt, 'i64', timestamp / 1000n, 'i32', length, 'pstring', message, ...acknowledgements) + + player.hasChainIntegrity = crypto.verify('RSA-SHA256', signable, player.publicKey, currentSignature) + } + + return player.hasChainIntegrity + } + + return false + } + function updateAndValidateChat (uuid, previousSignature, currentSignature, payload) { // Get the player information const player = client._players[uuid] @@ -63,7 +93,30 @@ module.exports = function (client, options) { return false } + client.on('player_remove', (packet) => { + for (const player of packet.players) { + delete client._players[player.UUID] + } + }) + client.on('player_info', (packet) => { + if (mcData.supportFeature('playerInfoActionIsBitfield')) { // 1.19.3+ + if (packet.action & 2) { // chat session + for (const player of packet.data) { + if (!player.chatSession) continue + client._players[player.UUID] = { + publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }), + publicKeyDER: player.chatSession.publicKey.keyBytes, + sessionUuid: player.chatSession.uuid + } + client._players[player.UUID].sessionIndex = true + client._players[player.UUID].hasChainIntegrity = true + } + } + + return + } + if (packet.action === 0) { // add player for (const player of packet.data) { if (player.crypto) { @@ -83,88 +136,163 @@ module.exports = function (client, options) { } }) + client.on('profileless_chat', (packet) => { + // Profileless chat is parsed as an unsigned player chat message but logged as a system message + + client.emit('playerChat', { + formattedMessage: packet.message, + type: packet.type, + senderName: packet.name, + targetName: packet.target, + verified: false + }) + + client._lastChatHistory.push({ + type: 2, // System message + message: { + decorated: packet.content // This should actually decorate the message with the sender and target name using the chat type + }, + timestamp: Date.now() + }) + }) + client.on('system_chat', (packet) => { client.emit('systemChat', { positionid: packet.isActionBar ? 2 : 1, formattedMessage: packet.content }) + + client._lastChatHistory.push({ + type: 2, // System message + message: { + decorated: packet.content + }, + timestamp: Date.now() + }) }) client.on('message_header', (packet) => { updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, packet.messageHash) client._lastChatHistory.push({ + type: 1, // Message header previousSignature: packet.previousSignature, signature: packet.signature, messageHash: packet.messageHash }) }) + client.on('hide_message', (packet) => { + if (mcData.supportFeature('useChatSessions')) { + const signature = packet.signature || client._signatureCache[packet.id] + if (signature) client._lastSeenMessages = client._lastSeenMessages.map(ack => (ack.signature === signature && ack.pending) ? null : ack) + } + }) + client.on('player_chat', (packet) => { - if (!mcData.supportFeature('chainedChatWithHashing')) { // 1.19.0 - const pubKey = client._players[packet.senderUuid]?.publicKey + if (mcData.supportFeature('useChatSessions')) { + const tsDelta = BigInt(Date.now()) - packet.timestamp + const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0 + const verified = !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired client.emit('playerChat', { - formattedMessage: packet.signedChatContent || packet.unsignedChatContent, + plainMessage: packet.plainMessage, + unsignedContent: packet.unsignedContent, type: packet.type, sender: packet.senderUuid, - senderName: packet.senderName, - senderTeam: packet.senderTeam, - verified: (pubKey && !packet.unsignedChatContent) ? client.verifyMessage(pubKey, packet) : false + senderName: packet.networkName, + targetName: packet.networkTargetName, + verified + }) + + client._lastChatHistory.push({ + type: 0, // Player message + signature: packet.signature, + message: { + plain: packet.plainMessage + }, + session: { + index: packet.index, + uuid: client._players[packet.senderUuid]?.sessionUuid + }, + timestamp: packet.timestamp, + salt: packet.salt, + lastSeen: packet.previousMessages.map(msg => msg.signature || client._signatureCache[msg.id]) }) + + if (client._lastSeenMessages.push(packet.signature) && client._lastSeenMessages.pending > 64) { + client.write('message_acknowledgement', { + count: client._lastSeenMessages.pending + }) + client._lastSeenMessages.pending = 0 + } return } - const hash = crypto.createHash('sha256') - hash.update(concat('i64', packet.salt, 'i64', packet.timestamp / 1000n, 'pstring', packet.plainMessage, 'i8', 70)) - if (packet.formattedMessage) hash.update(packet.formattedMessage) - for (const previousMessage of packet.previousMessages) { - hash.update(concat('i8', 70, 'UUID', previousMessage.messageSender)) - hash.update(previousMessage.messageSignature) + if (mcData.supportFeature('chainedChatWithHashing')) { + const hash = crypto.createHash('sha256') + hash.update(concat('i64', packet.salt, 'i64', packet.timestamp / 1000n, 'pstring', packet.plainMessage, 'i8', 70)) + if (packet.formattedMessage) hash.update(packet.formattedMessage) + for (const previousMessage of packet.previousMessages) { + hash.update(concat('i8', 70, 'UUID', previousMessage.messageSender)) + hash.update(previousMessage.messageSignature) + } + + // Chain integrity remains even if message is considered unverified due to expiry + const tsDelta = BigInt(Date.now()) - packet.timestamp + const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0 + const verified = !packet.unsignedChatContent && updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, hash.digest()) && !expired + client.emit('playerChat', { + plainMessage: packet.plainMessage, + unsignedContent: packet.unsignedChatContent, + formattedMessage: packet.formattedMessage, + type: packet.type, + sender: packet.senderUuid, + senderName: packet.networkName, + targetName: packet.networkTargetName, + verified + }) + + // We still accept a message (by pushing to seenMessages) even if the chain is broken. A vanilla client + // will reject a message if the client sets secure chat to be required and the message from the server + // isn't signed, or the client has blocked the sender. + // client1.19.1/client/net/minecraft/client/multiplayer/ClientPacketListener.java#L768 + client._lastChatHistory.push({ + type: 0, // Player message + previousSignature: packet.previousSignature, + signature: packet.signature, + message: { + plain: packet.plainMessage, + decorated: packet.formattedMessage + }, + messageHash: packet.bodyDigest, + timestamp: packet.timestamp, + salt: packet.salt, + lastSeen: packet.previousMessages + }) + + if (client._lastSeenMessages.push({ sender: packet.senderUuid, signature: packet.signature }) && client._lastSeenMessages.pending++ > 64) { + client.write('message_acknowledgement', { + previousMessages: client._lastSeenMessages.map((e) => ({ + messageSender: e.sender, + messageSignature: e.signature + })), + lastRejectedMessage: client._lastRejectedMessage + }) + client._lastSeenMessages.pending = 0 + } + + return } - // Chain integrity remains even if message is considered unverified due to expiry - const tsDelta = BigInt(Date.now()) - packet.timestamp - const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0 - const verified = !packet.unsignedChatContent && updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, hash.digest()) && !expired + const pubKey = client._players[packet.senderUuid]?.publicKey client.emit('playerChat', { - plainMessage: packet.plainMessage, - unsignedContent: packet.unsignedChatContent, - formattedMessage: packet.formattedMessage, + formattedMessage: packet.signedChatContent || packet.unsignedChatContent, type: packet.type, sender: packet.senderUuid, - senderName: packet.networkName, - targetName: packet.networkTargetName, - verified + senderName: packet.senderName, + senderTeam: packet.senderTeam, + verified: (pubKey && !packet.unsignedChatContent) ? client.verifyMessage(pubKey, packet) : false }) - - // We still accept a message (by pushing to seenMessages) even if the chain is broken. A vanilla client - // will reject a message if the client sets secure chat to be required and the message from the server - // isn't signed, or the client has blocked the sender. - // client1.19.1/client/net/minecraft/client/multiplayer/ClientPacketListener.java#L768 - client._lastSeenMessages.push({ sender: packet.senderUuid, signature: packet.signature }) - client._lastChatHistory.push({ - previousSignature: packet.previousSignature, - signature: packet.signature, - message: { - plain: packet.plainMessage, - decorated: packet.formattedMessage - }, - messageHash: packet.bodyDigest, - timestamp: packet.timestamp, - salt: packet.salt, - lastSeen: packet.previousMessages - }) - - if (client._lastSeenMessages.pending++ > 64) { - client.write('message_acknowledgement', { - previousMessages: client._lastSeenMessages.map((e) => ({ - messageSender: e.sender, - messageSignature: e.signature - })), - lastRejectedMessage: client._lastRejectedMessage - }) - client._lastSeenMessages.pending = 0 - } }) // Chat Sending @@ -175,6 +303,38 @@ module.exports = function (client, options) { options.timestamp = options.timestamp || BigInt(Date.now()) options.salt = options.salt || 1n + if (mcData.supportFeature('useChatSessions')) { + let acc = 0 + const acknowledgements = [] + + for (let i = 0; i < client._lastSeenMessages.capacity; i++) { + const idx = (client._lastSeenMessages.offset + i) % 20 + const message = client._lastSeenMessages[idx] + if (message) { + acc |= 1 << i + acknowledgements.push(message.signature) + message.pending = false + } + } + + const bitset = Buffer.allocUnsafe(3) + bitset[0] = acc & 0xFF + bitset[1] = (acc >> 8) & 0xFF + bitset[2] = (acc >> 16) & 0xFF + + client.write('chat_message', { + message, + timestamp: options.timestamp, + salt: options.salt, + signature: client.profileKeys ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined, + offset: client._lastSeenMessages.pending, + acknowledged: bitset + }) + client._lastSeenMessages.pending = 0 + + return + } + if (options.skipPreview || !client.serverFeatures.chatPreview) { client.write('chat_message', { message, @@ -207,10 +367,18 @@ module.exports = function (client, options) { }) // Signing methods - client.signMessage = (message, timestamp, salt = 0, preview) => { + client.signMessage = (message, timestamp, salt = 0, preview, acknowledgements) => { if (!client.profileKeys) throw Error("Can't sign message without profile keys, please set valid auth mode") - if (mcData.supportFeature('chainedChatWithHashing')) { + if (mcData.supportFeature('useChatSessions')) { + if (!client._session.uuid) throw Error("Chat session not initialized. Can't send chat") + + const length = Buffer.byteLength(message, 'utf8') + const previousMessages = acknowledgements.length > 0 ? ['i32', acknowledgements.length, 'buffer', Buffer.concat(acknowledgements)] : ['i32', 0] + + const signable = concat('i32', 1, 'UUID', client.uuid, 'UUID', client._session.uuid, 'i32', client._session.index++, 'i64', salt, 'i64', timestamp / 1000n, 'i32', length, 'pstring', message, ...previousMessages) + return crypto.sign('RSA-SHA256', signable, client.profileKeys.private) + } else if (mcData.supportFeature('chainedChatWithHashing')) { // 1.19.2 const signer = crypto.createSign('RSA-SHA256') if (client._lastChatSignature) signer.update(client._lastChatSignature) @@ -290,11 +458,39 @@ module.exports = function (client, options) { } } +class SignatureCache extends Array { + capacity = 128 + index = 0 + + push (e) { + if (!e) return + + this[this.index++] = e + this.index %= this.capacity + } +} + +class LastSeenMessagesWithInvalidation extends Array { + capacity = 20 + offset = 0 + pending = 0 + + push (e) { + if (!e) return false + + this[this.offset] = { pending: true, signature: e } + this.offset = (this.offset + 1) % this.capacity + this.pending++ + return true + } +} + class LastSeenMessages extends Array { capacity = 5 pending = 0 + push (e) { - if (e.signature.length === 0) return // We do not acknowledge unsigned messages + if (!e) return false // We do not acknowledge unsigned messages // Insert a new entry at the top and shift everything to the right let last = this[0] @@ -308,5 +504,6 @@ class LastSeenMessages extends Array { if (!current || (current.sender === e.sender)) break } } + return true } } diff --git a/src/client/play.js b/src/client/play.js index 5981726c9..dca101fa8 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -1,5 +1,6 @@ const states = require('../states') const signedChatPlugin = require('./chat') +const uuid = require('uuid-1345') module.exports = function (client, options) { client.serverFeatures = {} @@ -18,6 +19,20 @@ module.exports = function (client, options) { client.uuid = packet.uuid client.username = packet.username + if (mcData.supportFeature('useChatSessions') && client.profileKeys) { + client._session = { + index: 0, + uuid: uuid.v4fast() + } + + client.write('session', { + sessionUUID: client._session.uuid, + expireTime: client.profileKeys ? BigInt(client.profileKeys.expiresOn.getTime()) : undefined, + publicKey: client.profileKeys ? client.profileKeys.public.export({ type: 'spki', format: 'der' }) : undefined, + signature: client.profileKeys ? client.profileKeys.signatureV2 : undefined + }) + } + if (mcData.supportFeature('signedChat')) { if (options.disableChatSigning && client.serverFeatures.enforcesSecureChat) { throw new Error('"disableChatSigning" was enabled in client options, but server is enforcing secure chat') diff --git a/src/client/setProtocol.js b/src/client/setProtocol.js index cb3ec8090..e656744b4 100644 --- a/src/client/setProtocol.js +++ b/src/client/setProtocol.js @@ -25,10 +25,9 @@ module.exports = function (client, options) { nextState: 2 }) client.state = states.LOGIN - client.write('login_start', { username: client.username, - signature: client.profileKeys + signature: (client.profileKeys && !mcData.supportFeature('useChatSessions')) ? { timestamp: BigInt(client.profileKeys.expiresOn.getTime()), // should probably be called "expireTime" // Remove padding on the public key: not needed in vanilla server but matches how vanilla client looks diff --git a/src/index.d.ts b/src/index.d.ts index bb5c5f467..21a3c3a43 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -36,7 +36,7 @@ declare module 'minecraft-protocol' { registerChannel(name: string, typeDefinition: any, custom?: boolean): void unregisterChannel(name: string): void writeChannel(channel: any, params: any): void - signMessage(message: string, timestamp: BigInt, salt?: number, preview?: string): Buffer + signMessage(message: string, timestamp: BigInt, salt?: number, preview?: string, acknowledgements?: Buffer[]): Buffer verifyMessage(publicKey: Buffer | KeyObject, packet: object): boolean reportPlayer(uuid: string, reason: 'FALSE_REPORTING' | 'HATE_SPEECH' | 'TERRORISM_OR_VIOLENT_EXTREMISM' | 'CHILD_SEXUAL_EXPLOITATION_OR_ABUSE' | 'IMMINENT_HARM' | 'NON_CONSENSUAL_INTIMATE_IMAGERY' | 'HARASSMENT_OR_BULLYING' | 'DEFAMATION_IMPERSONATION_FALSE_INFORMATION' | 'SELF_HARM_OR_SUICIDE' | 'ALCOHOL_TOBACCO_DRUGS', signatures: Buffer[], comment?: string): Promise on(event: 'error', listener: (error: Error) => PromiseLike): this diff --git a/src/server/chat.js b/src/server/chat.js index 4e52314bb..bab658259 100644 --- a/src/server/chat.js +++ b/src/server/chat.js @@ -2,6 +2,7 @@ const crypto = require('crypto') const concat = require('../transforms/binaryStream').concat const debug = require('debug')('minecraft-protocol') const messageExpireTime = 300000 // 5 min (ms) +const { mojangPublicKeyPem } = require('./constants') class VerificationError extends Error {} function validateLastMessages (pending, lastSeen, lastRejected) { @@ -39,8 +40,9 @@ function validateLastMessages (pending, lastSeen, lastRejected) { } module.exports = function (client, server, options) { + const mojangPubKey = crypto.createPublicKey(mojangPublicKeyPem) const raise = (translatableError) => client.end(translatableError, JSON.stringify({ translate: translatableError })) - const pending = new Pending() + const pending = client.supportFeature('useChatSessions') ? new LastSeenMessages() : new Pending() if (!options.generatePreview) options.generatePreview = message => message @@ -57,6 +59,44 @@ module.exports = function (client, server, options) { } } + function validateSession (packet) { + try { + const unwrapped = pending.unwrap(packet.offset, packet.acknowledged) + + const length = Buffer.byteLength(packet.message, 'utf8') + const acknowledgements = unwrapped.length > 0 ? ['i32', unwrapped.length, 'buffer', Buffer.concat(...unwrapped)] : ['i32', 0] + + const signable = concat('i32', 1, 'UUID', client.uuid, 'UUID', client._session.uuid, 'i32', client._session.index++, 'i64', packet.salt, 'i64', packet.timestamp / 1000n, 'i32', length, 'pstring', packet.message, ...acknowledgements) + const valid = crypto.verify('RSA-SHA256', signable, client.profileKeys.public, packet.signature) + if (!valid) throw VerificationError('Invalid or missing message signature') + } catch (e) { + if (e instanceof VerificationError) { + raise('multiplayer.disconnect.chat_validation_failed') + if (!options.hideErrors) console.error(client.address, 'disconnected because', e) + } else { + client.emit('error', e) + } + } + } + + client.on('session', (packet) => { + client._session = { + index: 0, + uuid: packet.sessionUuid + } + + const publicKey = crypto.createPublicKey({ key: packet.publicKey, format: 'der', type: 'spki' }) + const signable = concat('UUID', client.uuid, 'i64', packet.expireTime, 'buffer', publicKey.export({ type: 'spki', format: 'der' })) + + // This makes sure 'signable' when signed with the mojang private key equals signature in this packet + if (!crypto.verify('RSA-SHA1', signable, mojangPubKey, packet.signature)) { + debug('Signature mismatch') + raise('multiplayer.disconnect.invalid_public_key_signature') + return + } + client.profileKeys = { public: publicKey } + }) + // Listen to chat messages and verify the `lastSeen` and `lastRejected` messages chain let lastTimestamp client.on('chat_message', (packet) => { @@ -67,19 +107,30 @@ module.exports = function (client, server, options) { } lastTimestamp = packet.timestamp - // Checks here: 1) make sure client can chat, 2) chain is OK, 3) signature is OK, 4) log if expired + // Checks here: 1) make sure client can chat, 2) chain/session is OK, 3) signature is OK, 4) log if expired if (client.settings.disabledChat) return raise('chat.disabled.options') if (client.supportFeature('chainedChatWithHashing')) validateMessageChain(packet) // 1.19.1 - if (!client.verifyMessage(packet)) raise('multiplayer.disconnect.unsigned_chat') + if (client.supportFeature('useChatSessions')) validateSession(packet) // 1.19.3 + else if (!client.verifyMessage(packet)) raise('multiplayer.disconnect.unsigned_chat') if ((BigInt(Date.now()) - packet.timestamp) > messageExpireTime) debug(client.socket.address(), 'sent expired message TS', packet.timestamp) }) // Client will occasionally send a list of seen messages to the server, here we listen & check chain validity - client.on('message_acknowledgement', validateMessageChain) + client.on('message_acknowledgement', (packet) => { + if (client.supportFeature('useChatSessions')) { + const valid = client._lastSeenMessages.applyOffset(packet.count) + if (!valid) { + raise('multiplayer.disconnect.chat_validation_failed') + if (!options.hideErrors) console.error(client.address, 'disconnected because', VerificationError('Failed to validate message acknowledgements')) + } + } else validateMessageChain(packet) + }) client.verifyMessage = (packet) => { if (!client.profileKeys) return null - if (client.supportFeature('chainedChatWithHashing')) { // 1.19.1+ + if (client.supportFeature('useChatSessions')) throw Error('client.verifyMessage is deprecated. Does not work for 1.19.3 and above') + + if (client.supportFeature('chainedChatWithHashing')) { // 1.19.1 if (client._lastChatSignature === packet.signature) return true // Called twice const verifier = crypto.createVerify('RSA-SHA256') if (client._lastChatSignature) verifier.update(client._lastChatSignature) @@ -124,6 +175,53 @@ module.exports = function (client, server, options) { } } +class LastSeenMessages extends Array { + tracking = 20 + + constructor () { + super() + for (let i = 0; i < this.tracking; i++) this.push(null) + } + + add (sender, signature) { + this.push({ signature, pending: true }) + } + + applyOffset (offset) { + const diff = this.length - this.tracking + if (offset >= 0 && offset <= diff) { + this.splice(0, offset) + return true + } + + return false + } + + unwrap (offset, acknowledged) { + if (!this.applyOffset(offset)) throw VerificationError('Failed to validate message acknowledgements') + + const n = (acknowledged[2] << 16) | (acknowledged[1] << 8) | acknowledged[0] + + const unwrapped = [] + for (let i = 0; i < this.tracking; i++) { + const ack = n & (1 << i) + const tracked = this[i] + if (ack) { + if (tracked === null) throw VerificationError('Failed to validate message acknowledgements') + + tracked.pending = false + unwrapped.push(tracked.signature) + } else { + if (tracked !== null && !tracked.pending) throw VerificationError('Failed to validate message acknowledgements') + + this[i] = null + } + } + + return unwrapped + } +} + class Pending extends Array { m = {} lastSeen = [] diff --git a/src/server/login.js b/src/server/login.js index bfa9d408d..df52a3630 100644 --- a/src/server/login.js +++ b/src/server/login.js @@ -60,7 +60,7 @@ module.exports = function (client, server, options) { try { const publicKey = crypto.createPublicKey({ key: packet.signature.publicKey, format: 'der', type: 'spki' }) - const signable = mcData.supportFeature('chainedChatWithHashing') + const signable = mcData.supportFeature('profileKeySignatureV2') ? concat('UUID', packet.playerUUID, 'i64', packet.signature.timestamp, 'buffer', publicKey.export({ type: 'spki', format: 'der' })) : Buffer.from(packet.signature.timestamp + mcPubKeyToPem(packet.signature.publicKey), 'utf8') // (expires at + publicKey) diff --git a/src/version.js b/src/version.js index 04fb38199..75cf507e9 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.19.2', - supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19'] + defaultVersion: '1.19.3', + supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3'] } diff --git a/test/clientTest.js b/test/clientTest.js index 01d0b2494..91796264c 100644 --- a/test/clientTest.js +++ b/test/clientTest.js @@ -105,7 +105,7 @@ for (const supportedVersion of mc.supportedVersions) { })) client.on('error', err => done(err)) const lineListener = function (line) { - const match = line.match(/\[Server thread\/INFO\]: <(.+?)> (.+)/) + const match = line.match(/\[Server thread\/INFO\]: (?:\[Not Secure\] )?<(.+?)> (.+)/) if (!match) return assert.strictEqual(match[1], 'Player') assert.strictEqual(match[2], 'hello everyone; I have logged in.') @@ -119,11 +119,62 @@ for (const supportedVersion of mc.supportedVersions) { client.chat('hello everyone; I have logged in.') }) - // 1.18 and below - client.on('chat', function (packet) { + client.on('playerChat', function (data) { + chatCount += 1 + assert.ok(chatCount <= 2) + + if (!mcData.supportFeature('clientsideChatFormatting')) { + const message = JSON.parse(data.formattedMessage) + if (chatCount === 1) { + assert.strictEqual(message.translate, 'chat.type.text') + assert.deepEqual(message.with[0].clickEvent, { + action: 'suggest_command', + value: mcData.version.version > 340 ? '/tell Player ' : '/msg Player ' + }) + assert.deepEqual(message.with[0].text, 'Player') + assert.strictEqual(message.with[1], 'hello everyone; I have logged in.') + } else if (chatCount === 2) { + assert.strictEqual(message.translate, 'chat.type.announcement') + assert.strictEqual(message.with[0].text ? message.with[0].text : message.with[0], 'Server') + assert.deepEqual(message.with[1].extra + ? (message.with[1].extra[0].text + ? message.with[1].extra[0].text + : message.with[1].extra[0]) + : message.with[1].text, 'hello') + wrap.removeListener('line', lineListener) + client.end() + done() + } + } else { + // 1.19+ + + const message = JSON.parse(data.formattedMessage || JSON.stringify({ text: data.plainMessage })) + + if (chatCount === 1) { + assert.strictEqual(message.text, 'hello everyone; I have logged in.') + const sender = JSON.parse(data.senderName) + assert.deepEqual(sender.clickEvent, { + action: 'suggest_command', + value: '/tell Player ' + }) + assert.strictEqual(sender.text, 'Player') + } else if (chatCount === 2) { + assert.strictEqual(message.text, 'hello') + const sender = JSON.parse(data.senderName) + assert.strictEqual(sender.text, 'Server') + wrap.removeListener('line', lineListener) + client.end() + done() + } + } + }) + + client.on('systemChat', function (data) { + // For 1.7.10 chatCount += 1 assert.ok(chatCount <= 2) - const message = JSON.parse(packet.message) + + const message = JSON.parse(data.formattedMessage) if (chatCount === 1) { assert.strictEqual(message.translate, 'chat.type.text') assert.deepEqual(message.with[0].clickEvent, { @@ -145,22 +196,6 @@ for (const supportedVersion of mc.supportedVersions) { done() } }) - - // 1.19 and above - let gotClientMessage, gotServerMessage - client.on('player_chat', (packet) => { - const message = JSON.parse(packet.unsignedChatContent || packet.signedChatContent) - // const sender = JSON.parse(packet.senderName) - - if (message.text === 'hello everyone; I have logged in.') gotClientMessage = true - if (message.text === 'hello') gotServerMessage = true - - if (gotClientMessage && gotServerMessage) { - wrap.removeListener('line', lineListener) - client.end() - done() - } - }) }) it('does not crash for ' + SURVIVE_TIME + 'ms', function (done) { diff --git a/test/common/clientHelpers.js b/test/common/clientHelpers.js index 8847d1739..01253780a 100644 --- a/test/common/clientHelpers.js +++ b/test/common/clientHelpers.js @@ -1,21 +1,20 @@ module.exports = client => { - const mcData = require('minecraft-data')(client.version) - const hasSignedChat = mcData.supportFeature('signedChat') - client.nextMessage = (containing) => { return new Promise((resolve) => { function onChat (packet) { - const m = packet.message || packet.unsignedChatContent || packet.signedChatContent + const m = packet.formattedMessage || packet.unsignedChatContent || JSON.stringify({ text: packet.plainMessage }) if (containing) { if (m.includes(containing)) return finish(m) else return } return finish(m) } - client.on(hasSignedChat ? 'player_chat' : 'chat', onChat) + client.on('playerChat', onChat) + client.on('systemChat', onChat) // For 1.7.10 function finish (m) { - client.off(hasSignedChat ? 'player_chat' : 'chat', onChat) + client.off('playerChat', onChat) + client.off('systemChat', onChat) resolve(m) } }) diff --git a/test/packetTest.js b/test/packetTest.js index 05682525d..ae4ac5ec1 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -44,7 +44,20 @@ const values = { i8: -10, u8: 8, string: 'hi hi this is my client string', - buffer: Buffer.alloc(8), + buffer: function (typeArgs, context) { + let count + if (typeof typeArgs.count === 'number') { + count = typeArgs.count + } else if (typeof typeArgs.count === 'object') { + count = evalCount(typeArgs.count, context) + } else if (typeArgs.count !== undefined) { + count = getField(typeArgs.count, context) + } else if (typeArgs.countType !== undefined) { + count = 8 + } + + return Buffer.alloc(count) + }, array: function (typeArgs, context) { let count if (typeof typeArgs.count === 'number') { @@ -114,6 +127,7 @@ const values = { test7: { type: 'intArray', value: [12, 42] } } }, + previousMessages: [], compressedNbt: { type: 'compound', name: 'test', diff --git a/test/serverTest.js b/test/serverTest.js index d1215620c..3683572d7 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -75,6 +75,7 @@ for (const supportedVersion of mc.supportedVersions) { function sendBroadcastMessage(server, clients, message, sender) { if (mcData.supportFeature('signedChat')) { server.writeToClients(clients, 'player_chat', { + plainMessage: message, signedChatContent: '', unsignedChatContent: JSON.stringify({ text: message }), type: 0, @@ -83,7 +84,10 @@ for (const supportedVersion of mc.supportedVersions) { senderTeam: undefined, timestamp: Date.now(), salt: 0n, - signature: Buffer.alloc(0) + signature: mcData.supportFeature('useChatSessions') ? undefined : Buffer.alloc(0), + previousMessages: [], + filterType: 0, + networkName: JSON.stringify({ text: sender }) }) } else { server.writeToClients(clients, 'chat', { message: JSON.stringify({ text: message }), position: 0, sender: sender || '0' }) @@ -318,6 +322,7 @@ for (const supportedVersion of mc.supportedVersions) { version: version.minecraftVersion, port: PORT })) + player1.on('login', async function (packet) { assert.strictEqual(packet.gameMode, 1) const player2 = applyClientHelpers(mc.createClient({ @@ -328,6 +333,7 @@ for (const supportedVersion of mc.supportedVersions) { })) const p1Join = await player1.nextMessage('player2') + assert.strictEqual(p1Join, '{"text":"player2 joined the game."}') player2.chat('hi') From 995797f587e605011d6ab972f64f7ff5134ba090 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sat, 21 Jan 2023 23:16:46 +0100 Subject: [PATCH 065/171] Release 1.40.0 (#1070) * Update HISTORY.md * Update package.json --- docs/HISTORY.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 1d7970faf..08238fb62 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,9 @@ # History +## 1.40.0 +* Add more fields to playerChat event (@frej4189) +* Update to 1.19.3 (@frej4189) + ## 1.39.0 * Use non-zero salt (@frej4189) * Mark message as insecure if unsigned content is present (@frej4189) diff --git a/package.json b/package.json index 54b612bb6..844bffaa9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.39.0", + "version": "1.40.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 046e3423e3dbc2ada6874a8875af3837d167a460 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sun, 22 Jan 2023 17:41:21 +0100 Subject: [PATCH 066/171] Add 1.19.3 in list of versions in readme --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 25a542ec4..a32126d21 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2) + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. From da85ee369eb3dc06ba7bcc13200423412ea474a5 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Sun, 22 Jan 2023 20:09:58 +0100 Subject: [PATCH 067/171] Fix offline mode (#1071) --- src/client/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/chat.js b/src/client/chat.js index 33323763d..f01e24835 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -490,7 +490,7 @@ class LastSeenMessages extends Array { pending = 0 push (e) { - if (!e) return false // We do not acknowledge unsigned messages + if (!e || !e.signature) return false // We do not acknowledge unsigned messages // Insert a new entry at the top and shift everything to the right let last = this[0] From b72cb89fc78c6ee5e553aa673122aff5b95dfeb8 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Sun, 22 Jan 2023 21:28:31 +0100 Subject: [PATCH 068/171] Release 1.40.1 (#1072) --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 08238fb62..9cdb8e916 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.40.1 +* Fix offline mode (@frej4189) + ## 1.40.0 * Add more fields to playerChat event (@frej4189) * Update to 1.19.3 (@frej4189) diff --git a/package.json b/package.json index 844bffaa9..c2dbc14ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.40.0", + "version": "1.40.1", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From d55c4984825c3256b652e71cd7efdc2a218bd937 Mon Sep 17 00:00:00 2001 From: Prosciuttino <123263403+Prosciuttino@users.noreply.github.com> Date: Mon, 23 Jan 2023 22:08:28 +0100 Subject: [PATCH 069/171] Small chat.js fix (#1074) --- src/client/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/chat.js b/src/client/chat.js index f01e24835..622f6cc23 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -490,7 +490,7 @@ class LastSeenMessages extends Array { pending = 0 push (e) { - if (!e || !e.signature) return false // We do not acknowledge unsigned messages + if (!e || !e.signature || e.signature.length === 0) return false // We do not acknowledge unsigned messages // Insert a new entry at the top and shift everything to the right let last = this[0] From 84bd97c3cb6efed87669b78134a784baf17a4be9 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Mon, 23 Jan 2023 22:11:50 +0100 Subject: [PATCH 070/171] Release 1.40.2 (#1075) * Update HISTORY.md * Update package.json --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 9cdb8e916..aa59eb806 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.40.2 +* Small chat.js fix (@frej4189) + ## 1.40.1 * Fix offline mode (@frej4189) diff --git a/package.json b/package.json index c2dbc14ad..23b52c8a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.40.1", + "version": "1.40.2", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From e3e20757ef8167abddea916cabf1db9cf8463ab3 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Fri, 27 Jan 2023 01:01:47 +0100 Subject: [PATCH 071/171] Use consistent parameter naming for systemChat event (#1076) * Use consistent parameter naming for systemChat event * Update docs * Update types --- docs/API.md | 2 +- src/client/chat.js | 2 +- src/index.d.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/API.md b/docs/API.md index 70108370a..0b2cb5e4e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -279,7 +279,7 @@ Called when a chat message from another player arrives. The emitted object conta Called when a system chat message arrives. A system chat message is any message not sent by a player. The emitted object contains: * formattedMessage -- the chat message preformatted -* positionid -- the chat type of the message. 1 for system chat and 2 for actionbar +* positionId -- the chat type of the message. 1 for system chat and 2 for actionbar See the [chat example](https://github.com/PrismarineJS/node-minecraft-protocol/blob/master/examples/client_chat/client_chat.js#L1) for usage. diff --git a/src/client/chat.js b/src/client/chat.js index 622f6cc23..69139578a 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -158,7 +158,7 @@ module.exports = function (client, options) { client.on('system_chat', (packet) => { client.emit('systemChat', { - positionid: packet.isActionBar ? 2 : 1, + positionId: packet.isActionBar ? 2 : 1, formattedMessage: packet.content }) diff --git a/src/index.d.ts b/src/index.d.ts index 21a3c3a43..c7607bcf0 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -48,7 +48,8 @@ declare module 'minecraft-protocol' { on(event: 'connect', handler: () => PromiseLike): this on(event: string, handler: (data: any, packetMeta: PacketMeta) => PromiseLike): this on(event: `raw.${string}`, handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this - on(event: 'playerChat', handler: ({ formattedMessage: string, message: string, type: string, sender: string, senderName: string, senderTeam: string, verified?: boolean })): this + on(event: 'playerChat', handler: (data: { formattedMessage: string, message: string, type: string, sender: string, senderName: string, senderTeam: string, verified?: boolean }) => PromiseLike): this + on(event: 'systemChat', handler: (data: { positionId: number, formattedMessage: string }) => PromiseLike): this once(event: 'error', listener: (error: Error) => PromiseLike): this once(event: 'packet', handler: (data: any, packetMeta: PacketMeta, buffer: Buffer, fullBuffer: Buffer) => PromiseLike): this once(event: 'raw', handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this From a8caf83ea7c50c6bb7504032f596dd4f4fe79f61 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Fri, 27 Jan 2023 23:19:57 +0100 Subject: [PATCH 072/171] Release 1.40.3 (#1077) * Update package.json * Update HISTORY.md --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index aa59eb806..db2280d44 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.40.3 +* Use consistent parameter naming for systemChat event + ## 1.40.2 * Small chat.js fix (@frej4189) diff --git a/package.json b/package.json index 23b52c8a9..76067d0c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.40.2", + "version": "1.40.3", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From c0877b003e26907a69956e4e0bc5458853aad476 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Wed, 8 Feb 2023 21:44:35 +0100 Subject: [PATCH 073/171] Catch errors in custom payloads (#1078) * Catch errors in custom payloads * Prevent uncatchable error when server sends invalid chat * Fix lint --- src/client/pluginChannels.js | 9 ++++++++- src/client/versionChecking.js | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/client/pluginChannels.js b/src/client/pluginChannels.js index fde8a9694..8ffe69145 100644 --- a/src/client/pluginChannels.js +++ b/src/client/pluginChannels.js @@ -51,7 +51,14 @@ module.exports = function (client, options) { return channel === packet.channel }) if (channel) { - if (proto.types[channel]) { packet.data = proto.parsePacketBuffer(channel, packet.data).data } + if (proto.types[channel]) { + try { + packet.data = proto.parsePacketBuffer(channel, packet.data).data + } catch (error) { + client.emit('error', error) + return + } + } debug('read custom payload ' + channel + ' ' + packet.data) client.emit(channel, packet.data) } diff --git a/src/client/versionChecking.js b/src/client/versionChecking.js index 25ccebb5d..9d27955ae 100644 --- a/src/client/versionChecking.js +++ b/src/client/versionChecking.js @@ -1,7 +1,13 @@ module.exports = function (client, options) { client.on('disconnect', message => { if (!message.reason) { return } - const parsed = JSON.parse(message.reason) + let parsed + try { + parsed = JSON.parse(message.reason) + } catch (error) { + client.emit('error', error) + return + } let text = parsed.text ? parsed.text : parsed let versionRequired From 45ea82a7dc0928e21eaa51b88697e942055b5548 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Sat, 18 Feb 2023 20:12:41 +0100 Subject: [PATCH 074/171] Fix client sending session packet when server is in offline mode (#1080) Co-authored-by: Frej Alexander Nielsen --- src/client/play.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/play.js b/src/client/play.js index dca101fa8..4504129fa 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -19,7 +19,7 @@ module.exports = function (client, options) { client.uuid = packet.uuid client.username = packet.username - if (mcData.supportFeature('useChatSessions') && client.profileKeys) { + if (mcData.supportFeature('useChatSessions') && client.profileKeys && client._cipher) { client._session = { index: 0, uuid: uuid.v4fast() From 90d414335872f6216a010dcca52be9aaff8007eb Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Sun, 19 Feb 2023 23:59:18 +0100 Subject: [PATCH 075/171] Fix client sending chat_session packet before server state transition (#1081) * Fix client sending session packet when server is in offline mode * Don't send chat session packet before server is in play state * Fix lint --------- Co-authored-by: Frej Alexander Nielsen --- src/client/play.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/client/play.js b/src/client/play.js index 4504129fa..5ca43d7b7 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -11,15 +11,10 @@ module.exports = function (client, options) { } }) - client.once('success', onLogin) + const mcData = require('minecraft-data')(client.version) - function onLogin (packet) { - const mcData = require('minecraft-data')(client.version) - client.state = states.PLAY - client.uuid = packet.uuid - client.username = packet.username - - if (mcData.supportFeature('useChatSessions') && client.profileKeys && client._cipher) { + client.once('login', () => { + if (mcData.supportFeature('useChatSessions') && client.profileKeys && client.cipher) { client._session = { index: 0, uuid: uuid.v4fast() @@ -32,6 +27,14 @@ module.exports = function (client, options) { signature: client.profileKeys ? client.profileKeys.signatureV2 : undefined }) } + }) + + client.once('success', onLogin) + + function onLogin (packet) { + client.state = states.PLAY + client.uuid = packet.uuid + client.username = packet.username if (mcData.supportFeature('signedChat')) { if (options.disableChatSigning && client.serverFeatures.enforcesSecureChat) { From 3974c503e4e08b271f028a87a91b1d9013990a35 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Mon, 20 Feb 2023 00:01:10 +0100 Subject: [PATCH 076/171] Release 1.41.0 (#1082) * Update HISTORY.md * Update package.json --- docs/HISTORY.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index db2280d44..062345a2b 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,11 @@ # History +## 1.41.0 + +* Catch errors in custom payloads (@frej4189) +* Fix client sending session packet when server is in offline mode (@frej4189) +* Fix client sending chat_session packet before server state transition (@frej4189) + ## 1.40.3 * Use consistent parameter naming for systemChat event diff --git a/package.json b/package.json index 76067d0c1..68848a490 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.40.3", + "version": "1.41.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 7a9cfdb3ec220cf774d127d926b86955a634b9ca Mon Sep 17 00:00:00 2001 From: extremeheat Date: Mon, 27 Feb 2023 15:21:35 -0500 Subject: [PATCH 077/171] Revert "Fix client sending chat_session packet before server state transition (#1081)" (#1084) This reverts commit 90d414335872f6216a010dcca52be9aaff8007eb. --- src/client/play.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/client/play.js b/src/client/play.js index 5ca43d7b7..4504129fa 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -11,10 +11,15 @@ module.exports = function (client, options) { } }) - const mcData = require('minecraft-data')(client.version) + client.once('success', onLogin) - client.once('login', () => { - if (mcData.supportFeature('useChatSessions') && client.profileKeys && client.cipher) { + function onLogin (packet) { + const mcData = require('minecraft-data')(client.version) + client.state = states.PLAY + client.uuid = packet.uuid + client.username = packet.username + + if (mcData.supportFeature('useChatSessions') && client.profileKeys && client._cipher) { client._session = { index: 0, uuid: uuid.v4fast() @@ -27,14 +32,6 @@ module.exports = function (client, options) { signature: client.profileKeys ? client.profileKeys.signatureV2 : undefined }) } - }) - - client.once('success', onLogin) - - function onLogin (packet) { - client.state = states.PLAY - client.uuid = packet.uuid - client.username = packet.username if (mcData.supportFeature('signedChat')) { if (options.disableChatSigning && client.serverFeatures.enforcesSecureChat) { From 49d218b08cc949ca193549f67f0e1a63b94e181a Mon Sep 17 00:00:00 2001 From: extremeheat Date: Mon, 27 Feb 2023 15:25:50 -0500 Subject: [PATCH 078/171] Release 1.41.1 (#1088) * Update package.json * Update HISTORY.md --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 062345a2b..a613dbcb3 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.41.1 +* Revert "Fix client sending chat_session packet before server state transition" + ## 1.41.0 * Catch errors in custom payloads (@frej4189) diff --git a/package.json b/package.json index 68848a490..2d99d31ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.41.0", + "version": "1.41.1", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 266a8659826a4ee70e7d145287bb395da10579a0 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Thu, 2 Mar 2023 19:24:55 +0100 Subject: [PATCH 079/171] Fix client sending session packet while server is in login state (#1089) * Fix client sending session packet while server is in login state * Fix lint --------- Co-authored-by: Frej Alexander Nielsen --- src/client/play.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/client/play.js b/src/client/play.js index 4504129fa..a75c29b7e 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -11,14 +11,8 @@ module.exports = function (client, options) { } }) - client.once('success', onLogin) - - function onLogin (packet) { + client.once('login', () => { const mcData = require('minecraft-data')(client.version) - client.state = states.PLAY - client.uuid = packet.uuid - client.username = packet.username - if (mcData.supportFeature('useChatSessions') && client.profileKeys && client._cipher) { client._session = { index: 0, @@ -32,6 +26,15 @@ module.exports = function (client, options) { signature: client.profileKeys ? client.profileKeys.signatureV2 : undefined }) } + }) + + client.once('success', onLogin) + + function onLogin (packet) { + const mcData = require('minecraft-data')(client.version) + client.state = states.PLAY + client.uuid = packet.uuid + client.username = packet.username if (mcData.supportFeature('signedChat')) { if (options.disableChatSigning && client.serverFeatures.enforcesSecureChat) { From 9c767a0b33f5fe7eb5e109d10d6a9b61881c9c36 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Mon, 6 Mar 2023 08:57:23 +0100 Subject: [PATCH 080/171] Fix client attempting to sign messages on offline servers (#1090) Co-authored-by: Frej Alexander Nielsen --- src/client/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/chat.js b/src/client/chat.js index 69139578a..913dc15f9 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -326,7 +326,7 @@ module.exports = function (client, options) { message, timestamp: options.timestamp, salt: options.salt, - signature: client.profileKeys ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined, + signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined, offset: client._lastSeenMessages.pending, acknowledged: bitset }) From f52ab55ef90e5f48e5748b9f6fa1d4cc0cd1da24 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Fri, 24 Mar 2023 23:28:19 +0100 Subject: [PATCH 081/171] Release 1.41.2 (#1091) * Update package.json * Update HISTORY.md --- docs/HISTORY.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index a613dbcb3..1082769a2 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,9 @@ # History +##1.41.2 +* Fix client sending session packet while server is in login state (@frej4189) +* Fix client attempting to sign messages on offline servers (@frej4189) + ## 1.41.1 * Revert "Fix client sending chat_session packet before server state transition" diff --git a/package.json b/package.json index 2d99d31ec..92fe9e9cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.41.1", + "version": "1.41.2", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 70aee3d49432147c0814b6e94b16cc80a64959a8 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Fri, 24 Mar 2023 23:29:03 +0100 Subject: [PATCH 082/171] 1.19.3 chat fixes (#1093) Co-authored-by: Frej Alexander Nielsen --- src/client/chat.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/chat.js b/src/client/chat.js index 913dc15f9..3d13b8de0 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -46,6 +46,7 @@ module.exports = function (client, options) { if (player && player.hasChainIntegrity) { if (!player.lastSignature || player.lastSignature.equals(currentSignature) || index > player.sessionIndex) { player.lastSignature = currentSignature + player.sessionIndex = index } else { player.hasChainIntegrity = false } @@ -194,6 +195,7 @@ module.exports = function (client, options) { const tsDelta = BigInt(Date.now()) - packet.timestamp const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0 const verified = !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired + if (verified) client._signatureCache.push(packet.signature) client.emit('playerChat', { plainMessage: packet.plainMessage, unsignedContent: packet.unsignedContent, From 6f57eb90d5072ad3a94a8734cdc4a5b6a8dd7639 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Wed, 5 Apr 2023 21:40:34 +0200 Subject: [PATCH 083/171] Delete CNAME --- docs/CNAME | 1 - 1 file changed, 1 deletion(-) delete mode 100644 docs/CNAME diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index cb9ce8606..000000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -node-minecraft-protocol.prismarine.js.org From 0625b29d526080e17d4031cd77c09b90350ae710 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 May 2023 13:40:24 +0200 Subject: [PATCH 084/171] Bump @types/node from 18.16.13 to 20.2.1 (#1203) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.16.13 to 20.2.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 92fe9e9cf..7f0d7a5da 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ }, "browser": "src/browser.js", "devDependencies": { - "@types/node": "^18.0.6", + "@types/node": "^20.2.1", "espower-loader": "^1.0.0", "intelli-espower-loader": "^1.0.0", "minecraft-packets": "^1.1.5", From 0134f1599f87caf94cd51b1379b4cb7e96273d72 Mon Sep 17 00:00:00 2001 From: Artur Khusainov Date: Sat, 20 May 2023 15:44:03 +0300 Subject: [PATCH 085/171] Fix plugin channels support (#1096) * fix custom channel registation * update channel examples * use actual protocol version * update custom channel examples * use default host & port * select channel name based on the feature set --- examples/client_channel/client_channel.js | 17 +++++++++++++---- .../client_custom_channel.js | 10 +++++----- examples/server_channel/server_channel.js | 10 ++++------ .../server_custom_channel.js | 12 +++++------- src/client/pluginChannels.js | 8 ++++---- 5 files changed, 31 insertions(+), 26 deletions(-) diff --git a/examples/client_channel/client_channel.js b/examples/client_channel/client_channel.js index 5d257e149..b5500b288 100644 --- a/examples/client_channel/client_channel.js +++ b/examples/client_channel/client_channel.js @@ -5,6 +5,14 @@ if (process.argv.length < 4 || process.argv.length > 6) { process.exit(1) } +function getBrandChannelName () { + const mcData = require('minecraft-data')(client.version) + if (mcData.supportFeature('customChannelIdentifier')) { + return 'minecraft:brand' // 1.13+ + } + return 'MC|Brand' +} + const client = mc.createClient({ version: false, host: process.argv[2], @@ -13,10 +21,11 @@ const client = mc.createClient({ password: process.argv[5] }) -client.registerChannel('MC|Brand', ['string', []]) -client.on('MC|Brand', console.log) +client.on('error', console.log) client.on('login', function () { - client.writeChannel('MC|Brand', 'vanilla') + const brandChannel = getBrandChannelName() + client.registerChannel(brandChannel, ['string', []]) + client.on(brandChannel, console.log) + client.writeChannel(brandChannel, 'vanilla') }) -client.on('error', console.log) diff --git a/examples/client_custom_channel/client_custom_channel.js b/examples/client_custom_channel/client_custom_channel.js index 6c130dcf0..b12f534fc 100644 --- a/examples/client_custom_channel/client_custom_channel.js +++ b/examples/client_custom_channel/client_custom_channel.js @@ -10,15 +10,15 @@ const client = mc.createClient({ port: parseInt(process.argv[3]), username: process.argv[4] ? process.argv[4] : 'test', password: process.argv[5], - version: '1.10' + version: false }) client.on('login', onlogin) client.on('error', console.log) function onlogin () { - client.registerChannel('CUSTOM|ChannelOne', ['i32', []], true) - client.registerChannel('CUSTOM|ChannelTwo', ['i32', []], true) - client.writeChannel('CUSTOM|ChannelOne', 4) - client.on('CUSTOM|ChannelTwo', console.log) + client.registerChannel('node-minecraft-protocol:custom_channel_one', ['string', []], true) + client.registerChannel('node-minecraft-protocol:custom_channel_two', ['string', []], true) + client.writeChannel('node-minecraft-protocol:custom_channel_one', 'hello from the client') + client.on('node-minecraft-protocol:custom_channel_two', console.log) } diff --git a/examples/server_channel/server_channel.js b/examples/server_channel/server_channel.js index d39eea5f8..28e842939 100644 --- a/examples/server_channel/server_channel.js +++ b/examples/server_channel/server_channel.js @@ -3,16 +3,14 @@ const mc = require('minecraft-protocol') const server = mc.createServer({ 'online-mode': false, // optional encryption: false, // optional - host: '0.0.0.0', // optional - port: 25565, // optional - version: '1.16' + version: '1.18.2' }) const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket server.on('login', function (client) { - client.registerChannel('MC|Brand', ['string', []]) - client.on('MC|Brand', console.log) + client.registerChannel('minecraft:brand', ['string', []]) + client.on('minecraft:brand', console.log) client.write('login', { entityId: client.id, @@ -39,5 +37,5 @@ server.on('login', function (client) { pitch: 0, flags: 0x00 }) - client.writeChannel('MC|Brand', 'vanilla') + client.writeChannel('minecraft:brand', 'vanilla') }) diff --git a/examples/server_custom_channel/server_custom_channel.js b/examples/server_custom_channel/server_custom_channel.js index f7f00b984..040fe80c0 100644 --- a/examples/server_custom_channel/server_custom_channel.js +++ b/examples/server_custom_channel/server_custom_channel.js @@ -3,9 +3,7 @@ const mc = require('minecraft-protocol') const server = mc.createServer({ 'online-mode': false, // optional encryption: false, // optional - host: '0.0.0.0', // optional - port: 25565, // optional - version: '1.16' + version: '1.18.2' }) const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket @@ -28,8 +26,8 @@ server.on('login', function (client) { isDebug: false, isFlat: false }) - client.registerChannel('CUSTOM|ChannelOne', ['i32', []], true) - client.registerChannel('CUSTOM|ChannelTwo', ['i32', []], true) + client.registerChannel('node-minecraft-protocol:custom_channel_one', ['string', []], true) + client.registerChannel('node-minecraft-protocol:custom_channel_two', ['string', []], true) client.write('position', { x: 0, y: 1.62, @@ -38,6 +36,6 @@ server.on('login', function (client) { pitch: 0, flags: 0x00 }) - client.writeChannel('CUSTOM|ChannelTwo', 10) - client.on('CUSTOM|ChannelOne', console.log) + client.writeChannel('node-minecraft-protocol:custom_channel_two', 'hello from the server') + client.on('node-minecraft-protocol:custom_channel_one', console.log) }) diff --git a/src/client/pluginChannels.js b/src/client/pluginChannels.js index 8ffe69145..dfcb2ffc8 100644 --- a/src/client/pluginChannels.js +++ b/src/client/pluginChannels.js @@ -14,15 +14,15 @@ module.exports = function (client, options) { client.unregisterChannel = unregisterChannel client.writeChannel = writeChannel - client.registerChannel('REGISTER', ['registerarr', []]) - client.registerChannel('UNREGISTER', ['registerarr', []]) - - const above385 = options.protocolVersion >= 385 + const above385 = mcdata.version.version >= 385 if (above385) { // 1.13-pre3 (385) added Added Login Plugin Message (https://wiki.vg/Protocol_History#1.13-pre3) client.on('login_plugin_request', onLoginPluginRequest) } const channelNames = above385 ? ['minecraft:register', 'minecraft:unregister'] : ['REGISTER', 'UNREGISTER'] + client.registerChannel(channelNames[0], ['registerarr', []]) + client.registerChannel(channelNames[1], ['registerarr', []]) + function registerChannel (name, parser, custom) { if (custom) { client.writeChannel(channelNames[0], [name]) From ab9aaac971123a05f5addb16d94d8159ebdeba1b Mon Sep 17 00:00:00 2001 From: "Hitesh. V" <91016099+hvlxh@users.noreply.github.com> Date: Sat, 20 May 2023 18:25:38 +0530 Subject: [PATCH 086/171] Update _sidebar.md (#1103) Updated the outdated link to working link --- docs/_sidebar.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 85de633f6..b30de2a0f 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -2,5 +2,5 @@ - [API](API.md) - [FAQ](FAQ.md) - [History](HISTORY.md) -- [Protocol documentation](http://minecraft-data.prismarine.js.org/?d=protocol) -- [Wiki.vg](https://wiki.vg/Protocol) \ No newline at end of file +- [Protocol Documentation](http://prismarinejs.github.io/minecraft-data?d=protocol) +- [Wiki.vg](https://wiki.vg/Protocol) From d6b18b1dadde92b8a08856adce6c8d30b09389d0 Mon Sep 17 00:00:00 2001 From: XHawk87 Date: Sat, 20 May 2023 13:58:38 +0100 Subject: [PATCH 087/171] Typo in "cypher" property check. Fixes #1106 (#1107) --- src/client/play.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/play.js b/src/client/play.js index a75c29b7e..21d31a94f 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -13,7 +13,7 @@ module.exports = function (client, options) { client.once('login', () => { const mcData = require('minecraft-data')(client.version) - if (mcData.supportFeature('useChatSessions') && client.profileKeys && client._cipher) { + if (mcData.supportFeature('useChatSessions') && client.profileKeys && client.cipher) { client._session = { index: 0, uuid: uuid.v4fast() From d9e37e3d76eea47b3e9a8583135a293da47b05ce Mon Sep 17 00:00:00 2001 From: IceTank <61137113+IceTank@users.noreply.github.com> Date: Sat, 20 May 2023 15:03:55 +0200 Subject: [PATCH 088/171] Add ipc connection option for servers (#1113) * Add ipc connection option for servers * Fix linting error --- docs/API.md | 3 ++- examples/ipc/ipc_server.js | 51 ++++++++++++++++++++++++++++++++++++++ examples/ipc/package.json | 8 ++++++ src/createServer.js | 9 +++++-- src/index.d.ts | 1 + 5 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 examples/ipc/ipc_server.js create mode 100644 examples/ipc/package.json diff --git a/docs/API.md b/docs/API.md index 0b2cb5e4e..c18a04241 100644 --- a/docs/API.md +++ b/docs/API.md @@ -33,7 +33,8 @@ automatically logged in and validated against mojang's auth. * validateChannelProtocol (optional) : whether or not to enable protocol validation for custom protocols using plugin channels for the connected clients. Defaults to true * 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. + ## mc.Server(version,[customPackets]) Create a server instance for `version` of minecraft. diff --git a/examples/ipc/ipc_server.js b/examples/ipc/ipc_server.js new file mode 100644 index 000000000..1eb917537 --- /dev/null +++ b/examples/ipc/ipc_server.js @@ -0,0 +1,51 @@ +/** IPC Connection example + * + * This example shows how to use a IPC connection to communicate with a server or client. + * + * See the node.js documentation about IPC connections here: https://nodejs.org/api/net.html#identifying-paths-for-ipc-connections + */ + +const nmp = require('minecraft-protocol') +const net = require('net') + +const ipcName = 'minecraft-ipc' + +// IPC with node.js works differently on windows and unix systems +let ipcPath +if (process.platform === 'win32') { + ipcPath = `\\\\.\\pipe\\${ipcName}` +} else { + ipcPath = `/tmp/${ipcName}.sock` +} + +const server = nmp.createServer({ + version: '1.18.2', + socketType: 'ipc', + host: ipcPath, // When the optional option socketType is 'ipc' the host becomes the socket path + 'online-mode': false +}) + +server.on('listening', () => { + console.info('Server listening on', server.socketServer.address()) + connectAClient() +}) + +server.on('login', (client) => { + console.info(`New user '${client.username}' logged into the server`) +}) + +function connectAClient () { + const client = nmp.createClient({ + version: '1.18.2', + username: 'ipc_client', + connect: (client) => { + const socket = net.connect(ipcPath, () => { + client.setSocket(socket) + client.emit('connect') + }) + }, + auth: 'offline' + }) + client.on('connect', () => console.info('Client connected to server')) + client.on('end', () => console.info('Client disconnected from server')) +} diff --git a/examples/ipc/package.json b/examples/ipc/package.json new file mode 100644 index 000000000..cb8cd4913 --- /dev/null +++ b/examples/ipc/package.json @@ -0,0 +1,8 @@ +{ + "name": "node-minecraft-protocol-example", + "version": "0.0.0", + "private": true, + "dependencies": { + }, + "description": "A node-minecraft-protocol example" +} \ No newline at end of file diff --git a/src/createServer.js b/src/createServer.js index a7e7372b1..4fa3477ee 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -23,7 +23,8 @@ function createServer (options = {}) { version, favicon, customPackets, - motdMsg // This is when you want to send formated motd's from ChatMessage instances + motdMsg, // This is when you want to send formated motd's from ChatMessage instances + socketType = 'tcp' } = options const maxPlayers = options['max-players'] !== undefined ? maxPlayersOld : maxPlayersNew @@ -63,6 +64,10 @@ function createServer (options = {}) { server.on('connection', function (client) { plugins.forEach(plugin => plugin(client, server, options)) }) - server.listen(port, host) + if (socketType === 'ipc') { + server.listen(host) + } else { + server.listen(port, host) + } return server } diff --git a/src/index.d.ts b/src/index.d.ts index c7607bcf0..7135cbb5a 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -192,6 +192,7 @@ declare module 'minecraft-protocol' { enforceSecureProfile?: boolean // 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' } export interface SerializerOptions { From d643d4041546558a4a9fd16a79be8a4b82c9cff8 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sun, 28 May 2023 04:28:56 -0400 Subject: [PATCH 089/171] reapply on master (#1067) --- docs/API.md | 8 +++---- src/client/chat.js | 54 ++++++++++++++++++++++++++++++++++++++++++++-- src/server/chat.js | 9 +++----- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/docs/API.md b/docs/API.md index c18a04241..85af2d650 100644 --- a/docs/API.md +++ b/docs/API.md @@ -266,9 +266,9 @@ Called when an error occurs within the client. Takes an Error as parameter. ### `playerChat` event Called when a chat message from another player arrives. The emitted object contains: -* formattedMessage -- the chat message preformatted, if done on server side -* plainMessage -- the chat message without formatting (for example no ` message` ; instead `message`), on version 1.19+ -* unsignedContent -- unsigned formatted chat contents ; should only be present when the message is modified and server has chat previews disabled - only on version 1.19 - 1.19.2 +* formattedMessage -- (JSON) the chat message preformatted, if done on server side +* plainMessage -- (Plaintext) the chat message without formatting (for example no ` message` ; instead `message`), on version 1.19+ +* unsignedContent -- (JSON) unsigned formatted chat contents ; should only be present when the message is modified and server has chat previews disabled - only on version 1.19 - 1.19.2 * 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) @@ -279,7 +279,7 @@ Called when a chat message from another player arrives. The emitted object conta ### `systemChat` event Called when a system chat message arrives. A system chat message is any message not sent by a player. The emitted object contains: -* formattedMessage -- the chat message preformatted +* formattedMessage -- (JSON) the chat message preformatted * positionId -- the chat type of the message. 1 for system chat and 2 for actionbar See the [chat example](https://github.com/PrismarineJS/node-minecraft-protocol/blob/master/examples/client_chat/client_chat.js#L1) for usage. diff --git a/src/client/chat.js b/src/client/chat.js index 3d13b8de0..6235e4ca5 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -266,7 +266,7 @@ module.exports = function (client, options) { plain: packet.plainMessage, decorated: packet.formattedMessage }, - messageHash: packet.bodyDigest, + messageHash: packet.messageHash, timestamp: packet.timestamp, salt: packet.salt, lastSeen: packet.previousMessages @@ -297,6 +297,39 @@ module.exports = function (client, options) { }) }) + const sliceIndexForMessage = {} + client.on('declare_commands', (packet) => { + const nodes = packet.nodes + for (const commandNode of nodes[0].children) { + const node = nodes[commandNode] + const commandName = node.extraNodeData.name + function visit (node, depth = 0) { + const name = node.extraNodeData.name + if (node.extraNodeData.parser === 'minecraft:message') { + sliceIndexForMessage[commandName] = [name, depth] + } + for (const child of node.children) { + visit(nodes[child], depth + 1) + } + } + visit(node, 0) + } + }) + + function signaturesForCommand (string, ts, salt) { + const signatures = [] + const slices = string.split(' ') + if (sliceIndexForMessage[slices[0]]) { + const [fieldName, sliceIndex] = sliceIndexForMessage[slices[0]] + const sliced = slices.slice(sliceIndex) + if (sliced.length > 0) { + const signable = sliced.join(' ') + signatures.push({ argumentName: fieldName, signature: client.signMessage(signable, ts, salt) }) + } + } + return signatures + } + // Chat Sending let pendingChatRequest let lastPreviewRequestId = 0 @@ -305,6 +338,23 @@ module.exports = function (client, options) { options.timestamp = options.timestamp || BigInt(Date.now()) options.salt = options.salt || 1n + if (message.startsWith('/') && !mcData.supportFeature('useChatSessions')) { + const command = message.slice(1) + client.write('chat_command', { + command, + timestamp: options.timestamp, + salt: options.salt, + argumentSignatures: signaturesForCommand(command, options.timestamp, options.salt), + signedPreview: options.didPreview, + previousMessages: client._lastSeenMessages.map((e) => ({ + messageSender: e.sender, + messageSignature: e.signature + })), + lastRejectedMessage: client._lastRejectedMessage + }) + return + } + if (mcData.supportFeature('useChatSessions')) { let acc = 0 const acknowledgements = [] @@ -351,7 +401,7 @@ module.exports = function (client, options) { lastRejectedMessage: client._lastRejectedMessage }) client._lastSeenMessages.pending = 0 - } else { + } else if (client.serverFeatures.chatPreview) { client.write('chat_preview', { query: lastPreviewRequestId, message diff --git a/src/server/chat.js b/src/server/chat.js index bab658259..a7b4daabb 100644 --- a/src/server/chat.js +++ b/src/server/chat.js @@ -7,12 +7,9 @@ const { mojangPublicKeyPem } = require('./constants') class VerificationError extends Error {} function validateLastMessages (pending, lastSeen, lastRejected) { if (lastRejected) { - const rejectedTs = pending.get(lastRejected.sender, lastRejected.signature) - if (!rejectedTs) { - throw new VerificationError(`Client rejected a message we never sent from '${lastRejected.sender}'`) - } else { - pending.acknowledge(lastRejected.sender, lastRejected.signature) - } + const rejectedTime = pending.get(lastRejected.sender, lastRejected.signature) + if (rejectedTime) pending.acknowledge(lastRejected.sender, lastRejected.signature) + else throw new VerificationError(`Client rejected a message we never sent from '${lastRejected.sender}'`) } let lastTimestamp From a32a1cf478eff23c8c6d799ff8bc55be5d770f99 Mon Sep 17 00:00:00 2001 From: andriycraft <92477814+andriycraft@users.noreply.github.com> Date: Sat, 3 Jun 2023 21:32:19 +0200 Subject: [PATCH 090/171] Improve code style of examples in README.md (#1227) --- docs/README.md | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/docs/README.md b/docs/README.md index a32126d21..fc300c1a3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -72,21 +72,25 @@ node-minecraft-protocol is pluggable. ### Echo client example ```js -var mc = require('minecraft-protocol'); -var client = mc.createClient({ +const mc = require('minecraft-protocol'); +const client = mc.createClient({ host: "localhost", // optional port: 25565, // optional username: "email@example.com", password: "12345678", auth: 'microsoft' // optional; by default uses offline mode, if using a microsoft account, set to 'microsoft' }); + client.on('chat', function(packet) { // Listen for chat messages and echo them back. - var jsonMsg = JSON.parse(packet.message); - if(jsonMsg.translate == 'chat.type.announcement' || jsonMsg.translate == 'chat.type.text') { - var username = jsonMsg.with[0].text; - var msg = jsonMsg.with[1]; - if(username === client.username) return; + const jsonMsg = JSON.parse(packet.message); + + if (jsonMsg.translate == 'chat.type.announcement' || jsonMsg.translate == 'chat.type.text') { + const username = jsonMsg.with[0].text; + const msg = jsonMsg.with[1]; + + if (username === client.username) return; + client.write('chat', {message: msg.text}); } }); @@ -100,8 +104,8 @@ You can also leave out `password` when using a Microsoft account. If provided, p Example to connect to a Realm that the authenticating account is owner of or has been invited to: ```js -var mc = require('minecraft-protocol'); -var client = mc.createClient({ +const mc = require('minecraft-protocol'); +const client = mc.createClient({ realms: { pickRealm: (realms) => realms[0] // Function which recieves an array of joined/owned Realms and must return a single Realm. Can be async }, @@ -112,8 +116,8 @@ var client = mc.createClient({ ### Hello World server example ```js -var mc = require('minecraft-protocol'); -var server = mc.createServer({ +const mc = require('minecraft-protocol'); +const server = mc.createServer({ 'online-mode': true, // optional encryption: true, // optional host: '0.0.0.0', // optional @@ -123,8 +127,7 @@ var server = mc.createServer({ const mcData = require('minecraft-data')(server.version) server.on('login', function(client) { - - let loginPacket = mcData.loginPacket + const loginPacket = mcData.loginPacket client.write('login', { entityId: client.id, @@ -143,21 +146,24 @@ server.on('login', function(client) { isDebug: false, isFlat: false }); + client.write('position', { x: 0, - y: 1.62, + y: 255, z: 0, yaw: 0, pitch: 0, flags: 0x00 }); - var msg = { + + const msg = { translate: 'chat.type.announcement', "with": [ 'Server', 'Hello, world!' ] }; + client.write("chat", { message: JSON.stringify(msg), position: 0, sender: '0' }); }); ``` @@ -175,7 +181,7 @@ You can enable some protocol debugging output using `DEBUG` environment variable DEBUG="minecraft-protocol" node [...] ``` -On windows : +On Windows: ``` set DEBUG=minecraft-protocol node your_script.js From 2718bc64c0c6a4a0b106a88a5b60d0ce46f3e8ff Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sat, 3 Jun 2023 15:54:31 -0400 Subject: [PATCH 091/171] 1.19.4 (#1226) * Rename 'session' (packet) to chat_session_update to fix auth event conflict * impl packet "bundle" grouping, add client.writeBundle(packets) * fix handling, test * test 1.19.4 * 1.19.4 test ci * test ci against mcdata fork * lint * fix delim * fix 1.19.3 being skipped * Update ci.yml * Update package.json --------- Co-authored-by: Romain Beaumont --- .github/workflows/ci.yml | 2 +- .gitpod.yml | 2 +- package.json | 2 +- src/client.js | 34 ++++++++++++++++++----- src/client/play.js | 2 +- src/server/chat.js | 2 +- src/version.js | 4 +-- test/packetTest.js | 14 ++++++++-- test/serverTest.js | 60 ++++++++++++++++++++++++++++++++-------- 9 files changed, 94 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4de44f807..97bc3e61b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - mcVersion: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3'] + mcVersion: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4'] fail-fast: false steps: diff --git a/.gitpod.yml b/.gitpod.yml index 38fc373b5..13f366c56 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -1,2 +1,2 @@ tasks: -- command: npm install +- command: npm install && sdk install java diff --git a/package.json b/package.json index 7f0d7a5da..4dfd98616 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^3.21.0", + "minecraft-data": "^3.34.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", diff --git a/src/client.js b/src/client.js index 4146d6bbf..d716be140 100644 --- a/src/client.js +++ b/src/client.js @@ -1,5 +1,4 @@ 'use strict' - const EventEmitter = require('events').EventEmitter const debug = require('debug')('minecraft-protocol') const compression = require('./transforms/compression') @@ -30,8 +29,9 @@ class Client extends EventEmitter { this.latency = 0 this.hideErrors = hideErrors this.closeTimer = null - + const mcData = require('minecraft-data')(version) this.state = states.HANDSHAKING + this._hasBundlePacket = mcData.supportFeature('hasBundlePacket') } get state () { @@ -77,7 +77,13 @@ class Client extends EventEmitter { 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) + this.emit(parsed.metadata.name, parsed.data, parsed.metadata) + 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 @@ -87,10 +93,18 @@ class Client extends EventEmitter { const s = JSON.stringify(parsed.data, null, 2) debug(s && s.length > 10000 ? parsed.data : s) } - this.emit('packet', parsed.data, parsed.metadata, parsed.buffer, parsed.fullBuffer) - this.emit(parsed.metadata.name, parsed.data, parsed.metadata) - this.emit('raw.' + parsed.metadata.name, parsed.buffer, parsed.metadata) - this.emit('raw', parsed.buffer, parsed.metadata) + if (parsed.metadata.name === 'bundle_delimiter') { + if (this._mcBundle.length) { + this._mcBundle.forEach(emitPacket) + this._mcBundle = [] + } else { // Start bundle + this._mcBundle.push(parsed) + } + } else if (this._mcBundle.length) { + this._mcBundle.push(parsed) + } else { + emitPacket(parsed) + } }) } @@ -221,6 +235,12 @@ class Client extends EventEmitter { this.serializer.write({ name, params }) } + writeBundle (packets) { + if (this._hasBundlePacket) this.write('bundle_delimiter', {}) + for (const [name, params] of packets) this.write(name, params) + if (this._hasBundlePacket) this.write('bundle_delimiter', {}) + } + writeRaw (buffer) { const stream = this.compressor === null ? this.framer : this.compressor if (!stream.writable) { return } diff --git a/src/client/play.js b/src/client/play.js index 21d31a94f..344ad9ddf 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -19,7 +19,7 @@ module.exports = function (client, options) { uuid: uuid.v4fast() } - client.write('session', { + client.write('chat_session_update', { sessionUUID: client._session.uuid, expireTime: client.profileKeys ? BigInt(client.profileKeys.expiresOn.getTime()) : undefined, publicKey: client.profileKeys ? client.profileKeys.public.export({ type: 'spki', format: 'der' }) : undefined, diff --git a/src/server/chat.js b/src/server/chat.js index a7b4daabb..dc9dc3b04 100644 --- a/src/server/chat.js +++ b/src/server/chat.js @@ -76,7 +76,7 @@ module.exports = function (client, server, options) { } } - client.on('session', (packet) => { + client.on('chat_session_update', (packet) => { client._session = { index: 0, uuid: packet.sessionUuid diff --git a/src/version.js b/src/version.js index 75cf507e9..d1c5ed88f 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.19.3', - supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3'] + defaultVersion: '1.19.4', + supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4'] } diff --git a/test/packetTest.js b/test/packetTest.js index ae4ac5ec1..c4b4ba5b5 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -82,7 +82,7 @@ const values = { } Object.keys(typeArgs).forEach(function (index) { const v = typeArgs[index].name === 'type' && typeArgs[index].type === 'string' && typeArgs[2] !== undefined && - typeArgs[2].type !== undefined + typeArgs[2].type !== undefined ? (typeArgs[2].type[1].fields['minecraft:crafting_shapeless'] === undefined ? 'crafting_shapeless' : 'minecraft:crafting_shapeless') : getValue(typeArgs[index].type, results) if (typeArgs[index].anon) { @@ -96,6 +96,15 @@ const values = { delete results['..'] return results }, + vec3f: { + x: 0, y: 0, z: 0 + }, + vec3f64: { + x: 0, y: 0, z: 0 + }, + vec4f: { + x: 0, y: 0, z: 0, w: 0 + }, count: 1, // TODO : might want to set this to a correct value bool: true, f64: 99999.2222, @@ -266,8 +275,7 @@ for (const supportedVersion of mc.supportedVersions) { Object.keys(packets[state]).forEach(function (direction) { Object.keys(packets[state][direction].types) .filter(function (packetName) { - return packetName !== 'packet' && - packetName.startsWith('packet_') + return packetName !== 'packet' && packetName.startsWith('packet_') }) .forEach(function (packetName) { packetInfo = packets[state][direction].types[packetName] diff --git a/test/serverTest.js b/test/serverTest.js index 3683572d7..7a8a5142a 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -62,23 +62,23 @@ for (const supportedVersion of mc.supportedVersions) { // removed `dimension` // removed `dimensionCodec` registryCodec: { - "type": "compound", - "name": "", - "value": {} + type: 'compound', + name: '', + value: {} }, - worldType: "minecraft:overworld", + worldType: 'minecraft:overworld', death: undefined // more to be added } } - function sendBroadcastMessage(server, clients, message, sender) { + function sendBroadcastMessage (server, clients, message, sender) { if (mcData.supportFeature('signedChat')) { server.writeToClients(clients, 'player_chat', { plainMessage: message, signedChatContent: '', unsignedChatContent: JSON.stringify({ text: message }), - type: 0, + type: 0, senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random senderName: JSON.stringify({ text: sender }), senderTeam: undefined, @@ -96,7 +96,7 @@ for (const supportedVersion of mc.supportedVersions) { describe('mc-server ' + version.minecraftVersion, function () { this.timeout(5000) - this.beforeAll(async function() { + this.beforeAll(async function () { PORT = await getPort() console.log(`Using port for tests: ${PORT}`) }) @@ -214,7 +214,7 @@ for (const supportedVersion of mc.supportedVersions) { sample: [] }, description: { - extra: [ { color: 'red', text: 'Red text' } ], + extra: [{ color: 'red', text: 'Red text' }], bold: true, text: 'Example chat mesasge' } @@ -278,7 +278,7 @@ for (const supportedVersion of mc.supportedVersions) { version: version.minecraftVersion, port: PORT }) - client.on('packet', (data, {name})=>{ + client.on('packet', (data, { name }) => { if (name === 'success') { assert.strictEqual(data.uuid, notchUUID, 'UUID') server.close() @@ -333,7 +333,7 @@ for (const supportedVersion of mc.supportedVersions) { })) const p1Join = await player1.nextMessage('player2') - + assert.strictEqual(p1Join, '{"text":"player2 joined the game."}') player2.chat('hi') @@ -441,7 +441,7 @@ for (const supportedVersion of mc.supportedVersions) { sendBroadcastMessage(server, Object.values(server.clients), 'A message from the server.') - let results = await Promise.all([player1.nextMessage(), player2.nextMessage()]) + const results = await Promise.all([player1.nextMessage(), player2.nextMessage()]) for (const msg of results) { assert.strictEqual(msg, '{"text":"A message from the server."}') } @@ -452,5 +452,43 @@ for (const supportedVersion of mc.supportedVersions) { server.close() }) }) + + it('supports bundle packet', function (done) { + const server = mc.createServer({ + 'online-mode': false, + version: version.minecraftVersion, + port: PORT + }) + server.on('login', function (client) { + client.on('end', function (reason) { + assert.strictEqual(reason, 'ServerShutdown') + }) + client.write('login', loginPacket(client, server)) + client.writeBundle([ + ['update_time', { age: 1, time: 2 }], + ['close_window', { windowId: 0 }] + ]) + }) + server.on('close', done) + server.on('listening', function () { + const client = mc.createClient({ + username: 'lalalal', + host: '127.0.0.1', + version: version.minecraftVersion, + port: PORT + }) + client.on('update_time', function () { + // Below handler synchronously defined should be guaranteed to be called after the above one + const d1 = Date.now() + client.on('close_window', function () { + server.close() + const d2 = Date.now() + if (mcData.supportFeature('hasBundlePacket') && (d2 - d1) > 1) { + throw new Error(`bundle packet constituents did not arrive at once : ${d1}, ${d2}`) + } + }) + }) + }) + }) }) } From 2f4485d528e92f0fae365823b5539717d0a493de Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sat, 3 Jun 2023 21:57:10 +0200 Subject: [PATCH 092/171] Release 1.42.0 --- docs/HISTORY.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 1082769a2..faf8e486a 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,6 +1,14 @@ # History -##1.41.2 +## 1.42.0 + +* 1.19.4 support (@extremeheat) +* Fix plugin channels support (@turikhay) +* Typo in "cypher" property check (@XHawk87) +* Add ipc connection option for servers (@IceTank) +* bug fix (@extremeheat) + +## 1.41.2 * Fix client sending session packet while server is in login state (@frej4189) * Fix client attempting to sign messages on offline servers (@frej4189) From 1af5a36fb042a20a07f4bf10935806d5cc471e58 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sat, 3 Jun 2023 22:09:51 +0200 Subject: [PATCH 093/171] Release 1.42.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4dfd98616..d98051df2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.41.2", + "version": "1.42.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 1376cf6a317d0156b188744068658ae6f7d93299 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy Date: Sat, 3 Jun 2023 23:17:27 +0200 Subject: [PATCH 094/171] chore(runtime): use LTS V18 (#1224) * chore(runtime): use LTS V18 * chore(workflows): update * remove usage `node:` * Update packetTest.js * Update client.js --------- Co-authored-by: Romain Beaumont --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/npm-publish.yml | 2 +- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97bc3e61b..9a6ef63ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,10 +18,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Use Node.js 14.x + - name: Use Node.js 18.x uses: actions/setup-node@v1 with: - node-version: 14.x + node-version: 18.x - name: Setup Java JDK uses: actions/setup-java@v1.4.3 with: @@ -36,10 +36,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Use Node.js 14.x + - name: Use Node.js 18.x uses: actions/setup-node@v1 with: - node-version: 14.x + node-version: 18.x - name: Setup Java JDK uses: actions/setup-java@v1.4.3 with: diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 387db4010..4ef5c8e1e 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -13,7 +13,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@master with: - node-version: 14.0.0 + node-version: 18.0.0 - id: publish uses: JS-DevTools/npm-publish@v1 with: diff --git a/package.json b/package.json index d98051df2..8bb40af52 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "author": "Andrew Kelley", "license": "BSD-3-Clause", "engines": { - "node": ">=14" + "node": ">=18" }, "browser": "src/browser.js", "devDependencies": { From 6579b0b564060724a1befccf0994092d3fb54f22 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sun, 4 Jun 2023 09:41:39 +0200 Subject: [PATCH 095/171] Add 1.19.4 to readme --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index fc300c1a3..0350933f7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3) + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. From 38e7914cc536f6ddd807c615362000c9fb7f54d5 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Fri, 23 Jun 2023 16:01:35 -0400 Subject: [PATCH 096/171] Fix test for entityMetadata on 1.19.4 (#1231) * Fix test for entityMetadata on 1.19.4 A field type was changed from int to a named enum * oops * fix timing --- test/packetTest.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/packetTest.js b/test/packetTest.js index c4b4ba5b5..0fc81af07 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -249,6 +249,11 @@ for (const supportedVersion of mc.supportedVersions) { before(async function () { PORT = await getPort() server = new Server(version.minecraftVersion) + if (mcData.supportFeature('mcDataHasEntityMetadata')) { + values.entityMetadata[0].type = 'byte' + } else { + values.entityMetadata[0].type = 0 + } return new Promise((resolve) => { console.log(`Using port for tests: ${PORT}`) server.once('listening', function () { From 960df173ccf6703678647639e6149a036e392409 Mon Sep 17 00:00:00 2001 From: PondWader <66561610+PondWader@users.noreply.github.com> Date: Sat, 24 Jun 2023 20:09:41 +0100 Subject: [PATCH 097/171] 1.20 (#1232) * 1.20 * Add 1.20 to README * bump mcdata * clean --------- Co-authored-by: Romain Beaumont --- .github/workflows/ci.yml | 2 +- docs/README.md | 2 +- package.json | 2 +- src/version.js | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a6ef63ee..deaedc4a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - mcVersion: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4'] + mcVersion: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1'] fail-fast: false steps: diff --git a/docs/README.md b/docs/README.md index 0350933f7..4f7f2d470 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4) + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. diff --git a/package.json b/package.json index 8bb40af52..1782e5b9a 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^3.34.0", + "minecraft-data": "^3.37.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", diff --git a/src/version.js b/src/version.js index d1c5ed88f..5b98d2753 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.19.4', - supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4'] + defaultVersion: '1.20.1', + supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1'] } From f404b8d94b950730e3584c57b19ca7a905f9e004 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sat, 24 Jun 2023 21:12:42 +0200 Subject: [PATCH 098/171] Release 1.43.0 (#1234) * Update package.json * Update HISTORY.md --- docs/HISTORY.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index faf8e486a..7588ed2cd 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,9 @@ # History +## 1.43.0 + +* 1.20.0 and .1 support (@PondWader) + ## 1.42.0 * 1.19.4 support (@extremeheat) diff --git a/package.json b/package.json index 1782e5b9a..17d1c2bc8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.42.0", + "version": "1.43.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From fa0116386b259212bfb65f73295595a03468b7c1 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Tue, 27 Jun 2023 13:20:01 +0200 Subject: [PATCH 099/171] Temporarily make node 18 not required in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 17d1c2bc8..09abd5473 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "author": "Andrew Kelley", "license": "BSD-3-Clause", "engines": { - "node": ">=18" + "node": ">=14" }, "browser": "src/browser.js", "devDependencies": { From 05e5a701f09911e39de9d9a078d0ff763a0928a5 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Tue, 27 Jun 2023 13:21:17 +0200 Subject: [PATCH 100/171] Release 1.43.1 (#1235) * Update HISTORY.md * Update package.json --- docs/HISTORY.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 7588ed2cd..714fc4922 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,9 @@ # History +## 1.43.1 + +* Temporarily make node 18 not required in package.json + ## 1.43.0 * 1.20.0 and .1 support (@PondWader) diff --git a/package.json b/package.json index 09abd5473..aed411781 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.43.0", + "version": "1.43.1", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 5a64f1435947e8ee4352f0149443bf6406180da7 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Fri, 14 Jul 2023 22:21:06 +0200 Subject: [PATCH 101/171] Fix client sending chat_session_update when local UUID does not match UUID on server (#1237) Co-authored-by: Frej Alexander Nielsen --- src/client/play.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/play.js b/src/client/play.js index 344ad9ddf..14d280433 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -13,7 +13,7 @@ module.exports = function (client, options) { client.once('login', () => { const mcData = require('minecraft-data')(client.version) - if (mcData.supportFeature('useChatSessions') && client.profileKeys && client.cipher) { + if (mcData.supportFeature('useChatSessions') && client.profileKeys && client.cipher && client.session.selectedProfile.id === client.uuid.replace(/-/g, '')) { client._session = { index: 0, uuid: uuid.v4fast() From 2e45c7f4ab2cc5a72ee07e5087f8062b3c371609 Mon Sep 17 00:00:00 2001 From: Frej Alexander Nielsen Date: Sun, 16 Jul 2023 20:34:44 +0200 Subject: [PATCH 102/171] Release 1.43.2 (#1239) * Update package.json * Update HISTORY.md --- docs/HISTORY.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 714fc4922..d18617a5d 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,9 @@ # History +## 1.43.2 + +* Fix client sending chat_session_update when local UUID does not match UUID on server (@frej4189) + ## 1.43.1 * Temporarily make node 18 not required in package.json diff --git a/package.json b/package.json index aed411781..94ff86f5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.43.1", + "version": "1.43.2", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 1a4cfa7f5ee1a896b6a924708536d3f956cb869e Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sat, 22 Jul 2023 14:39:59 +0200 Subject: [PATCH 103/171] Add command gh workflow allowing to use release command in comments (#1244) --- .github/workflows/commands.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/commands.yml diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml new file mode 100644 index 000000000..d2286e26e --- /dev/null +++ b/.github/workflows/commands.yml @@ -0,0 +1,22 @@ +name: Repo Commands + +on: + issue_comment: # Handle comment commands + types: [created] + pull_request: # Handle renamed PRs + types: [edited] + +jobs: + comment-trigger: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v3 + - name: Run command handlers + uses: PrismarineJS/prismarine-repo-actions@master + with: + # NOTE: You must specify a Personal Access Token (PAT) with repo access here. While you can use the default GITHUB_TOKEN, actions taken with it will not trigger other actions, so if you have a CI workflow, commits created by this action will not trigger it. + token: ${{ secrets.PAT_PASSWORD }} + # See `Options` section below for more info on these options + install-command: npm install + /fixlint.fix-command: npm run fix \ No newline at end of file From cc9aa9416101407421bdd085002ec2b26ccfbc83 Mon Sep 17 00:00:00 2001 From: Micha Date: Sat, 22 Jul 2023 14:59:37 +0200 Subject: [PATCH 104/171] fix broken link (#1243) * fix broken link * Update README.md --------- Co-authored-by: Romain Beaumont --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 4f7f2d470..1a5e7a792 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,7 +65,7 @@ node-minecraft-protocol is pluggable. * [API doc](API.md) * [faq](FAQ.md) -* [protocol doc](https://minecraft-data.prismarine.js.org/?d=protocol) and [wiki.vg/Protocol](https://wiki.vg/Protocol) +* [protocol doc](https://prismarinejs.github.io/minecraft-data/?d=protocol) and [wiki.vg/Protocol](https://wiki.vg/Protocol) ## Usage From 94b9c228b07bbaf210aa9f90ab240cb6aa9d7751 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 29 Jul 2023 10:27:33 +0200 Subject: [PATCH 105/171] Bump @types/readable-stream from 2.3.15 to 4.0.0 (#1247) Bumps [@types/readable-stream](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/readable-stream) from 2.3.15 to 4.0.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/readable-stream) --- updated-dependencies: - dependency-name: "@types/readable-stream" dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 94ff86f5b..0868a194d 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "standard": "^17.0.0" }, "dependencies": { - "@types/readable-stream": "^2.3.13", + "@types/readable-stream": "^4.0.0", "aes-js": "^3.1.2", "buffer-equal": "^1.0.0", "debug": "^4.3.2", From 35b2aa536a4739c11fe78f6e8e5c591abd0b0498 Mon Sep 17 00:00:00 2001 From: PondWader <66561610+PondWader@users.noreply.github.com> Date: Sat, 29 Jul 2023 09:34:36 +0100 Subject: [PATCH 106/171] Fix end bundle bundle_delimiter packet not being emitted (#1248) --- src/client.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index d716be140..6f1182337 100644 --- a/src/client.js +++ b/src/client.js @@ -94,8 +94,9 @@ class Client extends EventEmitter { debug(s && s.length > 10000 ? parsed.data : s) } if (parsed.metadata.name === 'bundle_delimiter') { - if (this._mcBundle.length) { + if (this._mcBundle.length) { // End bundle this._mcBundle.forEach(emitPacket) + emitPacket(parsed) this._mcBundle = [] } else { // Start bundle this._mcBundle.push(parsed) From 41f9e4ac4a35b0ce241264a3f964c4874d96a119 Mon Sep 17 00:00:00 2001 From: Lucas Wilson Date: Sat, 5 Aug 2023 07:21:26 -0600 Subject: [PATCH 107/171] Send chat commands as chat commands instead of chat messages for 1.19.3-1.20.1 (#1241) * Add fields required for 1.19.3-1.20 * Send chat_command if msg starts with / * Create separate block for useChatSessions * Extract acknowledgements to function * Set messageCount to pending * Handle useChatSessions for chat commands * Zero pending on chat command * Sign with acknowledgements --- src/client/chat.js | 90 ++++++++++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/src/client/chat.js b/src/client/chat.js index 6235e4ca5..5674f1f1c 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -316,7 +316,7 @@ module.exports = function (client, options) { } }) - function signaturesForCommand (string, ts, salt) { + function signaturesForCommand (string, ts, salt, preview, acknowledgements) { const signatures = [] const slices = string.split(' ') if (sliceIndexForMessage[slices[0]]) { @@ -324,7 +324,7 @@ module.exports = function (client, options) { const sliced = slices.slice(sliceIndex) if (sliced.length > 0) { const signable = sliced.join(' ') - signatures.push({ argumentName: fieldName, signature: client.signMessage(signable, ts, salt) }) + signatures.push({ argumentName: fieldName, signature: client.signMessage(signable, ts, salt, preview, acknowledgements) }) } } return signatures @@ -334,53 +334,75 @@ module.exports = function (client, options) { let pendingChatRequest let lastPreviewRequestId = 0 + function getAcknowledgements () { + let acc = 0 + const acknowledgements = [] + + for (let i = 0; i < client._lastSeenMessages.capacity; i++) { + const idx = (client._lastSeenMessages.offset + i) % 20 + const message = client._lastSeenMessages[idx] + if (message) { + acc |= 1 << i + acknowledgements.push(message.signature) + message.pending = false + } + } + + const bitset = Buffer.allocUnsafe(3) + bitset[0] = acc & 0xFF + bitset[1] = (acc >> 8) & 0xFF + bitset[2] = (acc >> 16) & 0xFF + + return { + acknowledgements, + acknowledged: bitset + } + } + client._signedChat = (message, options = {}) => { options.timestamp = options.timestamp || BigInt(Date.now()) options.salt = options.salt || 1n - if (message.startsWith('/') && !mcData.supportFeature('useChatSessions')) { + if (message.startsWith('/')) { const command = message.slice(1) - client.write('chat_command', { - command, - timestamp: options.timestamp, - salt: options.salt, - argumentSignatures: signaturesForCommand(command, options.timestamp, options.salt), - signedPreview: options.didPreview, - previousMessages: client._lastSeenMessages.map((e) => ({ - messageSender: e.sender, - messageSignature: e.signature - })), - lastRejectedMessage: client._lastRejectedMessage - }) + if (mcData.supportFeature('useChatSessions')) { + const { acknowledged, acknowledgements } = getAcknowledgements() + client.write('chat_command', { + command, + timestamp: options.timestamp, + salt: options.salt, + argumentSignatures: signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements), + messageCount: client._lastSeenMessages.pending, + acknowledged + }) + client._lastSeenMessages.pending = 0 + } else { + client.write('chat_command', { + command, + timestamp: options.timestamp, + salt: options.salt, + argumentSignatures: signaturesForCommand(command, options.timestamp, options.salt), + signedPreview: options.didPreview, + previousMessages: client._lastSeenMessages.map((e) => ({ + messageSender: e.sender, + messageSignature: e.signature + })), + lastRejectedMessage: client._lastRejectedMessage + }) + } + return } if (mcData.supportFeature('useChatSessions')) { - let acc = 0 - const acknowledgements = [] - - for (let i = 0; i < client._lastSeenMessages.capacity; i++) { - const idx = (client._lastSeenMessages.offset + i) % 20 - const message = client._lastSeenMessages[idx] - if (message) { - acc |= 1 << i - acknowledgements.push(message.signature) - message.pending = false - } - } - - const bitset = Buffer.allocUnsafe(3) - bitset[0] = acc & 0xFF - bitset[1] = (acc >> 8) & 0xFF - bitset[2] = (acc >> 16) & 0xFF - + const { acknowledgements, acknowledged } = getAcknowledgements() client.write('chat_message', { message, timestamp: options.timestamp, salt: options.salt, signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined, offset: client._lastSeenMessages.pending, - acknowledged: bitset + acknowledged }) client._lastSeenMessages.pending = 0 From 827b6cdb61eac9e1b737fa0a9fb8e81385a78d3d Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Sat, 5 Aug 2023 15:27:09 +0200 Subject: [PATCH 108/171] Release 1.44.0 (#1250) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index d18617a5d..410d9d0e8 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,12 @@ # History +## 1.44.0 +* [Send chat commands as chat commands instead of chat messages for 1.19.3-1.20.1 (#1241)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/41f9e4ac4a35b0ce241264a3f964c4874d96a119) (thanks @lkwilson) +* [Fix end bundle bundle_delimiter packet not being emitted (#1248)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/35b2aa536a4739c11fe78f6e8e5c591abd0b0498) (thanks @PondWader) +* [Bump @types/readable-stream from 2.3.15 to 4.0.0 (#1247)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/94b9c228b07bbaf210aa9f90ab240cb6aa9d7751) (thanks @dependabot[bot]) +* [fix broken link (#1243)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/cc9aa9416101407421bdd085002ec2b26ccfbc83) (thanks @FurriousFox) +* [Add command gh workflow allowing to use release command in comments (#1244)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/1a4cfa7f5ee1a896b6a924708536d3f956cb869e) (thanks @rom1504) + ## 1.43.2 * Fix client sending chat_session_update when local UUID does not match UUID on server (@frej4189) diff --git a/package.json b/package.json index 0868a194d..8dcff3d15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.43.2", + "version": "1.44.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 0ac8c087a28b3ccc73f8eea5941e4902e33c494e Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Sun, 17 Dec 2023 11:57:34 -0500 Subject: [PATCH 109/171] chat: Only sign command args when profile keys defined (#1257) --- src/client/chat.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/chat.js b/src/client/chat.js index 5674f1f1c..e4a94667f 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -371,7 +371,7 @@ module.exports = function (client, options) { command, timestamp: options.timestamp, salt: options.salt, - argumentSignatures: signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements), + argumentSignatures: client.profileKeys ? signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements) : [], messageCount: client._lastSeenMessages.pending, acknowledged }) @@ -381,7 +381,7 @@ module.exports = function (client, options) { command, timestamp: options.timestamp, salt: options.salt, - argumentSignatures: signaturesForCommand(command, options.timestamp, options.salt), + argumentSignatures: client.profileKeys ? signaturesForCommand(command, options.timestamp, options.salt) : [], signedPreview: options.didPreview, previousMessages: client._lastSeenMessages.map((e) => ({ messageSender: e.sender, From 788bff289030fa66c980de82d82cb953bf76332b Mon Sep 17 00:00:00 2001 From: IceTank <61137113+IceTank@users.noreply.github.com> Date: Sun, 17 Dec 2023 19:08:24 +0100 Subject: [PATCH 110/171] Add chat typing to client (#1260) --- src/index.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.d.ts b/src/index.d.ts index 7135cbb5a..6333a6a7b 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -5,7 +5,7 @@ import { Socket } from 'net' import * as Stream from 'stream' import { Agent } from 'http' import { Transform } from "readable-stream"; -import { KeyObject } from 'crypto'; +import { BinaryLike, KeyObject } from 'crypto'; import { Realm } from "prismarine-realms" type PromiseLike = Promise | void @@ -39,6 +39,7 @@ declare module 'minecraft-protocol' { signMessage(message: string, timestamp: BigInt, salt?: number, preview?: string, acknowledgements?: Buffer[]): Buffer verifyMessage(publicKey: Buffer | KeyObject, packet: object): boolean reportPlayer(uuid: string, reason: 'FALSE_REPORTING' | 'HATE_SPEECH' | 'TERRORISM_OR_VIOLENT_EXTREMISM' | 'CHILD_SEXUAL_EXPLOITATION_OR_ABUSE' | 'IMMINENT_HARM' | 'NON_CONSENSUAL_INTIMATE_IMAGERY' | 'HARASSMENT_OR_BULLYING' | 'DEFAMATION_IMPERSONATION_FALSE_INFORMATION' | 'SELF_HARM_OR_SUICIDE' | 'ALCOHOL_TOBACCO_DRUGS', signatures: Buffer[], comment?: string): Promise + chat(message: string, options?: { timestamp?: BigInt, salt?: BigInt, preview?: BinaryLike, didPreview?: boolean }): void on(event: 'error', listener: (error: Error) => PromiseLike): this on(event: 'packet', handler: (data: any, packetMeta: PacketMeta, buffer: Buffer, fullBuffer: Buffer) => PromiseLike): this on(event: 'raw', handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this From 066a2b3646cb8bef6be1fa974597b975aaf08d42 Mon Sep 17 00:00:00 2001 From: Ynfuien <88110072+Ynfuien@users.noreply.github.com> Date: Wed, 27 Dec 2023 16:32:42 +0100 Subject: [PATCH 111/171] Fixed 'unsignedContent' field using nonexistent 'packet.unsignedContent' when emitting 'playerChat' event. (#1263) --- src/client/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/chat.js b/src/client/chat.js index e4a94667f..3571b7330 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -198,7 +198,7 @@ module.exports = function (client, options) { if (verified) client._signatureCache.push(packet.signature) client.emit('playerChat', { plainMessage: packet.plainMessage, - unsignedContent: packet.unsignedContent, + unsignedContent: packet.unsignedChatContent, type: packet.type, sender: packet.senderUuid, senderName: packet.networkName, From 9e991094761d51243cb28a33bb45630f3064511d Mon Sep 17 00:00:00 2001 From: Vitaly Date: Wed, 27 Dec 2023 21:04:45 +0530 Subject: [PATCH 112/171] Allow to create custom client & communication between clients (#1254) * allow to create custom client & communication between client * pass client context * customClient should not be required * allow to override Server impl to use * better docs & typings * refactor: add new client class for customCommunication * fix doc * move custom client to prismarine web client * restore customPackets --- docs/API.md | 24 +++++++++++++----------- src/createClient.js | 3 ++- src/createServer.js | 3 ++- src/index.d.ts | 18 ++++++++++-------- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/docs/API.md b/docs/API.md index 85af2d650..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. @@ -34,7 +34,8 @@ 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]) Create a server instance for `version` of minecraft. @@ -112,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. @@ -129,21 +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. + * 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]) @@ -235,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 @@ -272,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/createClient.js b/src/createClient.js index 1c4f0330a..97a6336d0 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') @@ -31,6 +31,7 @@ function createClient (options) { options.majorVersion = version.majorVersion options.protocolVersion = version.version const hideErrors = options.hideErrors || false + const Client = options.Client || DefaultClientImpl const client = new Client(false, version.minecraftVersion, options.customPackets, hideErrors) 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, diff --git a/src/index.d.ts b/src/index.d.ts index 6333a6a7b..a788aecc7 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -118,7 +118,7 @@ declare module 'minecraft-protocol' { authTitle?: string sessionServer?: string keepAlive?: boolean - closeTimeout?: number + closeTimeout?: number noPongTimeout?: number checkTimeoutInterval?: number version?: string @@ -137,6 +137,8 @@ declare module 'minecraft-protocol' { realms?: RealmsOptions // 1.19+ disableChatSigning?: boolean + /** Pass custom client implementation if needed. */ + Client?: Client } export class Server extends EventEmitter { @@ -162,9 +164,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 } @@ -188,12 +190,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 { @@ -202,7 +204,7 @@ declare module 'minecraft-protocol' { state?: States version: string } - + export interface MicrosoftDeviceAuthorizationResponse { device_code: string user_code: string From 1740124c4722c2c49f8aed0d708ff5ebecc7743c Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Thu, 28 Dec 2023 00:12:23 +0100 Subject: [PATCH 113/171] Improve CI setup for per version tests (#1267) * Improve CI setup: move lint out of per version * fix * Simplify go back to all per version. * refactor cycle test to use supported versions * fix cycle packet test * Add v to version to avoid 1.19 running every 1.19 minor. * Add quotes. * Use versions from js file in ci.yml * Fix ci.yml syntax. * Fix matrix read. * fix * fix * fix gitignore --- .github/workflows/ci.yml | 46 +++++++++++++++++++++------------- .gitignore | 2 +- package.json | 4 +-- test/benchmark.js | 2 +- test/clientTest.js | 2 +- test/cyclePacketTest.js | 53 ++++++++++++++++++++++++++++++++++++++++ test/non-par-test.js | 39 ----------------------------- test/packetTest.js | 2 +- test/serverTest.js | 2 +- 9 files changed, 89 insertions(+), 63 deletions(-) create mode 100644 test/cyclePacketTest.js delete mode 100644 test/non-par-test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index deaedc4a9..9bf82ba3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,30 +9,39 @@ on: - master jobs: - test: + Lint: runs-on: ubuntu-latest - strategy: - matrix: - mcVersion: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1'] - fail-fast: false steps: - uses: actions/checkout@v2 - name: Use Node.js 18.x - uses: actions/setup-node@v1 + uses: actions/setup-node@v1.4.4 with: node-version: 18.x - - name: Setup Java JDK - uses: actions/setup-java@v1.4.3 + - run: npm i && npm run lint + PrepareSupportedVersions: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 18.x + uses: actions/setup-node@v1.4.4 with: - java-version: '17' - distribution: 'adopt' - - name: Install dependencies - run: npm install - - name: Run tests - run: npm test -- -g ${{ matrix.mcVersion }} - packet-cycle-test: + node-version: 18.x + - id: set-matrix + run: | + node -e " + const supportedVersions = require('./src/version').supportedVersions; + console.log('matrix='+JSON.stringify({'include': supportedVersions.map(mcVersion => ({mcVersion}))})) + " >> $GITHUB_OUTPUT + test: + needs: PrepareSupportedVersions runs-on: ubuntu-latest + strategy: + matrix: ${{fromJson(needs.PrepareSupportedVersions.outputs.matrix)}} + fail-fast: false steps: - uses: actions/checkout@v2 @@ -43,6 +52,9 @@ jobs: - name: Setup Java JDK uses: actions/setup-java@v1.4.3 with: - java-version: '16' + java-version: '17' distribution: 'adopt' - - run: npm install && npm run test-non-par + - name: Install dependencies + run: npm install + - name: Run tests + run: npm run mochaTest -- -g ${{ matrix.mcVersion }}v diff --git a/.gitignore b/.gitignore index 9afc68587..f6816b81a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ node_modules test/npm-debug.log -test/server* +test/server_* package-lock.json versions/ src/client/*.json \ No newline at end of file diff --git a/package.json b/package.json index 8dcff3d15..9a3f7b2ae 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "url": "git://github.com/PrismarineJS/node-minecraft-protocol.git" }, "scripts": { - "test": "mocha --recursive --reporter spec --exit --exclude \"non-par-test.js\"", - "test-non-par": "mocha --recursive --reporter spec --exit \"test/non-par-test.js\"", + "test": "npm run mochaTest", + "mochaTest": "mocha --recursive --reporter spec --exit", "lint": "standard", "fix": "standard --fix", "pretest": "npm run lint", diff --git a/test/benchmark.js b/test/benchmark.js index 4dc87f68b..a4610371d 100644 --- a/test/benchmark.js +++ b/test/benchmark.js @@ -16,7 +16,7 @@ const testDataWrite = [ for (const supportedVersion of mc.supportedVersions) { const mcData = require('minecraft-data')(supportedVersion) const version = mcData.version - describe('benchmark ' + version.minecraftVersion, function () { + describe('benchmark ' + supportedVersion + 'v', function () { this.timeout(60 * 1000) const inputData = [] it('bench serializing', function (done) { diff --git a/test/clientTest.js b/test/clientTest.js index 91796264c..2c229f7c3 100644 --- a/test/clientTest.js +++ b/test/clientTest.js @@ -28,7 +28,7 @@ for (const supportedVersion of mc.supportedVersions) { console.log(line) }) - describe('client ' + version.minecraftVersion, function () { + describe('client ' + supportedVersion + 'v', function () { this.timeout(10 * 60 * 1000) before(async function () { diff --git a/test/cyclePacketTest.js b/test/cyclePacketTest.js new file mode 100644 index 000000000..9bd9f62b0 --- /dev/null +++ b/test/cyclePacketTest.js @@ -0,0 +1,53 @@ +/* eslint-env mocha */ +// Tests packet serialization/deserialization from with raw binary from minecraft-packets +const { createSerializer, createDeserializer, states, supportedVersions } = require('minecraft-protocol') +const mcPackets = require('minecraft-packets') +const assert = require('assert') + +const makeClientSerializer = version => createSerializer({ state: states.PLAY, version, isServer: true }) +const makeClientDeserializer = version => createDeserializer({ state: states.PLAY, version }) + +for (const supportedVersion of supportedVersions) { + let serializer, deserializer, data + const mcData = require('minecraft-data')(supportedVersion) + const version = mcData.version + + function convertBufferToObject (buffer) { + return deserializer.parsePacketBuffer(buffer) + } + + function convertObjectToBuffer (object) { + return serializer.createPacketBuffer(object) + } + + function testBuffer (buffer, [packetName, packetIx]) { + const parsed = convertBufferToObject(buffer).data + const parsedBuffer = convertObjectToBuffer(parsed) + const areEq = buffer.equals(parsedBuffer) + assert.strictEqual(areEq, true, `Error when testing ${+packetIx + 1} ${packetName} packet`) + } + describe(`Test cycle packet for version ${supportedVersion}v`, () => { + before(() => { + serializer = makeClientSerializer(version.minecraftVersion) + deserializer = makeClientDeserializer(version.minecraftVersion) + }) + data = mcPackets.pc[version.minecraftVersion] + it('Has packet data', () => { + if (data === undefined) { + // many version do not have data, so print a log for now + // assert when most versions have packet data + console.log(`Version ${version.minecraftVersion} has no packet dump.`) + } + }) + // server -> client + if (data !== undefined) { + Object.entries(data['from-server']).forEach(([packetName, packetData]) => { + it(`${packetName} packet`, () => { + for (const i in packetData) { + testBuffer(packetData[i].raw, [packetName, i]) + } + }) + }) + } + }) +} diff --git a/test/non-par-test.js b/test/non-par-test.js deleted file mode 100644 index 81c420cb0..000000000 --- a/test/non-par-test.js +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-env mocha */ -// Tests packet serialization/deserialization from with raw binary from minecraft-packets -const { createSerializer, createDeserializer, states } = require('minecraft-protocol') -const mcPackets = require('minecraft-packets') -const assert = require('assert') - -const makeClientSerializer = version => createSerializer({ state: states.PLAY, version, isServer: true }) -const makeClientDeserializer = version => createDeserializer({ state: states.PLAY, version }) - -Object.entries(mcPackets.pc).forEach(([ver, data]) => { - let serializer, deserializer - - function convertBufferToObject (buffer) { - return deserializer.parsePacketBuffer(buffer) - } - - function convertObjectToBuffer (object) { - return serializer.createPacketBuffer(object) - } - - function testBuffer (buffer, [packetName, packetIx]) { - const parsed = convertBufferToObject(buffer).data - const parsedBuffer = convertObjectToBuffer(parsed) - const areEq = buffer.equals(parsedBuffer) - assert.strictEqual(areEq, true, `Error when testing ${+packetIx + 1} ${packetName} packet`) - } - describe(`Test version ${ver}`, () => { - serializer = makeClientSerializer(ver) - deserializer = makeClientDeserializer(ver) - // server -> client - Object.entries(data['from-server']).forEach(([packetName, packetData]) => { - it(`${packetName} packet`, () => { - for (const i in packetData) { - testBuffer(packetData[i].raw, [packetName, i]) - } - }) - }) - }) -}) diff --git a/test/packetTest.js b/test/packetTest.js index 0fc81af07..cedfe372e 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -244,7 +244,7 @@ for (const supportedVersion of mc.supportedVersions) { const version = mcData.version const packets = mcData.protocol - describe('packets ' + version.minecraftVersion, function () { + describe('packets ' + supportedVersion + 'v', function () { let client, server, serverClient before(async function () { PORT = await getPort() diff --git a/test/serverTest.js b/test/serverTest.js index 7a8a5142a..b18a5ba48 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -94,7 +94,7 @@ for (const supportedVersion of mc.supportedVersions) { } } - describe('mc-server ' + version.minecraftVersion, function () { + describe('mc-server ' + supportedVersion + 'v', function () { this.timeout(5000) this.beforeAll(async function () { PORT = await getPort() From 112926da0cb2490934d122dd8ed7b79f3f6de8eb Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 27 Dec 2023 18:48:10 -0500 Subject: [PATCH 114/171] Pc1.20.2 (#1265) * Initial changes for 1.20.2 * add NBT serialize tag type handling * update tests * Update pnbt and mcdata for nbt change * lint * fix wrong param to sizeOfNbt * fix dupe NBT types * move nbt logic to prismarine-nbt * update tests * update tests * disable protodef validator in pluginChannel * Fix state desync * dump loginPacket.json in test output * enable validation * remove testing line in ci.yml * update pnbt to 2.5.0 * update doc for `playerJoin` * Update serializer.js * update examples * lint * disable client bundle handling if bundle becomes too big * Update client.js * bump mcdata * add soundSource and packedChunkPos example test values --------- Co-authored-by: Romain Beaumont --- docs/API.md | 9 +++ docs/README.md | 14 ++--- examples/server/server.js | 58 ++++++++--------- examples/server_channel/server_channel.js | 6 +- .../server_custom_channel.js | 6 +- .../server_helloworld/server_helloworld.js | 2 +- examples/server_world/mc.js | 6 +- package.json | 4 +- src/client.js | 7 ++- src/client/play.js | 58 ++++++++++++----- src/client/pluginChannels.js | 2 + src/client/setProtocol.js | 2 +- src/createClient.js | 3 + src/createServer.js | 1 + src/datatypes/compiler-minecraft.js | 6 -- src/datatypes/minecraft.js | 35 +---------- src/datatypes/uuid.js | 18 ++++++ src/index.d.ts | 4 ++ src/server/login.js | 37 +++++------ src/states.js | 1 + src/transforms/serializer.js | 3 + src/version.js | 4 +- test/clientTest.js | 22 ++++++- test/packetTest.js | 63 +++++++------------ test/serverTest.js | 19 +++--- 25 files changed, 212 insertions(+), 178 deletions(-) create mode 100644 src/datatypes/uuid.js diff --git a/docs/API.md b/docs/API.md index fd5482035..3d6bd3cb7 100644 --- a/docs/API.md +++ b/docs/API.md @@ -89,6 +89,11 @@ Called when a client connects, but before any login has happened. Takes a Called when a client is logged in against server. Takes a `Client` parameter. +### `playerJoin` event + +Emitted after a player joins and enters the PLAY protocol state and can send and recieve game packets. This is emitted after the `login` event. On 1.20.2 and above after we emit the `login` event, the player will enter a CONFIG state, as opposed to the PLAY state (where game packets can be sent), so you must instead now wait for `playerJoin`. + + ### `listening` event Called when the server is listening for connections. This means that the server is ready to accept incoming connections. @@ -261,6 +266,10 @@ Called when user authentication is resolved. Takes session data as parameter. Called when the protocol changes state. Takes the new state and old state as parameters. +### `playerJoin` event + +Emitted after the player enters the PLAY protocol state and can send and recieve game packets + ### `error` event Called when an error occurs within the client. Takes an Error as parameter. diff --git a/docs/README.md b/docs/README.md index 1a5e7a792..2005dbe30 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1) + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. @@ -115,6 +115,8 @@ const client = mc.createClient({ ### Hello World server example +For a more up to date example, see examples/server/server.js. + ```js const mc = require('minecraft-protocol'); const server = mc.createServer({ @@ -126,18 +128,12 @@ const server = mc.createServer({ }); const mcData = require('minecraft-data')(server.version) -server.on('login', function(client) { +server.on('playerJoin', function(client) { const loginPacket = mcData.loginPacket client.write('login', { + ...loginPacket, entityId: client.id, - isHardcore: false, - gameMode: 0, - previousGameMode: 255, - worldNames: loginPacket.worldNames, - dimensionCodec: loginPacket.dimensionCodec, - dimension: loginPacket.dimension, - worldName: 'minecraft:overworld', hashedSeed: [0, 0], maxPlayers: server.maxPlayers, viewDistance: 10, diff --git a/examples/server/server.js b/examples/server/server.js index 4604c87a2..482531adf 100644 --- a/examples/server/server.js +++ b/examples/server/server.js @@ -11,7 +11,7 @@ const server = mc.createServer(options) const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket -server.on('login', function (client) { +server.on('playerJoin', function (client) { broadcast(client.username + ' joined the game.') const addr = client.socket.remoteAddress + ':' + client.socket.remotePort console.log(client.username + ' connected', '(' + addr + ')') @@ -23,14 +23,11 @@ server.on('login', function (client) { // send init data so client will start rendering world client.write('login', { + ...loginPacket, entityId: client.id, isHardcore: false, gameMode: 0, previousGameMode: 1, - worldNames: loginPacket.worldNames, - dimensionCodec: loginPacket.dimensionCodec, - dimension: loginPacket.dimension, - worldName: 'minecraft:overworld', hashedSeed: [0, 0], maxPlayers: server.maxPlayers, viewDistance: 10, @@ -48,11 +45,13 @@ server.on('login', function (client) { flags: 0x00 }) - client.on('chat', function (data) { + function handleChat (data) { const message = '<' + client.username + '>' + ' ' + data.message broadcast(message, null, client.username) console.log(message) - }) + } + client.on('chat', handleChat) // pre-1.19 + client.on('chat_message', handleChat) // post 1.19 }) server.on('error', function (error) { @@ -63,27 +62,28 @@ server.on('listening', function () { console.log('Server listening on port', server.socketServer.address().port) }) -function broadcast (message, exclude, username) { - let client - const translate = username ? 'chat.type.announcement' : 'chat.type.text' - username = username || 'Server' - for (const clientId in server.clients) { - if (server.clients[clientId] === undefined) continue - - client = server.clients[clientId] - if (client !== exclude) { - const msg = { - translate, - with: [ - username, - message - ] - } - client.write('chat', { - message: JSON.stringify(msg), - position: 0, - sender: '0' - }) - } +function sendBroadcastMessage (server, clients, message, sender) { + if (mcData.supportFeature('signedChat')) { + server.writeToClients(clients, 'player_chat', { + plainMessage: message, + signedChatContent: '', + unsignedChatContent: JSON.stringify({ text: message }), + type: 0, + senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random + senderName: JSON.stringify({ text: sender }), + senderTeam: undefined, + timestamp: Date.now(), + salt: 0n, + signature: mcData.supportFeature('useChatSessions') ? undefined : Buffer.alloc(0), + previousMessages: [], + filterType: 0, + networkName: JSON.stringify({ text: sender }) + }) + } else { + server.writeToClients(clients, 'chat', { message: JSON.stringify({ text: message }), position: 0, sender: sender || '0' }) } } + +function broadcast (message, exclude, username) { + sendBroadcastMessage(server, Object.values(server.clients).filter(client => client !== exclude), message) +} diff --git a/examples/server_channel/server_channel.js b/examples/server_channel/server_channel.js index 28e842939..72f00cf01 100644 --- a/examples/server_channel/server_channel.js +++ b/examples/server_channel/server_channel.js @@ -8,18 +8,16 @@ const server = mc.createServer({ const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket -server.on('login', function (client) { +server.on('playerJoin', function (client) { client.registerChannel('minecraft:brand', ['string', []]) client.on('minecraft:brand', console.log) client.write('login', { + ...loginPacket, entityId: client.id, isHardcore: false, gameMode: 0, previousGameMode: 1, - worldNames: loginPacket.worldNames, - dimensionCodec: loginPacket.dimensionCodec, - dimension: loginPacket.dimension, worldName: 'minecraft:overworld', hashedSeed: [0, 0], maxPlayers: server.maxPlayers, diff --git a/examples/server_custom_channel/server_custom_channel.js b/examples/server_custom_channel/server_custom_channel.js index 040fe80c0..69f2b1014 100644 --- a/examples/server_custom_channel/server_custom_channel.js +++ b/examples/server_custom_channel/server_custom_channel.js @@ -8,15 +8,13 @@ const server = mc.createServer({ const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket -server.on('login', function (client) { +server.on('playerJoin', function (client) { client.write('login', { + ...loginPacket, entityId: client.id, isHardcore: false, gameMode: 0, previousGameMode: 1, - worldNames: loginPacket.worldNames, - dimensionCodec: loginPacket.dimensionCodec, - dimension: loginPacket.dimension, worldName: 'minecraft:overworld', hashedSeed: [0, 0], maxPlayers: server.maxPlayers, diff --git a/examples/server_helloworld/server_helloworld.js b/examples/server_helloworld/server_helloworld.js index d752a3010..0dd0223fb 100644 --- a/examples/server_helloworld/server_helloworld.js +++ b/examples/server_helloworld/server_helloworld.js @@ -9,7 +9,7 @@ const server = mc.createServer(options) const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket -server.on('login', function (client) { +server.on('playerJoin', function (client) { const addr = client.socket.remoteAddress console.log('Incoming connection', '(' + addr + ')') diff --git a/examples/server_world/mc.js b/examples/server_world/mc.js index 7acf27ca0..eb0975ee9 100644 --- a/examples/server_world/mc.js +++ b/examples/server_world/mc.js @@ -22,15 +22,13 @@ for (let x = 0; x < 16; x++) { } } -server.on('login', function (client) { +server.on('playerJoin', function (client) { client.write('login', { + ...loginPacket, entityId: client.id, isHardcore: false, gameMode: 0, previousGameMode: 1, - worldNames: loginPacket.worldNames, - dimensionCodec: loginPacket.dimensionCodec, - dimension: loginPacket.dimension, worldName: 'minecraft:overworld', hashedSeed: [0, 0], maxPlayers: server.maxPlayers, diff --git a/package.json b/package.json index 9a3f7b2ae..4bddeebcb 100644 --- a/package.json +++ b/package.json @@ -51,12 +51,12 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^3.37.0", + "minecraft-data": "^3.53.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", "prismarine-auth": "^2.2.0", - "prismarine-nbt": "^2.0.0", + "prismarine-nbt": "^2.5.0", "prismarine-realms": "^1.2.0", "protodef": "^1.8.0", "readable-stream": "^4.1.0", diff --git a/src/client.js b/src/client.js index 6f1182337..c89375e32 100644 --- a/src/client.js +++ b/src/client.js @@ -93,7 +93,7 @@ class Client extends EventEmitter { const s = JSON.stringify(parsed.data, null, 2) debug(s && s.length > 10000 ? parsed.data : s) } - if (parsed.metadata.name === 'bundle_delimiter') { + if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') { if (this._mcBundle.length) { // End bundle this._mcBundle.forEach(emitPacket) emitPacket(parsed) @@ -103,6 +103,11 @@ class Client extends EventEmitter { } } else if (this._mcBundle.length) { this._mcBundle.push(parsed) + if (this._mcBundle.length > 32) { + this._mcBundle.forEach(emitPacket) + this._mcBundle = [] + this._hasBundlePacket = false + } } else { emitPacket(parsed) } diff --git a/src/client/play.js b/src/client/play.js index 14d280433..949a9e0c2 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -32,30 +32,54 @@ module.exports = function (client, options) { function onLogin (packet) { const mcData = require('minecraft-data')(client.version) - client.state = states.PLAY client.uuid = packet.uuid client.username = packet.username - if (mcData.supportFeature('signedChat')) { - if (options.disableChatSigning && client.serverFeatures.enforcesSecureChat) { - throw new Error('"disableChatSigning" was enabled in client options, but server is enforcing secure chat') - } - signedChatPlugin(client, options) + if (mcData.supportFeature('hasConfigurationState')) { + client.write('login_acknowledged', {}) + enterConfigState() + // Server can tell client to re-enter config state + client.on('start_configuration', enterConfigState) } else { - client.on('chat', (packet) => { - client.emit(packet.position === 0 ? 'playerChat' : 'systemChat', { - formattedMessage: packet.message, - sender: packet.sender, - positionId: packet.position, - verified: false - }) - }) + client.state = states.PLAY + onReady() } - function unsignedChat (message) { - client.write('chat', { message }) + function enterConfigState () { + if (client.state === states.CONFIGURATION) return + client.state = states.CONFIGURATION + // Server should send finish_configuration on its own right after sending the client a dimension codec + // for login (that has data about world height, world gen, etc) after getting a login success from client + client.once('finish_configuration', () => { + client.write('finish_configuration', {}) + client.state = states.PLAY + onReady() + }) } - client.chat = client._signedChat || unsignedChat + function onReady () { + client.emit('playerJoin') + if (mcData.supportFeature('signedChat')) { + if (options.disableChatSigning && client.serverFeatures.enforcesSecureChat) { + throw new Error('"disableChatSigning" was enabled in client options, but server is enforcing secure chat') + } + signedChatPlugin(client, options) + } else { + client.on('chat', (packet) => { + client.emit(packet.position === 0 ? 'playerChat' : 'systemChat', { + formattedMessage: packet.message, + sender: packet.sender, + positionId: packet.position, + verified: false + }) + }) + } + + function unsignedChat (message) { + client.write('chat', { message }) + } + + client.chat = client._signedChat || unsignedChat + } } } diff --git a/src/client/pluginChannels.js b/src/client/pluginChannels.js index dfcb2ffc8..671eb452f 100644 --- a/src/client/pluginChannels.js +++ b/src/client/pluginChannels.js @@ -1,11 +1,13 @@ const ProtoDef = require('protodef').ProtoDef const minecraft = require('../datatypes/minecraft') const debug = require('debug')('minecraft-protocol') +const nbt = require('prismarine-nbt') module.exports = function (client, options) { const mcdata = require('minecraft-data')(options.version || require('../version').defaultVersion) const channels = [] const proto = new ProtoDef(options.validateChannelProtocol ?? true) + nbt.addTypesToInterpreter('big', proto) proto.addTypes(mcdata.protocol.types) proto.addTypes(minecraft) proto.addType('registerarr', [readDumbArr, writeDumbArr, sizeOfDumbArr]) diff --git a/src/client/setProtocol.js b/src/client/setProtocol.js index e656744b4..3842f45ae 100644 --- a/src/client/setProtocol.js +++ b/src/client/setProtocol.js @@ -37,7 +37,7 @@ module.exports = function (client, options) { : client.profileKeys.signature } : null, - playerUUID: client.session?.selectedProfile?.id + playerUUID: client.session?.selectedProfile?.id ?? client.uuid }) } } diff --git a/src/createClient.js b/src/createClient.js index 97a6336d0..10cacc070 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -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 uuid = require('./datatypes/uuid') module.exports = createClient @@ -54,6 +55,8 @@ function createClient (options) { case 'offline': default: client.username = options.username + client.uuid = uuid.nameToMcOfflineUUID(client.username) + options.auth = 'offline' options.connect(client) break } diff --git a/src/createServer.js b/src/createServer.js index a81e34b92..4a56c0ad9 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -46,6 +46,7 @@ function createServer (options = {}) { server.onlineModeExceptions = Object.create(null) server.favicon = favicon server.options = options + options.registryCodec = options.registryCodec || mcData.registryCodec || mcData.loginPacket?.dimensionCodec // The RSA keypair can take some time to generate // and is only needed for online-mode diff --git a/src/datatypes/compiler-minecraft.js b/src/datatypes/compiler-minecraft.js index cac252609..89cacde04 100644 --- a/src/datatypes/compiler-minecraft.js +++ b/src/datatypes/compiler-minecraft.js @@ -16,8 +16,6 @@ module.exports = { size: buffer.length - offset } }], - nbt: ['native', minecraft.nbt[0]], - optionalNbt: ['native', minecraft.optionalNbt[0]], compressedNbt: ['native', minecraft.compressedNbt[0]], entityMetadataLoop: ['parametrizable', (compiler, { type, endVal }) => { let code = 'let cursor = offset\n' @@ -55,8 +53,6 @@ module.exports = { value.copy(buffer, offset) return offset + value.length }], - nbt: ['native', minecraft.nbt[1]], - optionalNbt: ['native', minecraft.optionalNbt[1]], compressedNbt: ['native', minecraft.compressedNbt[1]], entityMetadataLoop: ['parametrizable', (compiler, { type, endVal }) => { let code = 'for (const i in value) {\n' @@ -84,8 +80,6 @@ module.exports = { restBuffer: ['native', (value) => { return value.length }], - nbt: ['native', minecraft.nbt[2]], - optionalNbt: ['native', minecraft.optionalNbt[2]], compressedNbt: ['native', minecraft.compressedNbt[2]], entityMetadataLoop: ['parametrizable', (compiler, { type }) => { let code = 'let size = 1\n' diff --git a/src/datatypes/minecraft.js b/src/datatypes/minecraft.js index eb9ef2498..09e90355a 100644 --- a/src/datatypes/minecraft.js +++ b/src/datatypes/minecraft.js @@ -8,8 +8,6 @@ const [readVarInt, writeVarInt, sizeOfVarInt] = require('protodef').types.varint module.exports = { varlong: [readVarLong, writeVarLong, sizeOfVarLong], UUID: [readUUID, writeUUID, 16], - nbt: [readNbt, writeNbt, sizeOfNbt], - optionalNbt: [readOptionalNbt, writeOptionalNbt, sizeOfOptionalNbt], compressedNbt: [readCompressedNbt, writeCompressedNbt, sizeOfCompressedNbt], restBuffer: [readRestBuffer, writeRestBuffer, sizeOfRestBuffer], entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata], @@ -43,35 +41,8 @@ function writeUUID (value, buffer, offset) { return offset + 16 } -function readNbt (buffer, offset) { - return nbt.proto.read(buffer, offset, 'nbt') -} - -function writeNbt (value, buffer, offset) { - return nbt.proto.write(value, buffer, offset, 'nbt') -} - -function sizeOfNbt (value) { - return nbt.proto.sizeOf(value, 'nbt') -} - -function readOptionalNbt (buffer, offset) { - if (offset + 1 > buffer.length) { throw new PartialReadError() } - if (buffer.readInt8(offset) === 0) return { size: 1 } - return nbt.proto.read(buffer, offset, 'nbt') -} - -function writeOptionalNbt (value, buffer, offset) { - if (value === undefined) { - buffer.writeInt8(0, offset) - return offset + 1 - } - return nbt.proto.write(value, buffer, offset, 'nbt') -} - -function sizeOfOptionalNbt (value) { - if (value === undefined) { return 1 } - return nbt.proto.sizeOf(value, 'nbt') +function sizeOfNbt (value, { tagType } = { tagType: 'nbt' }) { + return nbt.proto.sizeOf(value, tagType) } // Length-prefixed compressed NBT, see differences: http://wiki.vg/index.php?title=Slot_Data&diff=6056&oldid=4753 @@ -111,7 +82,7 @@ function writeCompressedNbt (value, buffer, offset) { function sizeOfCompressedNbt (value) { if (value === undefined) { return 2 } - const nbtBuffer = Buffer.alloc(sizeOfNbt(value, 'nbt')) + const nbtBuffer = Buffer.alloc(sizeOfNbt(value, { tagType: 'nbt' })) nbt.proto.write(value, nbtBuffer, 0, 'nbt') const compressedNbt = zlib.gzipSync(nbtBuffer) // TODO: async diff --git a/src/datatypes/uuid.js b/src/datatypes/uuid.js new file mode 100644 index 000000000..7298230d7 --- /dev/null +++ b/src/datatypes/uuid.js @@ -0,0 +1,18 @@ +const crypto = require('crypto') +const UUID = require('uuid-1345') + +// https://github.com/openjdk-mirror/jdk7u-jdk/blob/f4d80957e89a19a29bb9f9807d2a28351ed7f7df/src/share/classes/java/util/UUID.java#L163 +function javaUUID (s) { + const hash = crypto.createHash('md5') + hash.update(s, 'utf8') + const buffer = hash.digest() + buffer[6] = (buffer[6] & 0x0f) | 0x30 + buffer[8] = (buffer[8] & 0x3f) | 0x80 + return buffer +} + +function nameToMcOfflineUUID (name) { + return (new UUID(javaUUID('OfflinePlayer:' + name))).toString() +} + +module.exports = { nameToMcOfflineUUID } diff --git a/src/index.d.ts b/src/index.d.ts index a788aecc7..0a5821c32 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -51,6 +51,8 @@ declare module 'minecraft-protocol' { on(event: `raw.${string}`, handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this on(event: 'playerChat', handler: (data: { formattedMessage: string, message: string, type: string, sender: string, senderName: string, senderTeam: string, verified?: boolean }) => PromiseLike): this on(event: 'systemChat', handler: (data: { positionId: number, formattedMessage: string }) => PromiseLike): this + // Emitted after the player enters the PLAY state and can send and recieve game packets + on(event: 'playerJoin', handler: () => void): this once(event: 'error', listener: (error: Error) => PromiseLike): this once(event: 'packet', handler: (data: any, packetMeta: PacketMeta, buffer: Buffer, fullBuffer: Buffer) => PromiseLike): this once(event: 'raw', handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this @@ -156,6 +158,8 @@ declare module 'minecraft-protocol' { on(event: 'error', listener: (error: Error) => PromiseLike): this on(event: 'login', handler: (client: ServerClient) => PromiseLike): this on(event: 'listening', listener: () => PromiseLike): this + // Emitted after the player enters the PLAY state and can send and recieve game packets + on(event: 'playerJoin', handler: (client: ServerClient) => void): this once(event: 'connection', handler: (client: ServerClient) => PromiseLike): this once(event: 'error', listener: (error: Error) => PromiseLike): this once(event: 'login', handler: (client: ServerClient) => PromiseLike): this diff --git a/src/server/login.js b/src/server/login.js index df52a3630..68dc27a86 100644 --- a/src/server/login.js +++ b/src/server/login.js @@ -1,4 +1,4 @@ -const UUID = require('uuid-1345') +const uuid = require('../datatypes/uuid') const crypto = require('crypto') const pluginChannels = require('../client/pluginChannels') const states = require('../states') @@ -166,37 +166,28 @@ module.exports = function (client, server, options) { } } - // https://github.com/openjdk-mirror/jdk7u-jdk/blob/f4d80957e89a19a29bb9f9807d2a28351ed7f7df/src/share/classes/java/util/UUID.java#L163 - function javaUUID (s) { - const hash = crypto.createHash('md5') - hash.update(s, 'utf8') - const buffer = hash.digest() - buffer[6] = (buffer[6] & 0x0f) | 0x30 - buffer[8] = (buffer[8] & 0x3f) | 0x80 - return buffer - } - - function nameToMcOfflineUUID (name) { - return (new UUID(javaUUID('OfflinePlayer:' + name))).toString() - } - function loginClient () { const isException = !!server.onlineModeExceptions[client.username.toLowerCase()] if (onlineMode === false || isException) { - client.uuid = nameToMcOfflineUUID(client.username) + client.uuid = uuid.nameToMcOfflineUUID(client.username) } options.beforeLogin?.(client) if (client.protocolVersion >= 27) { // 14w28a (27) added whole-protocol compression (http://wiki.vg/Protocol_History#14w28a), earlier versions per-packet compressed TODO: refactor into minecraft-data client.write('compress', { threshold: 256 }) // Default threshold is 256 client.compressionThreshold = 256 } + // TODO: find out what properties are on 'success' packet client.write('success', { uuid: client.uuid, username: client.username, properties: [] }) - // TODO: find out what properties are on 'success' packet - client.state = states.PLAY + if (client.supportFeature('hasConfigurationState')) { + client.once('login_acknowledged', onClientLoginAck) + } else { + client.state = states.PLAY + server.emit('playerJoin', client) + } client.settings = {} if (client.supportFeature('chainedChatWithHashing')) { // 1.19.1+ @@ -217,6 +208,16 @@ module.exports = function (client, server, options) { if (client.supportFeature('signedChat')) chatPlugin(client, server, options) server.emit('login', client) } + + function onClientLoginAck () { + client.state = states.CONFIGURATION + client.write('registry_data', { codec: options.registryCodec || {} }) + client.once('finish_configuration', () => { + client.state = states.PLAY + server.emit('playerJoin', client) + }) + client.write('finish_configuration', {}) + } } function mcPubKeyToPem (mcPubKeyBuffer) { diff --git a/src/states.js b/src/states.js index ba4792fda..34bf360d0 100644 --- a/src/states.js +++ b/src/states.js @@ -4,6 +4,7 @@ const states = { HANDSHAKING: 'handshaking', STATUS: 'status', LOGIN: 'login', + CONFIGURATION: 'configuration', PLAY: 'play' } diff --git a/src/transforms/serializer.js b/src/transforms/serializer.js index 22a6b7447..7cc127e51 100644 --- a/src/transforms/serializer.js +++ b/src/transforms/serializer.js @@ -5,6 +5,7 @@ const Serializer = require('protodef').Serializer const Parser = require('protodef').FullPacketParser const { ProtoDefCompiler } = require('protodef').Compiler +const nbt = require('prismarine-nbt') const minecraft = require('../datatypes/minecraft') const states = require('../states') const merge = require('lodash.merge') @@ -30,6 +31,7 @@ function createProtocol (state, direction, version, customPackets, compiled = tr const compiler = new ProtoDefCompiler() compiler.addTypes(require('../datatypes/compiler-minecraft')) compiler.addProtocol(merge(mcData.protocol, get(customPackets, [mcData.version.majorVersion])), [state, direction]) + nbt.addTypesToCompiler('big', compiler) const proto = compiler.compileProtoDefSync() protocols[key] = proto return proto @@ -38,6 +40,7 @@ function createProtocol (state, direction, version, customPackets, compiled = tr const proto = new ProtoDef(false) proto.addTypes(minecraft) proto.addProtocol(merge(mcData.protocol, get(customPackets, [mcData.version.majorVersion])), [state, direction]) + nbt.addTypesToInterperter('big', proto) protocols[key] = proto return proto } diff --git a/src/version.js b/src/version.js index 5b98d2753..12a5fea5e 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.20.1', - supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1'] + defaultVersion: '1.20.2', + supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2'] } diff --git a/test/clientTest.js b/test/clientTest.js index 2c229f7c3..fc1dc256b 100644 --- a/test/clientTest.js +++ b/test/clientTest.js @@ -2,6 +2,7 @@ const mc = require('../') const os = require('os') +const fs = require('fs') const path = require('path') const assert = require('power-assert') const util = require('util') @@ -20,7 +21,8 @@ for (const supportedVersion of mc.supportedVersions) { const version = mcData.version const MC_SERVER_JAR_DIR = process.env.MC_SERVER_JAR_DIR || os.tmpdir() const MC_SERVER_JAR = MC_SERVER_JAR_DIR + '/minecraft_server.' + version.minecraftVersion + '.jar' - const wrap = new Wrap(MC_SERVER_JAR, MC_SERVER_PATH + '_' + supportedVersion, { + const MC_SERVER_DIR = MC_SERVER_PATH + '_' + supportedVersion + const wrap = new Wrap(MC_SERVER_JAR, MC_SERVER_DIR, { minMem: 1024, maxMem: 1024 }) @@ -118,7 +120,23 @@ for (const supportedVersion of mc.supportedVersions) { assert.strictEqual(packet.gameMode, 0) client.chat('hello everyone; I have logged in.') }) - + // Dump some data for easier debugging + client.on('raw.registry_data', (buffer) => { + fs.writeFileSync(MC_SERVER_DIR + '_registry_data.bin', buffer) + }) + client.on('registry_data', (json) => { + fs.writeFileSync(MC_SERVER_DIR + '_registry_data.json', JSON.stringify(json)) + }) + client.on('login', (packet) => { + fs.writeFileSync(MC_SERVER_DIR + '_login.json', JSON.stringify(packet)) + if (fs.existsSync(MC_SERVER_DIR + '_registry_data.json')) { + // generate a loginPacket.json for minecraft-data + fs.writeFileSync(MC_SERVER_DIR + '_loginPacket.json', JSON.stringify({ + ...packet, + dimensionCodec: JSON.parse(fs.readFileSync(MC_SERVER_DIR + '_registry_data.json')).codec + }, null, 2)) + } + }) client.on('playerChat', function (data) { chatCount += 1 assert.ok(chatCount <= 2) diff --git a/test/packetTest.js b/test/packetTest.js index cedfe372e..e998ad4ea 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -35,6 +35,20 @@ const slotValue = { } } +const nbtValue = { + type: 'compound', + name: 'test', + value: { + test1: { type: 'int', value: 4 }, + test2: { type: 'long', value: [12, 42] }, + test3: { type: 'byteArray', value: [32] }, + test4: { type: 'string', value: 'ohi' }, + test5: { type: 'list', value: { type: 'int', value: [4] } }, + test6: { type: 'compound', value: { test: { type: 'int', value: 4 } } }, + test7: { type: 'intArray', value: [12, 42] } + } +} + const values = { i32: 123456, i16: -123, @@ -110,46 +124,12 @@ const values = { f64: 99999.2222, f32: -333.444, slot: slotValue, - nbt: { - type: 'compound', - name: 'test', - value: { - test1: { type: 'int', value: 4 }, - test2: { type: 'long', value: [12, 42] }, - test3: { type: 'byteArray', value: [32] }, - test4: { type: 'string', value: 'ohi' }, - test5: { type: 'list', value: { type: 'int', value: [4] } }, - test6: { type: 'compound', value: { test: { type: 'int', value: 4 } } }, - test7: { type: 'intArray', value: [12, 42] } - } - }, - optionalNbt: { - type: 'compound', - name: 'test', - value: { - test1: { type: 'int', value: 4 }, - test2: { type: 'long', value: [12, 42] }, - test3: { type: 'byteArray', value: [32] }, - test4: { type: 'string', value: 'ohi' }, - test5: { type: 'list', value: { type: 'int', value: [4] } }, - test6: { type: 'compound', value: { test: { type: 'int', value: 4 } } }, - test7: { type: 'intArray', value: [12, 42] } - } - }, + nbt: nbtValue, + optionalNbt: nbtValue, + compressedNbt: nbtValue, + anonymousNbt: nbtValue, + anonOptionalNbt: nbtValue, previousMessages: [], - compressedNbt: { - type: 'compound', - name: 'test', - value: { - test1: { type: 'int', value: 4 }, - test2: { type: 'long', value: [12, 42] }, - test3: { type: 'byteArray', value: [32] }, - test4: { type: 'string', value: 'ohi' }, - test5: { type: 'list', value: { type: 'int', value: [4] } }, - test6: { type: 'compound', value: { test: { type: 'int', value: 4 } } }, - test7: { type: 'intArray', value: [12, 42] } - } - }, i64: [0, 1], u64: [0, 1], entityMetadata: [ @@ -223,6 +203,11 @@ const values = { }, suggestionType: 'minecraft:summonable_entities' } + }, + soundSource: 'master', + packedChunkPos: { + x: 10, + z: 12 } } diff --git a/test/serverTest.js b/test/serverTest.js index b18a5ba48..b2f671058 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -96,7 +96,7 @@ for (const supportedVersion of mc.supportedVersions) { describe('mc-server ' + supportedVersion + 'v', function () { this.timeout(5000) - this.beforeAll(async function () { + this.beforeEach(async function () { PORT = await getPort() console.log(`Using port for tests: ${PORT}`) }) @@ -299,7 +299,8 @@ for (const supportedVersion of mc.supportedVersions) { const username = ['player1', 'player2'] let index = 0 - server.on('login', function (client) { + server.on('playerJoin', function (client) { + console.log('ChatTest: Player has joined') assert.notEqual(client.id, null) assert.strictEqual(client.username, username[index++]) broadcast(client.username + ' joined the game.') @@ -322,8 +323,10 @@ for (const supportedVersion of mc.supportedVersions) { version: version.minecraftVersion, port: PORT })) + console.log('ChatTest: Player1 is joining...') player1.on('login', async function (packet) { + console.log('ChatTest: Player 1 has joined') assert.strictEqual(packet.gameMode, 1) const player2 = applyClientHelpers(mc.createClient({ username: 'player2', @@ -332,14 +335,16 @@ for (const supportedVersion of mc.supportedVersions) { port: PORT })) + console.log('ChatTest: waiting for next message from P2') const p1Join = await player1.nextMessage('player2') assert.strictEqual(p1Join, '{"text":"player2 joined the game."}') - + console.log('ChatTest: Got message from P2') player2.chat('hi') const p2hi = await player1.nextMessage('player2') assert.strictEqual(p2hi, '{"text":" hi"}') + console.log('ChatTest: Waiting again for next message from P2') player1.chat('hello') const p1hello = await player2.nextMessage('player1') assert.strictEqual(p1hello, '{"text":" hello"}') @@ -389,7 +394,7 @@ for (const supportedVersion of mc.supportedVersions) { port: PORT }) let count = 2 - server.on('login', function (client) { + server.on('playerJoin', function (client) { client.on('end', function (reason) { assert.strictEqual(reason, 'ServerShutdown') resolve() @@ -404,7 +409,7 @@ for (const supportedVersion of mc.supportedVersions) { version: version.minecraftVersion, port: PORT }) - client.on('login', function () { + client.on('playerJoin', function () { server.close() }) }) @@ -420,7 +425,7 @@ for (const supportedVersion of mc.supportedVersions) { version: version.minecraftVersion, port: PORT }) - server.on('login', function (client) { + server.on('playerJoin', function (client) { client.write('login', loginPacket(client, server)) }) server.on('close', done) @@ -459,7 +464,7 @@ for (const supportedVersion of mc.supportedVersions) { version: version.minecraftVersion, port: PORT }) - server.on('login', function (client) { + server.on('playerJoin', function (client) { client.on('end', function (reason) { assert.strictEqual(reason, 'ServerShutdown') }) From eaf4c2e003e167788a90a3523a5ea7f91934f91f Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Thu, 28 Dec 2023 00:51:49 +0100 Subject: [PATCH 115/171] Release 1.45.0 (#1270) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 410d9d0e8..cef148e46 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,13 @@ # History +## 1.45.0 +* [Pc1.20.2 (#1265)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/112926da0cb2490934d122dd8ed7b79f3f6de8eb) (thanks @extremeheat) +* [Improve CI setup for per version tests (#1267)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/1740124c4722c2c49f8aed0d708ff5ebecc7743c) (thanks @rom1504) +* [Allow to create custom client & communication between clients (#1254)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/9e991094761d51243cb28a33bb45630f3064511d) (thanks @zardoy) +* [Fixed 'unsignedContent' field using nonexistent 'packet.unsignedContent' when emitting 'playerChat' event. (#1263)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/066a2b3646cb8bef6be1fa974597b975aaf08d42) (thanks @Ynfuien) +* [Add chat typing to client (#1260)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/788bff289030fa66c980de82d82cb953bf76332b) (thanks @IceTank) +* [chat: Only sign command args when profile keys defined (#1257)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/0ac8c087a28b3ccc73f8eea5941e4902e33c494e) (thanks @evan-goode) + ## 1.44.0 * [Send chat commands as chat commands instead of chat messages for 1.19.3-1.20.1 (#1241)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/41f9e4ac4a35b0ce241264a3f964c4874d96a119) (thanks @lkwilson) * [Fix end bundle bundle_delimiter packet not being emitted (#1248)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/35b2aa536a4739c11fe78f6e8e5c591abd0b0498) (thanks @PondWader) diff --git a/package.json b/package.json index 4bddeebcb..56b5f9894 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.44.0", + "version": "1.45.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From ccaf538ffd2ab1e25dabd752d721f97bd8bd188f Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Fri, 29 Dec 2023 21:49:42 +0100 Subject: [PATCH 116/171] Align supported versions with mineflayer (#1272) --- src/version.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.js b/src/version.js index 12a5fea5e..0bba3cd80 100644 --- a/src/version.js +++ b/src/version.js @@ -2,5 +2,5 @@ module.exports = { defaultVersion: '1.20.2', - supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2'] + supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2'] } From 614be919d0f20a43e238751c829a6d584ae636cd Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Fri, 29 Dec 2023 23:11:31 +0100 Subject: [PATCH 117/171] Print if there is a diff in packets in the cycle packet test (#1273) --- test/cyclePacketTest.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/cyclePacketTest.js b/test/cyclePacketTest.js index 9bd9f62b0..cd8693e97 100644 --- a/test/cyclePacketTest.js +++ b/test/cyclePacketTest.js @@ -24,6 +24,10 @@ for (const supportedVersion of supportedVersions) { const parsed = convertBufferToObject(buffer).data const parsedBuffer = convertObjectToBuffer(parsed) const areEq = buffer.equals(parsedBuffer) + if (!areEq) { + console.log('original buffer', buffer.toString('hex')) + console.log('cycled buffer', parsedBuffer.toString('hex')) + } assert.strictEqual(areEq, true, `Error when testing ${+packetIx + 1} ${packetName} packet`) } describe(`Test cycle packet for version ${supportedVersion}v`, () => { From 80d038bd61d1933daa1e5e3251635be9ce2116b6 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sat, 30 Dec 2023 23:42:17 +0100 Subject: [PATCH 118/171] =?UTF-8?q?Add=20test=20to=20make=20sure=20version?= =?UTF-8?q?=20that=20are=20tested=20are=20mentioned=20in=20the=20RE?= =?UTF-8?q?=E2=80=A6=20(#1276)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add test to make sure version that are tested are mentioned in the README * fix lint --- docs/README.md | 2 +- test/docTest.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 test/docTest.js diff --git a/docs/README.md b/docs/README.md index 2005dbe30..4b738c8d6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2) + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. diff --git a/test/docTest.js b/test/docTest.js new file mode 100644 index 000000000..1b3d80fea --- /dev/null +++ b/test/docTest.js @@ -0,0 +1,16 @@ +/* eslint-env mocha */ + +const mc = require('../') +const fs = require('fs') +const assert = require('assert') +const path = require('path') + +const readmeContent = fs.readFileSync(path.join(__dirname, '/../docs/README.md'), { encoding: 'utf8', flag: 'r' }) + +for (const supportedVersion of mc.supportedVersions) { + describe('doc ' + supportedVersion + 'v', function () { + it('mentions the supported version in the readme', () => { + assert.ok(readmeContent.includes(supportedVersion), `${supportedVersion} should be mentionned in the README.md but it is not`) + }) + }) +} From 21240f8ab2fd41c76f50b64e3b3a945f50b25b5e Mon Sep 17 00:00:00 2001 From: forester302 <109694860+forester302@users.noreply.github.com> Date: Mon, 1 Jan 2024 10:24:49 +0000 Subject: [PATCH 119/171] Allow commands not to be signed (#1277) --- src/client/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/chat.js b/src/client/chat.js index 3571b7330..d35cbab65 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -371,7 +371,7 @@ module.exports = function (client, options) { command, timestamp: options.timestamp, salt: options.salt, - argumentSignatures: client.profileKeys ? signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements) : [], + argumentSignatures: (client.profileKeys && client._session) ? signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements) : [], messageCount: client._lastSeenMessages.pending, acknowledged }) From 092e10c53d33a7b9be52b5cbb67b1e3e55ac2690 Mon Sep 17 00:00:00 2001 From: William Gaylord Date: Sat, 3 Feb 2024 17:07:30 -0600 Subject: [PATCH 120/171] Acknowledge returning to configuration state if in play state. (#1284) * Acknowledge returning to configuration state if in play state. * Fix packet spelling --- src/client/play.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/play.js b/src/client/play.js index 949a9e0c2..2f2c58765 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -47,6 +47,10 @@ module.exports = function (client, options) { function enterConfigState () { if (client.state === states.CONFIGURATION) return + // If we are returning to the configuration state from the play state, we ahve to acknowledge it. + if (client.state === states.PLAY) { + client.write('configuration_acknowledged', {}) + } client.state = states.CONFIGURATION // Server should send finish_configuration on its own right after sending the client a dimension codec // for login (that has data about world height, world gen, etc) after getting a login success from client From 85a26a52944c89af273bc974380b438073280981 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sun, 11 Feb 2024 09:55:15 -0500 Subject: [PATCH 121/171] Ensure `onReady` in client is called once (#1287) Fix https://github.com/PrismarineJS/node-minecraft-protocol/issues/1286 Server sending start_config packets will incorrectly cause onReady to call multiple times --- src/client/play.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/play.js b/src/client/play.js index 2f2c58765..246556e0f 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -37,15 +37,15 @@ module.exports = function (client, options) { if (mcData.supportFeature('hasConfigurationState')) { client.write('login_acknowledged', {}) - enterConfigState() + enterConfigState(onReady) // Server can tell client to re-enter config state - client.on('start_configuration', enterConfigState) + client.on('start_configuration', () => enterConfigState()) } else { client.state = states.PLAY onReady() } - function enterConfigState () { + function enterConfigState (finishCb) { if (client.state === states.CONFIGURATION) return // If we are returning to the configuration state from the play state, we ahve to acknowledge it. if (client.state === states.PLAY) { @@ -57,7 +57,7 @@ module.exports = function (client, options) { client.once('finish_configuration', () => { client.write('finish_configuration', {}) client.state = states.PLAY - onReady() + finishCb?.() }) } From f97a2367bae29df4caa57c911ba39c1aa10ee221 Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Sun, 11 Feb 2024 16:09:25 +0100 Subject: [PATCH 122/171] Release 1.46.0 (#1285) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index cef148e46..33408f1d2 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,13 @@ # History +## 1.46.0 +* [Ensure `onReady` in client is called once (#1287)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/85a26a52944c89af273bc974380b438073280981) (thanks @extremeheat) +* [Acknowledge returning to configuration state if in play state. (#1284)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/092e10c53d33a7b9be52b5cbb67b1e3e55ac2690) (thanks @wgaylord) +* [Allow commands not to be signed (#1277)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/21240f8ab2fd41c76f50b64e3b3a945f50b25b5e) (thanks @forester302) +* [Add test to make sure version that are tested are mentioned in the RE… (#1276)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/80d038bd61d1933daa1e5e3251635be9ce2116b6) (thanks @rom1504) +* [Print if there is a diff in packets in the cycle packet test (#1273)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/614be919d0f20a43e238751c829a6d584ae636cd) (thanks @rom1504) +* [Align supported versions with mineflayer (#1272)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/ccaf538ffd2ab1e25dabd752d721f97bd8bd188f) (thanks @rom1504) + ## 1.45.0 * [Pc1.20.2 (#1265)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/112926da0cb2490934d122dd8ed7b79f3f6de8eb) (thanks @extremeheat) * [Improve CI setup for per version tests (#1267)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/1740124c4722c2c49f8aed0d708ff5ebecc7743c) (thanks @rom1504) diff --git a/package.json b/package.json index 56b5f9894..856abf282 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.45.0", + "version": "1.46.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 1d9a38253a28a515d82fffa13806cb0874c5b36c Mon Sep 17 00:00:00 2001 From: William Gaylord Date: Sun, 25 Feb 2024 18:14:49 -0600 Subject: [PATCH 123/171] 1.20.3 / 1.20.4 support (#1275) * Inital 1.20.3 update, bring in minecraft-data with 1.20.3 support and add it to the versions * Add data for packetTest for explosion packet. * Add 1.20.4 to version list. * Add fix for 1.20.3+ NBT isntead of strings for chat. Not happy with this but it does work. * Fix linting * Update version.js Remove 1.20.3 since its the same as 1.20.4. (As suggested) * Comment how handleNBTStrings works. * Removed debug console.log * Update README.md * chat packet nbt handling fix, use feature * big endian UUID, add back `text` wrapper * use prismarine-chat in client test * expose _handleNbtComponent * use prismarine-chat exposed processNbtMessage * fix pre-1.20.4 * Update package.json * Update server.js * Update server.js add missing import * update server hello world --------- Co-authored-by: Romain Beaumont Co-authored-by: extremeheat --- docs/API.md | 2 +- docs/README.md | 48 ++++++++++++++----- examples/server/server.js | 8 +++- .../server_helloworld/server_helloworld.js | 29 ++++++++++- package.json | 3 +- src/client/chat.js | 20 ++++---- src/datatypes/uuid.js | 8 +++- src/version.js | 4 +- test/clientTest.js | 16 ++++--- test/common/clientHelpers.js | 19 ++++++++ test/packetTest.js | 4 ++ 11 files changed, 126 insertions(+), 35 deletions(-) diff --git a/docs/API.md b/docs/API.md index 3d6bd3cb7..88067bcaf 100644 --- a/docs/API.md +++ b/docs/API.md @@ -238,7 +238,7 @@ The client's protocol version ### client.version -The client's version +The client's version, as a string ### `packet` event diff --git a/docs/README.md b/docs/README.md index 4b738c8d6..ed5d8753e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2) + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2, 1.20.3 and 1.20.4) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. @@ -118,16 +118,23 @@ const client = mc.createClient({ For a more up to date example, see examples/server/server.js. ```js -const mc = require('minecraft-protocol'); +const mc = require('minecraft-protocol') +const nbt = require('prismarine-nbt') const server = mc.createServer({ 'online-mode': true, // optional encryption: true, // optional host: '0.0.0.0', // optional port: 25565, // optional - version: '1.16.3' -}); + version: '1.20.4' +}) const mcData = require('minecraft-data')(server.version) +function chatText (text) { + return mcData.supportFeature('chatPacketsUseNbtComponents') + ? nbt.comp({ text: nbt.string(text) }) + : JSON.stringify({ text }) +} + server.on('playerJoin', function(client) { const loginPacket = mcData.loginPacket @@ -141,7 +148,7 @@ server.on('playerJoin', function(client) { enableRespawnScreen: true, isDebug: false, isFlat: false - }); + }) client.write('position', { x: 0, @@ -150,18 +157,35 @@ server.on('playerJoin', function(client) { yaw: 0, pitch: 0, flags: 0x00 - }); + }) - const msg = { + const message = { translate: 'chat.type.announcement', - "with": [ + with: [ 'Server', 'Hello, world!' ] - }; - - client.write("chat", { message: JSON.stringify(msg), position: 0, sender: '0' }); -}); + } + if (mcData.supportFeature('signedChat')) { + client.write('player_chat', { + plainMessage: message, + signedChatContent: '', + unsignedChatContent: chatText(message), + type: 0, + senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random + senderName: JSON.stringify({ text: 'me' }), + senderTeam: undefined, + timestamp: Date.now(), + salt: 0n, + signature: mcData.supportFeature('useChatSessions') ? undefined : Buffer.alloc(0), + previousMessages: [], + filterType: 0, + networkName: JSON.stringify({ text: 'me' }) + }) + } else { + client.write('chat', { message: JSON.stringify({ text: message }), position: 0, sender: 'me' }) + } +}) ``` ## Testing diff --git a/examples/server/server.js b/examples/server/server.js index 482531adf..bf639702a 100644 --- a/examples/server/server.js +++ b/examples/server/server.js @@ -1,4 +1,5 @@ const mc = require('minecraft-protocol') +const nbt = require('prismarine-nbt') const options = { motd: 'Vox Industries', @@ -10,6 +11,11 @@ const options = { const server = mc.createServer(options) const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket +function chatText (text) { + return mcData.supportFeature('chatPacketsUseNbtComponents') + ? nbt.comp({ text: nbt.string(text) }) + : JSON.stringify({ text }) +} server.on('playerJoin', function (client) { broadcast(client.username + ' joined the game.') @@ -67,7 +73,7 @@ function sendBroadcastMessage (server, clients, message, sender) { server.writeToClients(clients, 'player_chat', { plainMessage: message, signedChatContent: '', - unsignedChatContent: JSON.stringify({ text: message }), + unsignedChatContent: chatText(message), type: 0, senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random senderName: JSON.stringify({ text: sender }), diff --git a/examples/server_helloworld/server_helloworld.js b/examples/server_helloworld/server_helloworld.js index 0dd0223fb..3391717e2 100644 --- a/examples/server_helloworld/server_helloworld.js +++ b/examples/server_helloworld/server_helloworld.js @@ -8,6 +8,13 @@ const options = { const server = mc.createServer(options) const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket +const nbt = require('prismarine-nbt') + +function chatText (text) { + return mcData.supportFeature('chatPacketsUseNbtComponents') + ? nbt.comp({ text: nbt.string(text) }) + : JSON.stringify({ text }) +} server.on('playerJoin', function (client) { const addr = client.socket.remoteAddress @@ -49,14 +56,32 @@ server.on('playerJoin', function (client) { flags: 0x00 }) - const msg = { + const message = { translate: 'chat.type.announcement', with: [ 'Server', 'Hello, world!' ] } - client.write('chat', { message: JSON.stringify(msg), position: 0, sender: '0' }) + if (mcData.supportFeature('signedChat')) { + client.write('player_chat', { + plainMessage: message, + signedChatContent: '', + unsignedChatContent: chatText(message), + type: 0, + senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random + senderName: JSON.stringify({ text: 'me' }), + senderTeam: undefined, + timestamp: Date.now(), + salt: 0n, + signature: mcData.supportFeature('useChatSessions') ? undefined : Buffer.alloc(0), + previousMessages: [], + filterType: 0, + networkName: JSON.stringify({ text: 'me' }) + }) + } else { + client.write('chat', { message: JSON.stringify({ text: message }), position: 0, sender: 'me' }) + } }) server.on('error', function (error) { diff --git a/package.json b/package.json index 856abf282..ab6974453 100644 --- a/package.json +++ b/package.json @@ -51,11 +51,12 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^3.53.0", + "minecraft-data": "^3.55.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", "prismarine-auth": "^2.2.0", + "prismarine-chat": "^1.10.0", "prismarine-nbt": "^2.5.0", "prismarine-realms": "^1.2.0", "protodef": "^1.8.0", diff --git a/src/client/chat.js b/src/client/chat.js index d35cbab65..5cad9954d 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -1,5 +1,6 @@ const crypto = require('crypto') const concat = require('../transforms/binaryStream').concat +const { processNbtMessage } = require('prismarine-chat') const messageExpireTime = 420000 // 7 minutes (ms) function isFormatted (message) { @@ -25,6 +26,10 @@ module.exports = function (client, options) { // This stores the last n (5 or 20) messages that the player has seen, from unique players if (mcData.supportFeature('chainedChatWithHashing')) client._lastSeenMessages = new LastSeenMessages() else client._lastSeenMessages = new LastSeenMessagesWithInvalidation() + // 1.20.3+ serializes chat components in either NBT or JSON. If the chat is sent as NBT, then the structure read will differ + // from the normal JSON structure, so it needs to be normalized. prismarine-chat processNbtMessage will do that by default + // on a fromNotch call. Since we don't call fromNotch here (done in mineflayer), we manually call processNbtMessage + const processMessage = (msg) => mcData.supportFeature('chatPacketsUseNbtComponents') ? processNbtMessage(msg) : msg // This stores the last 128 inbound (signed) messages for 1.19.3 chat validation client._signatureCache = new SignatureCache() @@ -139,12 +144,11 @@ module.exports = function (client, options) { client.on('profileless_chat', (packet) => { // Profileless chat is parsed as an unsigned player chat message but logged as a system message - client.emit('playerChat', { - formattedMessage: packet.message, + formattedMessage: processMessage(packet.message), type: packet.type, - senderName: packet.name, - targetName: packet.target, + senderName: processMessage(packet.name), + targetName: processMessage(packet.target), verified: false }) @@ -160,7 +164,7 @@ module.exports = function (client, options) { client.on('system_chat', (packet) => { client.emit('systemChat', { positionId: packet.isActionBar ? 2 : 1, - formattedMessage: packet.content + formattedMessage: processMessage(packet.content) }) client._lastChatHistory.push({ @@ -198,11 +202,11 @@ module.exports = function (client, options) { if (verified) client._signatureCache.push(packet.signature) client.emit('playerChat', { plainMessage: packet.plainMessage, - unsignedContent: packet.unsignedChatContent, + unsignedContent: processMessage(packet.unsignedChatContent), type: packet.type, sender: packet.senderUuid, - senderName: packet.networkName, - targetName: packet.networkTargetName, + senderName: processMessage(packet.networkName), + targetName: processMessage(packet.networkTargetName), verified }) diff --git a/src/datatypes/uuid.js b/src/datatypes/uuid.js index 7298230d7..23e9bbd8e 100644 --- a/src/datatypes/uuid.js +++ b/src/datatypes/uuid.js @@ -15,4 +15,10 @@ function nameToMcOfflineUUID (name) { return (new UUID(javaUUID('OfflinePlayer:' + name))).toString() } -module.exports = { nameToMcOfflineUUID } +function fromIntArray (arr) { + const buf = Buffer.alloc(16) + arr.forEach((num, index) => { buf.writeInt32BE(num, index * 4) }) + return buf.toString('hex') +} + +module.exports = { nameToMcOfflineUUID, fromIntArray } diff --git a/src/version.js b/src/version.js index 0bba3cd80..f4707bdf4 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.20.2', - supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2'] + defaultVersion: '1.20.4', + supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4'] } diff --git a/test/clientTest.js b/test/clientTest.js index fc1dc256b..891988b38 100644 --- a/test/clientTest.js +++ b/test/clientTest.js @@ -59,6 +59,7 @@ for (const supportedVersion of mc.supportedVersions) { 'server-port': PORT, motd: 'test1234', 'max-players': 120, + // 'level-type': 'flat', 'use-native-transport': 'false' // java 16 throws errors without this, https://www.spigotmc.org/threads/unable-to-access-address-of-buffer.311602 }, (err) => { if (err) reject(err) @@ -165,21 +166,22 @@ for (const supportedVersion of mc.supportedVersions) { } } else { // 1.19+ - - const message = JSON.parse(data.formattedMessage || JSON.stringify({ text: data.plainMessage })) + console.log('Chat Message', data) + const sender = JSON.parse(data.senderName) + const msgPayload = data.formattedMessage ? JSON.parse(data.formattedMessage) : data.plainMessage + const plainMessage = client.parseMessage(msgPayload).toString() if (chatCount === 1) { - assert.strictEqual(message.text, 'hello everyone; I have logged in.') - const sender = JSON.parse(data.senderName) + assert.strictEqual(plainMessage, 'hello everyone; I have logged in.') assert.deepEqual(sender.clickEvent, { action: 'suggest_command', value: '/tell Player ' }) assert.strictEqual(sender.text, 'Player') } else if (chatCount === 2) { - assert.strictEqual(message.text, 'hello') - const sender = JSON.parse(data.senderName) - assert.strictEqual(sender.text, 'Server') + const plainSender = client.parseMessage(sender).toString() + assert.strictEqual(plainMessage, 'hello') + assert.strictEqual(plainSender, 'Server') wrap.removeListener('line', lineListener) client.end() done() diff --git a/test/common/clientHelpers.js b/test/common/clientHelpers.js index 01253780a..2fb78d2d6 100644 --- a/test/common/clientHelpers.js +++ b/test/common/clientHelpers.js @@ -1,3 +1,4 @@ +const Registry = require('prismarine-registry') module.exports = client => { client.nextMessage = (containing) => { return new Promise((resolve) => { @@ -20,5 +21,23 @@ module.exports = client => { }) } + client.on('login', (packet) => { + client.registry ??= Registry(client.version) + if (packet.dimensionCodec) { + client.registry.loadDimensionCodec(packet.dimensionCodec) + } + }) + client.on('registry_data', (data) => { + client.registry ??= Registry(client.version) + client.registry.loadDimensionCodec(data.codec) + }) + + client.on('playerJoin', () => { + const ChatMessage = require('prismarine-chat')(client.registry || client.version) + client.parseMessage = (comp) => { + return new ChatMessage(comp) + } + }) + return client } diff --git a/test/packetTest.js b/test/packetTest.js index e998ad4ea..893d3ab56 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -208,6 +208,10 @@ const values = { packedChunkPos: { x: 10, z: 12 + }, + particle: { + particleId: 0, + data: null } } From e50b604a60403e7412e5124f29cc31a2c00d66ab Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Mon, 26 Feb 2024 01:20:24 +0100 Subject: [PATCH 124/171] Release 1.47.0 (#1288) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 33408f1d2..5e8aa5449 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.47.0 +* [1.20.3 / 1.20.4 support (#1275)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/1d9a38253a28a515d82fffa13806cb0874c5b36c) (thanks @wgaylord) + ## 1.46.0 * [Ensure `onReady` in client is called once (#1287)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/85a26a52944c89af273bc974380b438073280981) (thanks @extremeheat) * [Acknowledge returning to configuration state if in play state. (#1284)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/092e10c53d33a7b9be52b5cbb67b1e3e55ac2690) (thanks @wgaylord) diff --git a/package.json b/package.json index ab6974453..f705215b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.46.0", + "version": "1.47.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From ccab9fb39681f3ebe0d264e2a3f833aa3c5a1ac7 Mon Sep 17 00:00:00 2001 From: William Gaylord Date: Sun, 17 Mar 2024 09:52:25 -0500 Subject: [PATCH 125/171] Fix handling of disconnect in versionChecking on 1.20.3+. (#1291) * Fix handling of disconnect in versionChecking on 1.20.3+. Also handle newer translate string used in 1.20.2+ * Comments why we ignore PLAY and CONFIGURATION state disconnects, and un-inline an if * Add extra comment on why we are ignoring those states. * Change bitwise or to logical or --- src/client/versionChecking.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/client/versionChecking.js b/src/client/versionChecking.js index 9d27955ae..ab9833fd5 100644 --- a/src/client/versionChecking.js +++ b/src/client/versionChecking.js @@ -1,6 +1,11 @@ +const states = require('../states') + module.exports = function (client, options) { client.on('disconnect', message => { if (!message.reason) { return } + // Prevent the disconnect packet handler in the versionChecking code from triggering on PLAY or CONFIGURATION state disconnects + // Since version checking only happens during that HANDSHAKE / LOGIN state. + if (client.state === states.PLAY || client.state === states.CONFIGURATION) { return } let parsed try { parsed = JSON.parse(message.reason) @@ -11,7 +16,9 @@ module.exports = function (client, options) { let text = parsed.text ? parsed.text : parsed let versionRequired - if (text.translate && text.translate.startsWith('multiplayer.disconnect.outdated_')) { versionRequired = text.with[0] } else { + if (text.translate && (text.translate.startsWith('multiplayer.disconnect.outdated_') || text.translate.startsWith('multiplayer.disconnect.incompatible'))) { + versionRequired = text.with[0] + } else { if (text.extra) text = text.extra[0].text versionRequired = /(?:Outdated client! Please use|Outdated server! I'm still on) (.+)/.exec(text) versionRequired = versionRequired ? versionRequired[1] : null From 495eed56ab230b2615596590064671356d86a2dc Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 22 May 2024 02:12:48 -0400 Subject: [PATCH 126/171] Update doc (#1300) * Update README.md * Update index.d.ts --- docs/README.md | 40 +++++++++++++++++++++------------------- src/index.d.ts | 2 +- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/docs/README.md b/docs/README.md index ed5d8753e..94bf117a4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,7 +22,8 @@ Parse and serialize minecraft packets, plus authentication and encryption. - Encryption - Compression - Both online and offline mode - - Respond to keep-alive packets. + - Respond to keep-alive packets + - Follow DNS service records (SRV) - Ping a server for status * Server - Online/Offline mode @@ -75,29 +76,30 @@ node-minecraft-protocol is pluggable. const mc = require('minecraft-protocol'); const client = mc.createClient({ host: "localhost", // optional - port: 25565, // optional - username: "email@example.com", - password: "12345678", - auth: 'microsoft' // optional; by default uses offline mode, if using a microsoft account, set to 'microsoft' + port: 25565, // set if you need a port that isn't 25565 + username: 'Bot', // username to join as if auth is `offline`, else a unique identifier for this account. Switch if you want to change accounts + // version: false, // only set if you need a specific version or snapshot (ie: "1.8.9" or "1.16.5"), otherwise it's set automatically + // password: '12345678' // set if you want to use password-based auth (may be unreliable). If specified, the `username` must be an email }); -client.on('chat', function(packet) { +client.on('playerChat', function (ev) { // Listen for chat messages and echo them back. - const jsonMsg = JSON.parse(packet.message); - - if (jsonMsg.translate == 'chat.type.announcement' || jsonMsg.translate == 'chat.type.text') { - const username = jsonMsg.with[0].text; - const msg = jsonMsg.with[1]; - - if (username === client.username) return; - - client.write('chat', {message: msg.text}); - } + const content = ev.formattedMessage + ? JSON.parse(ev.formattedMessage) + : ev.unsignedChat + ? JSON.parse(ev.unsignedContent) + : ev.plainMessage + const jsonMsg = JSON.parse(packet.message) + if (ev.senderName === client.username) return + client.chat(JSON.stringify(content)) }); ``` -If the server is in offline mode, you may leave out the `password` option and switch auth to `offline`. -You can also leave out `password` when using a Microsoft account. If provided, password based auth will be attempted first which may fail. *Note:* if using a Microsoft account, your account age must be >= 18 years old. +Set `auth` to `offline` if the server is in offline mode. If `auth` is set to `microsoft`, you will be prompted to login to microsoft.com with a code in your browser. After signing in on your browser, the client will automatically obtain and cache authentication tokens (under your specified username) so you don't have to sign-in again. + +To switch the account, update the supplied username. By default, cached tokens will be stored in your user's .minecraft folder, or if profilesFolder is specified, they'll instead be stored there. For more information on bot options see the [API doc](./API.md). + +Note: SRV records will only be looked up if the port is unspecified or set to 25565 and if the `host` is a valid non-local domain name. ### Client example joining a Realm @@ -125,7 +127,7 @@ const server = mc.createServer({ encryption: true, // optional host: '0.0.0.0', // optional port: 25565, // optional - version: '1.20.4' + version: '1.18' }) const mcData = require('minecraft-data')(server.version) diff --git a/src/index.d.ts b/src/index.d.ts index 0a5821c32..70b86cc4d 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -49,7 +49,7 @@ declare module 'minecraft-protocol' { on(event: 'connect', handler: () => PromiseLike): this on(event: string, handler: (data: any, packetMeta: PacketMeta) => PromiseLike): this on(event: `raw.${string}`, handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this - on(event: 'playerChat', handler: (data: { formattedMessage: string, message: string, type: string, sender: string, senderName: string, senderTeam: string, verified?: boolean }) => PromiseLike): this + on(event: 'playerChat', handler: (data: { formattedMessage: string, plainMessage: string, type: string, sender: string, senderName: string, senderTeam: string, verified?: boolean }) => PromiseLike): this on(event: 'systemChat', handler: (data: { positionId: number, formattedMessage: string }) => PromiseLike): this // Emitted after the player enters the PLAY state and can send and recieve game packets on(event: 'playerJoin', handler: () => void): this From 7057ad979b416192ada235f2f4e3b5eb26af5fa1 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sat, 13 Jul 2024 17:03:33 -0400 Subject: [PATCH 127/171] Fix realms loading issue due to createClient plugin init order (#1303) * Fix realms loading issue due to createClient plugin init order * remove exception on bad parse of disconnect * Update index.d.ts * Improve serverTest check logic try and debug flaky tests --- src/client/versionChecking.js | 1 - src/createClient.js | 25 +++++++---- src/index.d.ts | 15 ++++++- test/serverTest.js | 83 ++++++++++++++++++++++++----------- 4 files changed, 87 insertions(+), 37 deletions(-) diff --git a/src/client/versionChecking.js b/src/client/versionChecking.js index ab9833fd5..418544203 100644 --- a/src/client/versionChecking.js +++ b/src/client/versionChecking.js @@ -10,7 +10,6 @@ module.exports = function (client, options) { try { parsed = JSON.parse(message.reason) } catch (error) { - client.emit('error', error) return } let text = parsed.text ? parsed.text : parsed diff --git a/src/createClient.js b/src/createClient.js index 10cacc070..912e331e6 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -39,17 +39,20 @@ function createClient (options) { tcpDns(client, options) if (options.auth instanceof Function) { options.auth(client, options) + onReady() } else { switch (options.auth) { case 'mojang': console.warn('[deprecated] mojang auth servers no longer accept mojang accounts to login. convert your account.\nhttps://help.minecraft.net/hc/en-us/articles/4403181904525-How-to-Migrate-Your-Mojang-Account-to-a-Microsoft-Account') auth(client, options) + onReady() break case 'microsoft': if (options.realms) { - microsoftAuth.realmAuthenticate(client, options).then(() => microsoftAuth.authenticate(client, options)).catch((err) => client.emit('error', err)) + microsoftAuth.realmAuthenticate(client, options).then(() => microsoftAuth.authenticate(client, options)).catch((err) => client.emit('error', err)).then(onReady) } else { microsoftAuth.authenticate(client, options).catch((err) => client.emit('error', err)) + onReady() } break case 'offline': @@ -58,17 +61,21 @@ function createClient (options) { client.uuid = uuid.nameToMcOfflineUUID(client.username) options.auth = 'offline' options.connect(client) + onReady() break } } - if (options.version === false) autoVersion(client, options) - setProtocol(client, options) - keepalive(client, options) - encrypt(client, options) - play(client, options) - compress(client, options) - pluginChannels(client, options) - versionChecking(client, options) + + function onReady () { + if (options.version === false) autoVersion(client, options) + setProtocol(client, options) + keepalive(client, options) + encrypt(client, options) + play(client, options) + compress(client, options) + pluginChannels(client, options) + versionChecking(client, options) + } return client } diff --git a/src/index.d.ts b/src/index.d.ts index 70b86cc4d..423085259 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -49,7 +49,20 @@ declare module 'minecraft-protocol' { on(event: 'connect', handler: () => PromiseLike): this on(event: string, handler: (data: any, packetMeta: PacketMeta) => PromiseLike): this on(event: `raw.${string}`, handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this - on(event: 'playerChat', handler: (data: { formattedMessage: string, plainMessage: string, type: string, sender: string, senderName: string, senderTeam: string, verified?: boolean }) => PromiseLike): this + on(event: 'playerChat', handler: (data: { + // (JSON string) The chat message preformatted, if done on server side + formattedMessage: string, + // (Plaintext) The chat message without formatting (for example no ` message` ; instead `message`), on version 1.19+ + plainMessage: string, + // (JSON string) Unsigned formatted chat contents. Should only be present when the message is modified and server has chat previews disabled. Only on versions 1.19.0, 1.19.1 and 1.19.2 + unsignedContent?: string, + type: string, + sender: string, + senderName: string, + senderTeam: string, + targetName: string, + verified?: boolean + }) => PromiseLike): this on(event: 'systemChat', handler: (data: { positionId: number, formattedMessage: string }) => PromiseLike): this // Emitted after the player enters the PLAY state and can send and recieve game packets on(event: 'playerJoin', handler: () => void): this diff --git a/test/serverTest.js b/test/serverTest.js index b2f671058..f4105975d 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -126,23 +126,30 @@ for (const supportedVersion of mc.supportedVersions) { version: version.minecraftVersion, port: PORT }) - let count = 2 + let serverClosed, clientClosed server.on('connection', function (client) { client.on('end', function (reason) { assert.strictEqual(reason, 'LoginTimeout') server.close() }) }) - server.on('close', resolve) + server.on('close', () => { + serverClosed = true + console.log('Server closed') + checkFinish() + }) server.on('listening', function () { const client = new mc.Client(false, version.minecraftVersion) - client.on('end', resolve) + client.on('end', () => { + clientClosed = true + console.log('Client closed') + checkFinish() + }) client.connect(PORT, '127.0.0.1') }) - function resolve () { - count -= 1 - if (count <= 0) done() + function checkFinish () { + if (serverClosed && clientClosed) done() } }) @@ -154,14 +161,19 @@ for (const supportedVersion of mc.supportedVersions) { version: version.minecraftVersion, port: PORT }) - let count = 2 + let serverClosed, clientClosed server.on('connection', function (client) { client.on('end', function (reason) { assert.strictEqual(reason, 'KeepAliveTimeout') + console.log('Server client disconnected') server.close() }) }) - server.on('close', resolve) + server.on('close', () => { + serverClosed = true + console.log('Server closed') + checkFinish() + }) server.on('listening', function () { const client = mc.createClient({ username: 'superpants', @@ -170,11 +182,14 @@ for (const supportedVersion of mc.supportedVersions) { keepAlive: false, version: version.minecraftVersion }) - client.on('end', resolve) + client.on('end', () => { + clientClosed = true + console.log('Client closed') + checkFinish() + }) }) - function resolve () { - count -= 1 - if (count <= 0) done() + function checkFinish () { + if (serverClosed && clientClosed) done() } }) @@ -363,27 +378,36 @@ for (const supportedVersion of mc.supportedVersions) { version: version.minecraftVersion, port: PORT }) - let count = 4 + let serverPlayerDisconnected, serverClosed, clientClosed server.on('connection', function (client) { client.on('end', function (reason) { - resolve() + serverPlayerDisconnected = true + console.log('Server player disconnected') + checkFinish() server.close() }) }) - server.on('close', resolve) + server.on('close', () => { + serverClosed = true + console.log('Server closed') + checkFinish() + }) server.on('listening', function () { - resolve() + console.log('Server is listening') const client = mc.createClient({ username: 'lalalal', host: '127.0.0.1', version: version.minecraftVersion, port: PORT }) - client.on('end', resolve) + client.on('end', () => { + clientClosed = true + console.log('Client closed') + checkFinish() + }) }) - function resolve () { - count -= 1 - if (count <= 0) done() + function checkFinish () { + if (serverPlayerDisconnected && clientClosed && serverClosed) done() } }) @@ -393,15 +417,22 @@ for (const supportedVersion of mc.supportedVersions) { version: version.minecraftVersion, port: PORT }) - let count = 2 + let serverPlayerDisconnected, serverClosed server.on('playerJoin', function (client) { + console.log('Server got player join') client.on('end', function (reason) { assert.strictEqual(reason, 'ServerShutdown') - resolve() + serverPlayerDisconnected = true + console.log('Server player disconnected') + checkFinish() }) client.write('login', loginPacket(client, server)) }) - server.on('close', resolve) + server.on('close', () => { + serverClosed = true + console.log('Server closed') + checkFinish() + }) server.on('listening', function () { const client = mc.createClient({ username: 'lalalal', @@ -410,12 +441,12 @@ for (const supportedVersion of mc.supportedVersions) { port: PORT }) client.on('playerJoin', function () { + console.log('Client joined') server.close() }) }) - function resolve () { - count -= 1 - if (count <= 0) done() + function checkFinish () { + if (serverPlayerDisconnected && serverClosed) done() } }) From 9b029e8b6f33d4e8ee1476de6821bad942f1ab6b Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sat, 12 Oct 2024 17:55:36 -0400 Subject: [PATCH 128/171] 1.20.5 (#1309) * 1.20.5 * update examples * Update for 1.20.5 chat_command_signed with seperateSignedChatCommandPacket feature * updates * update java * re-enable packet tests * Update client.js add debug code after decompress * Update client.js * Update ci.yml * Add `arrayWithLengthOffset` type to interpeter * Update minecraft.js * Update compiler-minecraft.js * Update minecraft.js * lint * remote custom ci install * Update package.json * Update packetTest.js add Slot, SlotComponent * Update packetTest.js * Update packetTest.js * Fix lint. * Fix declare_recipes, Slot * Update package.json --------- Co-authored-by: Romain Beaumont --- .github/workflows/ci.yml | 2 +- docs/README.md | 3 +- examples/server/server.js | 1 + examples/server_channel/server_channel.js | 1 + .../server_custom_channel.js | 1 + package.json | 7 +- src/client.js | 1 + src/client/chat.js | 9 +-- src/client/play.js | 8 ++- src/datatypes/compiler-minecraft.js | 53 ++++++++++++++ src/datatypes/minecraft.js | 36 +++++++++- src/server/login.js | 12 +++- src/version.js | 4 +- test/clientTest.js | 57 ++++++++++----- test/common/clientHelpers.js | 2 +- test/packetTest.js | 69 ++++++++++++++++--- test/serverTest.js | 8 ++- 17 files changed, 230 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bf82ba3e..93ef806e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: - name: Setup Java JDK uses: actions/setup-java@v1.4.3 with: - java-version: '17' + java-version: '21' distribution: 'adopt' - name: Install dependencies run: npm install diff --git a/docs/README.md b/docs/README.md index 94bf117a4..94a6cd276 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2, 1.20.3 and 1.20.4) + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5 * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. @@ -142,6 +142,7 @@ server.on('playerJoin', function(client) { client.write('login', { ...loginPacket, + enforceSecureChat: false, entityId: client.id, hashedSeed: [0, 0], maxPlayers: server.maxPlayers, diff --git a/examples/server/server.js b/examples/server/server.js index bf639702a..547c095d5 100644 --- a/examples/server/server.js +++ b/examples/server/server.js @@ -30,6 +30,7 @@ server.on('playerJoin', function (client) { // send init data so client will start rendering world client.write('login', { ...loginPacket, + enforceSecureChat: false, entityId: client.id, isHardcore: false, gameMode: 0, diff --git a/examples/server_channel/server_channel.js b/examples/server_channel/server_channel.js index 72f00cf01..4dc4ff1d9 100644 --- a/examples/server_channel/server_channel.js +++ b/examples/server_channel/server_channel.js @@ -14,6 +14,7 @@ server.on('playerJoin', function (client) { client.write('login', { ...loginPacket, + enforceSecureChat: false, entityId: client.id, isHardcore: false, gameMode: 0, diff --git a/examples/server_custom_channel/server_custom_channel.js b/examples/server_custom_channel/server_custom_channel.js index 69f2b1014..cc2c3b4ae 100644 --- a/examples/server_custom_channel/server_custom_channel.js +++ b/examples/server_custom_channel/server_custom_channel.js @@ -11,6 +11,7 @@ const loginPacket = mcData.loginPacket server.on('playerJoin', function (client) { client.write('login', { ...loginPacket, + enforceSecureChat: false, entityId: client.id, isHardcore: false, gameMode: 0, diff --git a/package.json b/package.json index f705215b8..47b3d027b 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "minecraft-wrap": "^1.2.3", "mocha": "^10.0.0", "power-assert": "^1.0.0", - "standard": "^17.0.0" + "standard": "^17.0.0", + "prismarine-registry": "^1.8.0" }, "dependencies": { "@types/readable-stream": "^4.0.0", @@ -51,7 +52,7 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^3.55.0", + "minecraft-data": "^3.71.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", @@ -59,7 +60,7 @@ "prismarine-chat": "^1.10.0", "prismarine-nbt": "^2.5.0", "prismarine-realms": "^1.2.0", - "protodef": "^1.8.0", + "protodef": "^1.17.0", "readable-stream": "^4.1.0", "uuid-1345": "^1.0.1", "yggdrasil": "^1.4.0" diff --git a/src/client.js b/src/client.js index c89375e32..5b63c2950 100644 --- a/src/client.js +++ b/src/client.js @@ -137,6 +137,7 @@ class Client extends EventEmitter { this.splitter.pipe(this.deserializer) } else { this.serializer.pipe(this.compressor) + if (globalThis.debugNMP) this.decompressor.on('data', (data) => { console.log('DES>', data.toString('hex')) }) this.decompressor.pipe(this.deserializer) } diff --git a/src/client/chat.js b/src/client/chat.js index 5cad9954d..f14269bea 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -176,7 +176,7 @@ module.exports = function (client, options) { }) }) - client.on('message_header', (packet) => { + client.on('message_header', (packet) => { // [1.19.2] updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, packet.messageHash) client._lastChatHistory.push({ @@ -369,13 +369,14 @@ module.exports = function (client, options) { if (message.startsWith('/')) { const command = message.slice(1) - if (mcData.supportFeature('useChatSessions')) { + if (mcData.supportFeature('useChatSessions')) { // 1.19.3+ const { acknowledged, acknowledgements } = getAcknowledgements() - client.write('chat_command', { + const canSign = client.profileKeys && client._session + client.write((mcData.supportFeature('seperateSignedChatCommandPacket') && canSign) ? 'chat_command_signed' : 'chat_command', { command, timestamp: options.timestamp, salt: options.salt, - argumentSignatures: (client.profileKeys && client._session) ? signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements) : [], + argumentSignatures: canSign ? signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements) : [], messageCount: client._lastSeenMessages.pending, acknowledged }) diff --git a/src/client/play.js b/src/client/play.js index 246556e0f..6e06dc152 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -7,11 +7,12 @@ module.exports = function (client, options) { client.on('server_data', (packet) => { client.serverFeatures = { chatPreview: packet.previewsChat, - enforcesSecureChat: packet.enforcesSecureChat + enforcesSecureChat: packet.enforcesSecureChat // in LoginPacket v>=1.20.5 } }) - client.once('login', () => { + client.once('login', (packet) => { + if (packet.enforcesSecureChat) client.serverFeatures.enforcesSecureChat = packet.enforcesSecureChat const mcData = require('minecraft-data')(client.version) if (mcData.supportFeature('useChatSessions') && client.profileKeys && client.cipher && client.session.selectedProfile.id === client.uuid.replace(/-/g, '')) { client._session = { @@ -52,6 +53,9 @@ module.exports = function (client, options) { client.write('configuration_acknowledged', {}) } client.state = states.CONFIGURATION + client.on('select_known_packs', () => { + client.write('select_known_packs', { packs: [] }) + }) // Server should send finish_configuration on its own right after sending the client a dimension codec // for login (that has data about world height, world gen, etc) after getting a login success from client client.once('finish_configuration', () => { diff --git a/src/datatypes/compiler-minecraft.js b/src/datatypes/compiler-minecraft.js index 89cacde04..ba4db3e70 100644 --- a/src/datatypes/compiler-minecraft.js +++ b/src/datatypes/compiler-minecraft.js @@ -40,6 +40,27 @@ module.exports = { code += ' if ((item & 128) === 0) return { value: data, size: cursor - offset }\n' code += '}' return compiler.wrapCode(code) + }], + arrayWithLengthOffset: ['parametrizable', (compiler, array) => { + let code = '' + if (array.countType) { + code += 'const { value: count, size: countSize } = ' + compiler.callType(array.countType) + '\n' + } else if (array.count) { + code += 'const count = ' + array.count + '\n' + code += 'const countSize = 0\n' + } else { + throw new Error('Array must contain either count or countType') + } + code += 'if (count > 0xffffff) throw new Error("array size is abnormally large, not reading: " + count)\n' + code += 'const data = []\n' + code += 'let size = countSize\n' + code += `for (let i = 0; i < count + ${array.lengthOffset}; i++) {\n` + code += ' const elem = ' + compiler.callType(array.type, 'offset + size') + '\n' + code += ' data.push(elem.value)\n' + code += ' size += elem.size\n' + code += '}\n' + code += 'return { value: data, size }' + return compiler.wrapCode(code) }] }, Write: { @@ -72,6 +93,19 @@ module.exports = { code += '}\n' code += 'return offset' return compiler.wrapCode(code) + }], + arrayWithLengthOffset: ['parametrizable', (compiler, array) => { + let code = '' + if (array.countType) { + code += 'offset = ' + compiler.callType('value.length', array.countType) + '\n' + } else if (array.count === null) { + throw new Error('Array must contain either count or countType') + } + code += 'for (let i = 0; i < value.length; i++) {\n' + code += ' offset = ' + compiler.callType('value[i]', array.type) + '\n' + code += '}\n' + code += 'return offset' + return compiler.wrapCode(code) }] }, SizeOf: { @@ -96,6 +130,25 @@ module.exports = { code += '}\n' code += 'return size' return compiler.wrapCode(code) + }], + arrayWithLengthOffset: ['parametrizable', (compiler, array) => { + let code = '' + if (array.countType) { + code += 'let size = ' + compiler.callType('value.length', array.countType) + '\n' + } else if (array.count) { + code += 'let size = 0\n' + } else { + throw new Error('Array must contain either count or countType') + } + if (!isNaN(compiler.callType('value[i]', array.type))) { + code += 'size += value.length * ' + compiler.callType('value[i]', array.type) + '\n' + } else { + code += 'for (let i = 0; i < value.length; i++) {\n' + code += ' size += ' + compiler.callType('value[i]', array.type) + '\n' + code += '}\n' + } + code += 'return size' + return compiler.wrapCode(code) }] } } diff --git a/src/datatypes/minecraft.js b/src/datatypes/minecraft.js index 09e90355a..39f5d867c 100644 --- a/src/datatypes/minecraft.js +++ b/src/datatypes/minecraft.js @@ -11,7 +11,8 @@ module.exports = { compressedNbt: [readCompressedNbt, writeCompressedNbt, sizeOfCompressedNbt], restBuffer: [readRestBuffer, writeRestBuffer, sizeOfRestBuffer], entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata], - topBitSetTerminatedArray: [readTopBitSetTerminatedArray, writeTopBitSetTerminatedArray, sizeOfTopBitSetTerminatedArray] + topBitSetTerminatedArray: [readTopBitSetTerminatedArray, writeTopBitSetTerminatedArray, sizeOfTopBitSetTerminatedArray], + arrayWithLengthOffset: [readArrayWithLengthOffset, writeArrayWithLengthOffset, sizeOfArrayWithLengthOffset] } const PartialReadError = require('protodef').utils.PartialReadError @@ -180,3 +181,36 @@ function sizeOfTopBitSetTerminatedArray (value, { type }) { } return size } + +// +const { getCount, sendCount, calcCount, tryDoc } = require('protodef/src/utils') + +function readArrayWithLengthOffset (buffer, offset, typeArgs, rootNode) { + const results = { + value: [], + size: 0 + } + let value + let { count, size } = getCount.call(this, buffer, offset, typeArgs, rootNode) + offset += size + results.size += size + for (let i = 0; i < count + typeArgs.lengthOffset; i++) { + ({ size, value } = tryDoc(() => this.read(buffer, offset, typeArgs.type, rootNode), i)) + results.size += size + offset += size + results.value.push(value) + } + return results +} + +// no changes +function writeArrayWithLengthOffset (value, buffer, offset, typeArgs, rootNode) { + offset = sendCount.call(this, value.length, buffer, offset, typeArgs, rootNode) + return value.reduce((offset, v, index) => tryDoc(() => this.write(v, buffer, offset, typeArgs.type, rootNode), index), offset) +} + +function sizeOfArrayWithLengthOffset (value, typeArgs, rootNode) { + let size = calcCount.call(this, value.length, typeArgs, rootNode) + size = value.reduce((size, v, index) => tryDoc(() => size + this.sizeOf(v, typeArgs.type, rootNode), index), size) + return size + typeArgs +} diff --git a/src/server/login.js b/src/server/login.js index 68dc27a86..ad143d383 100644 --- a/src/server/login.js +++ b/src/server/login.js @@ -193,7 +193,8 @@ module.exports = function (client, server, options) { if (client.supportFeature('chainedChatWithHashing')) { // 1.19.1+ client.write('server_data', { previewsChat: options.enableChatPreview, - enforceSecureProfile: options.enforceSecureProfile + // Note: in 1.20.5+ user must send this with `login` + enforcesSecureChat: options.enforceSecureProfile }) } @@ -211,7 +212,14 @@ module.exports = function (client, server, options) { function onClientLoginAck () { client.state = states.CONFIGURATION - client.write('registry_data', { codec: options.registryCodec || {} }) + if (client.supportFeature('segmentedRegistryCodecData')) { + for (const key in options.registryCodec) { + const entry = options.registryCodec[key] + client.write('registry_data', entry) + } + } else { + client.write('registry_data', { codec: options.registryCodec || {} }) + } client.once('finish_configuration', () => { client.state = states.PLAY server.emit('playerJoin', client) diff --git a/src/version.js b/src/version.js index f4707bdf4..b95f01d51 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.20.4', - supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4'] + defaultVersion: '1.20.5', + supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.5'] } diff --git a/test/clientTest.js b/test/clientTest.js index 891988b38..67eb8ece6 100644 --- a/test/clientTest.js +++ b/test/clientTest.js @@ -107,37 +107,62 @@ for (const supportedVersion of mc.supportedVersions) { auth: 'offline' })) client.on('error', err => done(err)) - const lineListener = function (line) { - const match = line.match(/\[Server thread\/INFO\]: (?:\[Not Secure\] )?<(.+?)> (.+)/) - if (!match) return - assert.strictEqual(match[1], 'Player') - assert.strictEqual(match[2], 'hello everyone; I have logged in.') - wrap.writeServer('say hello\n') - wrap.off('line', lineListener) - } - wrap.on('line', lineListener) - let chatCount = 0 - client.on('login', function (packet) { - assert.strictEqual(packet.gameMode, 0) - client.chat('hello everyone; I have logged in.') + + client.on('state', (state) => { + console.log('Client now in state', state) }) - // Dump some data for easier debugging + + // ** Dump some server data ** + fs.rmSync(MC_SERVER_DIR + '_registry_data.json', { force: true }) client.on('raw.registry_data', (buffer) => { fs.writeFileSync(MC_SERVER_DIR + '_registry_data.bin', buffer) }) client.on('registry_data', (json) => { - fs.writeFileSync(MC_SERVER_DIR + '_registry_data.json', JSON.stringify(json)) + if (json.codec) { // Pre 1.20.5, codec is 1 json + fs.writeFileSync(MC_SERVER_DIR + '_registry_data.json', JSON.stringify(json)) + } else { // 1.20.5+, codec is many nbt's each with their own ids, merge them + let currentData = {} + if (fs.existsSync(MC_SERVER_DIR + '_registry_data.json')) { + currentData = JSON.parse(fs.readFileSync(MC_SERVER_DIR + '_registry_data.json', 'utf8')) + } + currentData[json.id] = json + fs.writeFileSync(MC_SERVER_DIR + '_registry_data.json', JSON.stringify(currentData)) + } + console.log('Wrote registry data') }) client.on('login', (packet) => { fs.writeFileSync(MC_SERVER_DIR + '_login.json', JSON.stringify(packet)) if (fs.existsSync(MC_SERVER_DIR + '_registry_data.json')) { // generate a loginPacket.json for minecraft-data + const codec = JSON.parse(fs.readFileSync(MC_SERVER_DIR + '_registry_data.json')) fs.writeFileSync(MC_SERVER_DIR + '_loginPacket.json', JSON.stringify({ ...packet, - dimensionCodec: JSON.parse(fs.readFileSync(MC_SERVER_DIR + '_registry_data.json')).codec + dimensionCodec: codec.codec || codec }, null, 2)) + console.log('Wrote loginPacket.json') } }) + // ** End dumping code ** + + const lineListener = function (line) { + const match = line.match(/\[Server thread\/INFO\]: (?:\[Not Secure\] )?<(.+?)> (.+)/) + if (!match) return + assert.strictEqual(match[1], 'Player') + assert.strictEqual(match[2], 'hello everyone; I have logged in.') + wrap.writeServer('say hello\n') + wrap.off('line', lineListener) + } + wrap.on('line', lineListener) + let chatCount = 0 + + client.on('login', function (packet) { + if (packet.worldState) { // 1.20.5+ + assert.strictEqual(packet.worldState.gamemode, 'survival') + } else { + assert.strictEqual(packet.gameMode, 0) + } + client.chat('hello everyone; I have logged in.') + }) client.on('playerChat', function (data) { chatCount += 1 assert.ok(chatCount <= 2) diff --git a/test/common/clientHelpers.js b/test/common/clientHelpers.js index 2fb78d2d6..797b2325b 100644 --- a/test/common/clientHelpers.js +++ b/test/common/clientHelpers.js @@ -29,7 +29,7 @@ module.exports = client => { }) client.on('registry_data', (data) => { client.registry ??= Registry(client.version) - client.registry.loadDimensionCodec(data.codec) + client.registry.loadDimensionCodec(data.codec || data) }) client.on('playerJoin', () => { diff --git a/test/packetTest.js b/test/packetTest.js index 893d3ab56..da03037d3 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -18,7 +18,6 @@ function evalCount (count, fields) { const slotValue = { present: true, blockId: 5, - itemCount: 56, itemDamage: 2, nbtData: { type: 'compound', @@ -32,7 +31,14 @@ const slotValue = { test6: { type: 'compound', value: { test: { type: 'int', value: 4 } } }, test7: { type: 'intArray', value: [12, 42] } } - } + }, + // 1.20.5 + itemCount: 1, + itemId: 1111, + addedComponentCount: 0, + removedComponentCount: 0, + components: [], + removeComponents: [] } const nbtValue = { @@ -49,6 +55,24 @@ const nbtValue = { } } +function getFixedPacketPayload (version, packetName) { + if (packetName === 'declare_recipes') { + if (version['>=']('1.20.5')) { + return { + recipes: [ + { + name: 'minecraft:crafting_decorated_pot', + type: 'minecraft:crafting_decorated_pot', + data: { + category: 0 + } + } + ] + } + } + } +} + const values = { i32: 123456, i16: -123, @@ -57,6 +81,7 @@ const values = { varlong: -20, i8: -10, u8: 8, + ByteArray: [], string: 'hi hi this is my client string', buffer: function (typeArgs, context) { let count @@ -124,6 +149,11 @@ const values = { f64: 99999.2222, f32: -333.444, slot: slotValue, + Slot: slotValue, + SlotComponent: { + type: 'hide_tooltip' + }, + SlotComponentType: 0, nbt: nbtValue, optionalNbt: nbtValue, compressedNbt: nbtValue, @@ -161,8 +191,8 @@ const values = { const i = typeArgs.fields[getField(typeArgs.compareTo, context)] if (i === undefined) { if (typeArgs.default === undefined) { - throw new Error("couldn't find the field " + typeArgs.compareTo + - ' of the compareTo and the default is not defined') + typeArgs.default = 'void' + // throw new Error("couldn't find the field " + typeArgs.compareTo + ' of the compareTo and the default is not defined') } return getValue(typeArgs.default, context) } else { return getValue(i, context) } @@ -177,6 +207,7 @@ const values = { }) return results }, + mapper: '', tags: [{ tagName: 'hi', entries: [1, 2, 3, 4, 5] }], ingredient: [slotValue], particleData: null, @@ -212,6 +243,20 @@ const values = { particle: { particleId: 0, data: null + }, + Particle: {}, + SpawnInfo: { + dimension: 0, + name: 'minecraft:overworld', + hashedSeed: [ + 572061085, + 1191958278 + ], + gamemode: 'survival', + previousGamemode: 255, + isDebug: false, + isFlat: false, + portalCooldown: 0 } } @@ -274,24 +319,28 @@ for (const supportedVersion of mc.supportedVersions) { .forEach(function (packetName) { packetInfo = packets[state][direction].types[packetName] packetInfo = packetInfo || null + if (packetName.includes('bundle_delimiter')) return // not a real packet + if (['packet_set_projectile_power', 'packet_debug_sample_subscription'].includes(packetName)) return it(state + ',' + (direction === 'toServer' ? 'Server' : 'Client') + 'Bound,' + packetName, - callTestPacket(packetName.substr(7), packetInfo, state, direction === 'toServer')) + callTestPacket(mcData, packetName.substr(7), packetInfo, state, direction === 'toServer')) }) }) }) - function callTestPacket (packetName, packetInfo, state, toServer) { + function callTestPacket (mcData, packetName, packetInfo, state, toServer) { return function (done) { client.state = state serverClient.state = state - testPacket(packetName, packetInfo, state, toServer, done) + testPacket(mcData, packetName, packetInfo, state, toServer, done) } } - function testPacket (packetName, packetInfo, state, toServer, done) { + function testPacket (mcData, packetName, packetInfo, state, toServer, done) { // empty object uses default values - const packet = getValue(packetInfo, {}) + const packet = getFixedPacketPayload(mcData.version, packetName) || getValue(packetInfo, {}) if (toServer) { + console.log('Writing to server', packetName, JSON.stringify(packet)) serverClient.once(packetName, function (receivedPacket) { + console.log('Recv', packetName) try { assertPacketsMatch(packet, receivedPacket) } catch (e) { @@ -302,7 +351,9 @@ for (const supportedVersion of mc.supportedVersions) { }) client.write(packetName, packet) } else { + console.log('Writing to client', packetName, JSON.stringify(packet)) client.once(packetName, function (receivedPacket) { + console.log('Recv', packetName) assertPacketsMatch(packet, receivedPacket) done() }) diff --git a/test/serverTest.js b/test/serverTest.js index f4105975d..30ed0b0f4 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -30,6 +30,9 @@ for (const supportedVersion of mc.supportedVersions) { const version = mcData.version const loginPacket = (client, server) => { + if (mcData.loginPacket) { + return mcData.loginPacket + } return { // 1.7 entityId: client.id, @@ -67,7 +70,9 @@ for (const supportedVersion of mc.supportedVersions) { value: {} }, worldType: 'minecraft:overworld', - death: undefined + death: undefined, + // 1.20.5 + enforceSecureChat: false // more to be added } } @@ -342,7 +347,6 @@ for (const supportedVersion of mc.supportedVersions) { player1.on('login', async function (packet) { console.log('ChatTest: Player 1 has joined') - assert.strictEqual(packet.gameMode, 1) const player2 = applyClientHelpers(mc.createClient({ username: 'player2', host: '127.0.0.1', From 81d7c71c9359bc498584b51112c23b17c491c4f4 Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Sun, 13 Oct 2024 00:00:06 +0200 Subject: [PATCH 129/171] Release 1.48.0 (#1336) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 5e8aa5449..47b964914 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,11 @@ # History +## 1.48.0 +* [1.20.5 (#1309)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/9b029e8b6f33d4e8ee1476de6821bad942f1ab6b) (thanks @extremeheat) +* [Fix realms loading issue due to createClient plugin init order (#1303)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/7057ad979b416192ada235f2f4e3b5eb26af5fa1) (thanks @extremeheat) +* [Update doc (#1300)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/495eed56ab230b2615596590064671356d86a2dc) (thanks @extremeheat) +* [Fix handling of disconnect in versionChecking on 1.20.3+. (#1291)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/ccab9fb39681f3ebe0d264e2a3f833aa3c5a1ac7) (thanks @wgaylord) + ## 1.47.0 * [1.20.3 / 1.20.4 support (#1275)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/1d9a38253a28a515d82fffa13806cb0874c5b36c) (thanks @wgaylord) diff --git a/package.json b/package.json index 47b3d027b..609daf7ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.47.0", + "version": "1.48.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 0b0012d60f0f1648be5ff705e7694bb1cd4ec37c Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sun, 13 Oct 2024 01:08:18 +0200 Subject: [PATCH 130/171] support 1.20.6 (#1338) * support 1.20.6 * change default version * bump mcdata * update mcdata --- docs/README.md | 2 +- package.json | 2 +- src/version.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/README.md b/docs/README.md index 94a6cd276..68fdd4bb9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5 + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4, 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. diff --git a/package.json b/package.json index 609daf7ad..6b06d3795 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^3.71.0", + "minecraft-data": "^3.75.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", diff --git a/src/version.js b/src/version.js index b95f01d51..1bfcd27a5 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.20.5', - supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.5'] + defaultVersion: '1.20.6', + supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.6'] } From 11e8594bb43dff4fe89f84462ed37d593ee17380 Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Sun, 13 Oct 2024 01:09:14 +0200 Subject: [PATCH 131/171] Release 1.49.0 (#1339) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 47b964914..b07d2b132 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.49.0 +* [support 1.20.6 (#1338)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/0b0012d60f0f1648be5ff705e7694bb1cd4ec37c) (thanks @rom1504) + ## 1.48.0 * [1.20.5 (#1309)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/9b029e8b6f33d4e8ee1476de6821bad942f1ab6b) (thanks @extremeheat) * [Fix realms loading issue due to createClient plugin init order (#1303)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/7057ad979b416192ada235f2f4e3b5eb26af5fa1) (thanks @extremeheat) diff --git a/package.json b/package.json index 6b06d3795..b79780747 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.48.0", + "version": "1.49.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 5bebac36620d8f8ec256d19483e20e643d63de2a Mon Sep 17 00:00:00 2001 From: Grooble Date: Sat, 26 Oct 2024 23:52:46 +0200 Subject: [PATCH 132/171] 1.21 Support (#1342) * Update default version * Update README * 1.21.1 * Update version.js * Update ci.yml * Update version.js * add values for vec2f and ChatTypes * fix lint * fix server tests * fix lint * update mcdata * remove debug install --------- Co-authored-by: Romain Beaumont --- docs/README.md | 2 +- package.json | 2 +- src/version.js | 4 ++-- test/packetTest.js | 6 ++++++ test/serverTest.js | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/README.md b/docs/README.md index 68fdd4bb9..97360021b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4, 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6) + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4, 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21.1) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. diff --git a/package.json b/package.json index b79780747..7904916be 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^3.75.0", + "minecraft-data": "^3.78.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", diff --git a/src/version.js b/src/version.js index 1bfcd27a5..327c242bd 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.20.6', - supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.6'] + defaultVersion: '1.21.1', + supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.6', '1.21.1'] } diff --git a/test/packetTest.js b/test/packetTest.js index da03037d3..534d28edd 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -135,6 +135,9 @@ const values = { delete results['..'] return results }, + vec2f: { + x: 0, y: 0 + }, vec3f: { x: 0, y: 0, z: 0 }, @@ -153,6 +156,9 @@ const values = { SlotComponent: { type: 'hide_tooltip' }, + ChatTypes: { + registryIndex: 1 + }, SlotComponentType: 0, nbt: nbtValue, optionalNbt: nbtValue, diff --git a/test/serverTest.js b/test/serverTest.js index 30ed0b0f4..5837e7656 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -83,7 +83,7 @@ for (const supportedVersion of mc.supportedVersions) { plainMessage: message, signedChatContent: '', unsignedChatContent: JSON.stringify({ text: message }), - type: 0, + type: mcData.supportFeature('incrementedChatType') ? { registryIndex: 1 } : 0, senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random senderName: JSON.stringify({ text: sender }), senderTeam: undefined, From 89de052d44c2e5d667b3c733977b28705891fa87 Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Sat, 26 Oct 2024 23:54:20 +0200 Subject: [PATCH 133/171] Release 1.50.0 (#1346) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index b07d2b132..cc315a389 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.50.0 +* [1.21 Support (#1342)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/5bebac36620d8f8ec256d19483e20e643d63de2a) (thanks @GroobleDierne) + ## 1.49.0 * [support 1.20.6 (#1338)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/0b0012d60f0f1648be5ff705e7694bb1cd4ec37c) (thanks @rom1504) diff --git a/package.json b/package.json index 7904916be..8d3b436e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.49.0", + "version": "1.50.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 590dc33fed2100e77ef58e7db716dfc45eb61159 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 27 Oct 2024 21:29:40 +0100 Subject: [PATCH 134/171] Bump @types/node from 20.16.15 to 22.7.9 (#1345) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.16.15 to 22.7.9. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8d3b436e3..2eee53d5a 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ }, "browser": "src/browser.js", "devDependencies": { - "@types/node": "^20.2.1", + "@types/node": "^22.7.9", "espower-loader": "^1.0.0", "intelli-espower-loader": "^1.0.0", "minecraft-packets": "^1.1.5", From 2224d824065908e910520dfa8ea9f3f3ade242e4 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Wed, 4 Dec 2024 21:21:43 +0100 Subject: [PATCH 135/171] support 1.21.3 (#1347) * support 1.21.3 * Add bitflags, registryEntryHolder and registryEntryHolderSet types * Fix spacing in compiler types * Update compiler-minecraft.js * Fix registryEntryHolderSet read code (#1351) * Update ci.yml * Fix test for 1.21.3 (#1353) * Remove debug logging * Fix benchmark tests for 1.21.3 * Start updating packetTest for 1.21.3 * Update packetTest.js with new types * Fix minecraft-compiler * Speedup tests by setting world type to flat and disabling structures. * Didn't mean to commit that --------- Co-authored-by: extremeheat Co-authored-by: Grooble --- docs/README.md | 2 +- src/datatypes/compiler-minecraft.js | 159 +++++++++++++++++++++++++++- src/version.js | 2 +- test/benchmark.js | 17 +-- test/clientTest.js | 4 +- test/packetTest.js | 78 +++++++++++++- 6 files changed, 247 insertions(+), 15 deletions(-) diff --git a/docs/README.md b/docs/README.md index 97360021b..f8cb78a32 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4, 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21.1) + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6), 1.21 (1.21, 1.21.1, 1.21.3) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. diff --git a/src/datatypes/compiler-minecraft.js b/src/datatypes/compiler-minecraft.js index ba4db3e70..864b269f0 100644 --- a/src/datatypes/compiler-minecraft.js +++ b/src/datatypes/compiler-minecraft.js @@ -1,3 +1,4 @@ +/* eslint-disable no-return-assign */ const UUID = require('uuid-1345') const minecraft = require('./minecraft') @@ -41,7 +42,7 @@ module.exports = { code += '}' return compiler.wrapCode(code) }], - arrayWithLengthOffset: ['parametrizable', (compiler, array) => { + arrayWithLengthOffset: ['parametrizable', (compiler, array) => { // TODO: remove let code = '' if (array.countType) { code += 'const { value: count, size: countSize } = ' + compiler.callType(array.countType) + '\n' @@ -61,6 +62,56 @@ module.exports = { code += '}\n' code += 'return { value: data, size }' return compiler.wrapCode(code) + }], + bitflags: ['parametrizable', (compiler, { type, flags, shift, big }) => { + let fstr = JSON.stringify(flags) + if (Array.isArray(flags)) { + fstr = '{' + for (const [k, v] of Object.entries(flags)) fstr += `"${v}": ${big ? (1n << BigInt(k)) : (1 << k)}` + (big ? 'n,' : ',') + fstr += '}' + } else if (shift) { + fstr = '{' + for (const key in flags) fstr += `"${key}": ${1 << flags[key]},` + fstr += '}' + } + return compiler.wrapCode(` + const { value: _value, size } = ${compiler.callType(type, 'offset')} + const value = { _value } + const flags = ${fstr} + for (const key in flags) { + value[key] = (_value & flags[key]) == flags[key] + } + return { value, size } + `.trim()) + }], + registryEntryHolder: ['parametrizable', (compiler, opts) => { + return compiler.wrapCode(` +const { value: n, size: nSize } = ${compiler.callType('varint')} +if (n !== 0) { + return { value: { ${opts.baseName}: n - 1 }, size: nSize } +} else { + const holder = ${compiler.callType(opts.otherwise.type)} + return { value: { ${opts.otherwise.name}: holder.data }, size: nSize + holder.size } +} + `.trim()) + }], + registryEntryHolderSet: ['parametrizable', (compiler, opts) => { + return compiler.wrapCode(` + const { value: n, size: nSize } = ${compiler.callType('varint')} + if (n === 0) { + const base = ${compiler.callType(opts.base.type, 'offset + nSize')} + return { value: { ${opts.base.name}: base.value }, size: base.size + nSize } + } else { + const set = [] + let accSize = nSize + for (let i = 0; i < n - 1; i++) { + const entry = ${compiler.callType(opts.otherwise.type, 'offset + accSize')} + set.push(entry.value) + accSize += entry.size + } + return { value: { ${opts.otherwise.name}: set }, size: accSize } + } + `.trim()) }] }, Write: { @@ -106,6 +157,58 @@ module.exports = { code += '}\n' code += 'return offset' return compiler.wrapCode(code) + }], + bitflags: ['parametrizable', (compiler, { type, flags, shift, big }) => { + let fstr = JSON.stringify(flags) + if (Array.isArray(flags)) { + fstr = '{' + for (const [k, v] of Object.entries(flags)) fstr += `"${v}": ${big ? (1n << BigInt(k)) : (1 << k)}` + (big ? 'n,' : ',') + fstr += '}' + } else if (shift) { + fstr = '{' + for (const key in flags) fstr += `"${key}": ${1 << flags[key]},` + fstr += '}' + } + return compiler.wrapCode(` + const flags = ${fstr} + let val = value._value ${big ? '|| 0n' : ''} + for (const key in flags) { + if (value[key]) val |= flags[key] + } + return (ctx.${type})(val, buffer, offset) + `.trim()) + }], + registryEntryHolder: ['parametrizable', (compiler, opts) => { + const baseName = `value.${opts.baseName}` + const otherwiseName = `value.${opts.otherwise.name}` + return compiler.wrapCode(` +if (${baseName}) { + offset = ${compiler.callType(`${baseName} + 1`, 'varint')} +} else if (${otherwiseName}) { + offset = ${compiler.callType(`${otherwiseName}`, opts.otherwise.type)} +} else { + throw new Error('registryEntryHolder type requires "${baseName}" or "${otherwiseName}" fields to be set') +} +return offset + `.trim()) + }], + registryEntryHolderSet: ['parametrizable', (compiler, opts) => { + const baseName = `value.${opts.base.name}` + const otherwiseName = `value.${opts.otherwise.name}` + return compiler.wrapCode(` +if (${baseName}) { + offset = ${compiler.callType(0, 'varint')} + offset = ${compiler.callType(`${baseName}`, opts.base.type)} +} else if (${otherwiseName}) { + offset = ${compiler.callType(`${otherwiseName}.length + 1`, 'varint')} + for (let i = 0; i < ${otherwiseName}.length; i++) { + offset = ${compiler.callType(`${otherwiseName}[i]`, opts.otherwise.type)} + } +} else { + throw new Error('registryEntryHolder type requires "${opts.base.name}" or "${opts.otherwise.name}" fields to be set') +} +return offset + `.trim()) }] }, SizeOf: { @@ -149,6 +252,60 @@ module.exports = { } code += 'return size' return compiler.wrapCode(code) + }], + bitflags: ['parametrizable', (compiler, { type, flags, shift, big }) => { + let fstr = JSON.stringify(flags) + if (Array.isArray(flags)) { + fstr = '{' + for (const [k, v] of Object.entries(flags)) fstr += `"${v}": ${big ? (1n << BigInt(k)) : (1 << k)}` + (big ? 'n,' : ',') + fstr += '}' + } else if (shift) { + fstr = '{' + for (const key in flags) fstr += `"${key}": ${1 << flags[key]},` + fstr += '}' + } + return compiler.wrapCode(` + const flags = ${fstr} + let val = value._value ${big ? '|| 0n' : ''} + for (const key in flags) { + if (value[key]) val |= flags[key] + } + return (ctx.${type})(val) + `.trim()) + }], + registryEntryHolder: ['parametrizable', (compiler, opts) => { + const baseName = `value.${opts.baseName}` + const otherwiseName = `value.${opts.otherwise.name}` + return compiler.wrapCode(` +let size = 0 +if (${baseName}) { + size += ${compiler.callType(`${baseName} + 1`, 'varint')} +} else if (${otherwiseName}) { + size += ${compiler.callType(`${otherwiseName}`, opts.otherwise.type)} +} else { + throw new Error('registryEntryHolder type requires "${baseName}" or "${otherwiseName}" fields to be set') +} +return size + `.trim()) + }], + registryEntryHolderSet: ['parametrizable', (compiler, opts) => { + const baseName = `value.${opts.base.name}` + const otherwiseName = `value.${opts.otherwise.name}` + return compiler.wrapCode(` +let size = 0 +if (${baseName}) { + size += ${compiler.callType(0, 'varint')} + size += ${compiler.callType(`${baseName}`, opts.base.type)} +} else if (${otherwiseName}) { + size += ${compiler.callType(`${otherwiseName}.length + 1`, 'varint')} + for (let i = 0; i < ${otherwiseName}.length; i++) { + size += ${compiler.callType(`${otherwiseName}[i]`, opts.otherwise.type)} + } +} else { + throw new Error('registryEntryHolder type requires "${opts.base.name}" or "${opts.otherwise.name}" fields to be set') +} +return size + `.trim()) }] } } diff --git a/src/version.js b/src/version.js index 327c242bd..29d71e768 100644 --- a/src/version.js +++ b/src/version.js @@ -2,5 +2,5 @@ module.exports = { defaultVersion: '1.21.1', - supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.6', '1.21.1'] + supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.6', '1.21.1', '1.21.3'] } diff --git a/test/benchmark.js b/test/benchmark.js index a4610371d..1d24cd754 100644 --- a/test/benchmark.js +++ b/test/benchmark.js @@ -5,17 +5,18 @@ const ITERATIONS = 10000 const mc = require('../') const states = mc.states -const testDataWrite = [ - { name: 'keep_alive', params: { keepAliveId: 957759560 } }, - // TODO: 1.19+ `chat` -> `player_chat` feature toggle - // { name: 'chat', params: { message: ' Hello World!' } }, - { name: 'position_look', params: { x: 6.5, y: 65.62, stance: 67.24, z: 7.5, yaw: 0, pitch: 0, onGround: true } } - // TODO: add more packets for better quality data -] - for (const supportedVersion of mc.supportedVersions) { const mcData = require('minecraft-data')(supportedVersion) const version = mcData.version + const positionFlags = mcData.isNewerOrEqualTo('1.21.3') ? { flags: { onGround: true, hasHorizontalCollision: false } } : { onGround: true } + const testDataWrite = [ + { name: 'keep_alive', params: { keepAliveId: 957759560 } }, + // TODO: 1.19+ `chat` -> `player_chat` feature toggle + // { name: 'chat', params: { message: ' Hello World!' } }, + { name: 'position_look', params: { x: 6.5, y: 65.62, stance: 67.24, z: 7.5, yaw: 0, pitch: 0, ...positionFlags } } + // TODO: add more packets for better quality data + ] + describe('benchmark ' + supportedVersion + 'v', function () { this.timeout(60 * 1000) const inputData = [] diff --git a/test/clientTest.js b/test/clientTest.js index 67eb8ece6..ef46d3462 100644 --- a/test/clientTest.js +++ b/test/clientTest.js @@ -59,7 +59,8 @@ for (const supportedVersion of mc.supportedVersions) { 'server-port': PORT, motd: 'test1234', 'max-players': 120, - // 'level-type': 'flat', + 'level-type': 'flat', + 'generate-structures': 'false', // 12m 'use-native-transport': 'false' // java 16 throws errors without this, https://www.spigotmc.org/threads/unable-to-access-address-of-buffer.311602 }, (err) => { if (err) reject(err) @@ -191,7 +192,6 @@ for (const supportedVersion of mc.supportedVersions) { } } else { // 1.19+ - console.log('Chat Message', data) const sender = JSON.parse(data.senderName) const msgPayload = data.formattedMessage ? JSON.parse(data.formattedMessage) : data.plainMessage const plainMessage = client.parseMessage(msgPayload).toString() diff --git a/test/packetTest.js b/test/packetTest.js index 534d28edd..7f89b95f5 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -57,7 +57,32 @@ const nbtValue = { function getFixedPacketPayload (version, packetName) { if (packetName === 'declare_recipes') { - if (version['>=']('1.20.5')) { + if (version['>=']('1.21.3')) { + return { + recipes: [ + { + name: 'minecraft:campfire_input', + items: [ + 903, + 976 + ] + } + ], + stoneCutterRecipes: [ + { + input: { + ids: [ + 6 + ] + }, + slotDisplay: { + type: 'item_stack', + data: slotValue + } + } + ] + } + } else if (version['>=']('1.20.5')) { return { recipes: [ { @@ -241,6 +266,13 @@ const values = { suggestionType: 'minecraft:summonable_entities' } }, + bitflags: function (typeArgs, context) { + const results = {} + Object.keys(typeArgs.flags).forEach(function (index) { + results[typeArgs.flags[index]] = true + }) + return results + }, soundSource: 'master', packedChunkPos: { x: 10, @@ -263,7 +295,49 @@ const values = { isDebug: false, isFlat: false, portalCooldown: 0 - } + }, + MovementFlags: { + onGround: true, + hasHorizontalCollision: false + }, + ContainerID: 0, + PositionUpdateRelatives: { + x: true, + y: true, + z: true, + yaw: true, + pitch: true, + dx: true, + dy: true, + dz: true, + yawDelta: true + }, + RecipeDisplay: { + type: 'stonecutter', + data: { + ingredient: { type: 'empty' }, + result: { type: 'empty' }, + craftingStation: { type: 'empty' } + } + }, + SlotDisplay: { type: 'empty' }, + game_profile: { + name: 'test', + properties: [{ + key: 'foo', + value: 'bar' + }] + }, + optvarint: 1, + chat_session: { + uuid: '00112233-4455-6677-8899-aabbccddeeff', + publicKey: { + expireTime: 30, + keyBytes: [], + keySignature: [] + } + }, + IDSet: { ids: [2, 5] } } function getValue (_type, packet) { From d6b4e82eb170984380e7ea9f125ea5d72777bef2 Mon Sep 17 00:00:00 2001 From: u9g Date: Wed, 4 Dec 2024 15:22:11 -0500 Subject: [PATCH 136/171] Add type to serverKey in server (#1349) * add types to serverKey in server * don't change dep order --- package.json | 1 + src/index.d.ts | 2 ++ src/server/login.js | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/package.json b/package.json index 2eee53d5a..11cbe1d14 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "prismarine-registry": "^1.8.0" }, "dependencies": { + "@types/node-rsa": "^1.1.4", "@types/readable-stream": "^4.0.0", "aes-js": "^3.1.2", "buffer-equal": "^1.0.0", diff --git a/src/index.d.ts b/src/index.d.ts index 423085259..e61d5403b 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -7,6 +7,7 @@ import { Agent } from 'http' import { Transform } from "readable-stream"; import { BinaryLike, KeyObject } from 'crypto'; import { Realm } from "prismarine-realms" +import NodeRSA from 'node-rsa'; type PromiseLike = Promise | void @@ -166,6 +167,7 @@ declare module 'minecraft-protocol' { motd: string motdMsg?: Object favicon: string + serverKey: NodeRSA close(): void on(event: 'connection', handler: (client: ServerClient) => PromiseLike): this on(event: 'error', listener: (error: Error) => PromiseLike): this diff --git a/src/server/login.js b/src/server/login.js index ad143d383..858b952ca 100644 --- a/src/server/login.js +++ b/src/server/login.js @@ -8,6 +8,11 @@ const { concat } = require('../transforms/binaryStream') const { mojangPublicKeyPem } = require('./constants') const debug = require('debug')('minecraft-protocol') +/** + * @param {import('../index').Client} client + * @param {import('../index').Server} server + * @param {Object} options + */ module.exports = function (client, server, options) { const mojangPubKey = crypto.createPublicKey(mojangPublicKeyPem) const raise = (translatableError) => client.end(translatableError, JSON.stringify({ translate: translatableError })) From f258c76b3a15badd902e82cd892168849444d79d Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Wed, 4 Dec 2024 21:23:47 +0100 Subject: [PATCH 137/171] Release 1.51.0 (#1354) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index cc315a389..be234e791 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,10 @@ # History +## 1.51.0 +* [Add type to serverKey in server (#1349)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/d6b4e82eb170984380e7ea9f125ea5d72777bef2) (thanks @u9g) +* [support 1.21.3 (#1347)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/2224d824065908e910520dfa8ea9f3f3ade242e4) (thanks @rom1504) +* [Bump @types/node from 20.16.15 to 22.7.9 (#1345)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/590dc33fed2100e77ef58e7db716dfc45eb61159) (thanks @dependabot[bot]) + ## 1.50.0 * [1.21 Support (#1342)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/5bebac36620d8f8ec256d19483e20e643d63de2a) (thanks @GroobleDierne) diff --git a/package.json b/package.json index 11cbe1d14..40a222aca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.50.0", + "version": "1.51.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 8e131c359ebd5509136fd849a82cc59cd0dc1e58 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Mon, 6 Jan 2025 13:47:44 -0500 Subject: [PATCH 138/171] Fix server_data payload for 1.19+, fix kicks messages on 1.20.3+ (#1364) * Fix server_data payload sent to clients for versions 1.19+ Fixes #1362 Add missing fields to `server_data` payload for versions 1.19+. * Add `motd` and `icon` fields to `server_data` payload for version 1.19. * Add `motd`, `icon`, and `enforcesSecureChat` fields to `server_data` payload for version 1.19.2. * Add `motd`, `icon`, and `enforcesSecureChat` fields to `server_data` payload for version 1.19.3. * Add `motd`, `iconBytes`, and `enforcesSecureChat` fields to `server_data` payload for versions 1.19.4, 1.20, and 1.20.2. * Add `motd`, `iconBytes`, and `enforcesSecureChat` fields to `server_data` payload for version 1.20.3. * Add `motd` and `iconBytes` fields to `server_data` payload for version 1.20.5+. * Use NBT components for `motd` if `chatPacketsUseNbtComponents` feature is supported. * Convert `favicon` to buffer for `iconBytes` field if available. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/PrismarineJS/node-minecraft-protocol/issues/1362?shareId=XXXX-XXXX-XXXX-XXXX). * add --retries 2 * lint, debug close event emit twice * bail tests * fix * flaky test fix * remove debug * Update serverTest.js * Fix NBT chat not being used for 1.20.3+ kicks * Update createClient.js * fix client.._supportFeature not being defined * only nbt on the play state disconnect --- src/client.js | 1 + src/createServer.js | 1 + src/server.js | 8 ++++++-- src/server/login.js | 6 ++++++ test/serverTest.js | 15 +++++++++++++-- 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/client.js b/src/client.js index 5b63c2950..74749698f 100644 --- a/src/client.js +++ b/src/client.js @@ -30,6 +30,7 @@ class Client extends EventEmitter { this.hideErrors = hideErrors this.closeTimer = null const mcData = require('minecraft-data')(version) + this._supportFeature = mcData.supportFeature this.state = states.HANDSHAKING this._hasBundlePacket = mcData.supportFeature('hasBundlePacket') } diff --git a/src/createServer.js b/src/createServer.js index 4a56c0ad9..50ae0a38c 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -46,6 +46,7 @@ function createServer (options = {}) { server.onlineModeExceptions = Object.create(null) server.favicon = favicon server.options = options + server._supportFeature = mcData.supportFeature options.registryCodec = options.registryCodec || mcData.registryCodec || mcData.loginPacket?.dimensionCodec // The RSA keypair can take some time to generate diff --git a/src/server.js b/src/server.js index 99cfcbac2..46df10422 100644 --- a/src/server.js +++ b/src/server.js @@ -4,6 +4,7 @@ const net = require('net') const EventEmitter = require('events').EventEmitter const Client = require('./client') const states = require('./states') +const nbt = require('prismarine-nbt') const { createSerializer } = require('./transforms/serializer') class Server extends EventEmitter { @@ -26,11 +27,14 @@ class Server extends EventEmitter { self.socketServer.on('connection', socket => { const client = new Client(true, this.version, this.customPackets, this.hideErrors) client._end = client.end - client.end = function end (endReason, fullReason = JSON.stringify({ text: endReason })) { + client.end = function end (endReason, fullReason) { if (client.state === states.PLAY) { + fullReason ||= this._supportFeature('chatPacketsUseNbtComponents') + ? nbt.comp({ text: nbt.string(endReason) }) + : JSON.stringify({ text: endReason }) client.write('kick_disconnect', { reason: fullReason }) } else if (client.state === states.LOGIN) { - client.write('disconnect', { reason: fullReason }) + client.write('disconnect', { reason: fullReason || endReason }) } client._end(endReason) } diff --git a/src/server/login.js b/src/server/login.js index 858b952ca..bc5785114 100644 --- a/src/server/login.js +++ b/src/server/login.js @@ -7,6 +7,7 @@ const chatPlugin = require('./chat') const { concat } = require('../transforms/binaryStream') const { mojangPublicKeyPem } = require('./constants') const debug = require('debug')('minecraft-protocol') +const nbt = require('prismarine-nbt') /** * @param {import('../index').Client} client @@ -196,7 +197,12 @@ module.exports = function (client, server, options) { client.settings = {} if (client.supportFeature('chainedChatWithHashing')) { // 1.19.1+ + const jsonMotd = JSON.stringify(server.motdMsg ?? { text: server.motd }) + const nbtMotd = nbt.comp({ text: nbt.string(server.motd) }) client.write('server_data', { + motd: client.supportFeature('chatPacketsUseNbtComponents') ? nbtMotd : jsonMotd, + icon: server.favicon, // b64 + iconBytes: server.favicon ? Buffer.from(server.favicon, 'base64') : undefined, previewsChat: options.enableChatPreview, // Note: in 1.20.5+ user must send this with `login` enforcesSecureChat: options.enforceSecureProfile diff --git a/test/serverTest.js b/test/serverTest.js index 5837e7656..197603422 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -102,6 +102,7 @@ for (const supportedVersion of mc.supportedVersions) { describe('mc-server ' + supportedVersion + 'v', function () { this.timeout(5000) this.beforeEach(async function () { + console.log('🔻 Starting test', this.currentTest.title) PORT = await getPort() console.log(`Using port for tests: ${PORT}`) }) @@ -411,9 +412,12 @@ for (const supportedVersion of mc.supportedVersions) { }) }) function checkFinish () { - if (serverPlayerDisconnected && clientClosed && serverClosed) done() + if (serverPlayerDisconnected && clientClosed && serverClosed) { + console.log('Kick test is done') + callOnce(done) + } } - }) + }).retries(2) it('gives correct reason for kicking clients when shutting down', function (done) { const server = mc.createServer({ @@ -532,3 +536,10 @@ for (const supportedVersion of mc.supportedVersions) { }) }) } + +function callOnce (fn, ...args) { + console.log('Call Fn', fn.called) + if (fn.called) return + fn(...args) + fn.called = true +} From d38f24aedf08499dce7bdbb547b35bb8b182c2ff Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Mon, 6 Jan 2025 19:51:40 +0100 Subject: [PATCH 139/171] Release 1.52.0 (#1365) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index be234e791..1d980a811 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.52.0 +* [Fix server_data payload for 1.19+, fix kicks messages on 1.20.3+ (#1364)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/8e131c359ebd5509136fd849a82cc59cd0dc1e58) (thanks @extremeheat) + ## 1.51.0 * [Add type to serverKey in server (#1349)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/d6b4e82eb170984380e7ea9f125ea5d72777bef2) (thanks @u9g) * [support 1.21.3 (#1347)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/2224d824065908e910520dfa8ea9f3f3ade242e4) (thanks @rom1504) diff --git a/package.json b/package.json index 40a222aca..1b6854c1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.51.0", + "version": "1.52.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 502513b432695bd9e0fdff039bd8a7de02b307e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 19:51:58 +0100 Subject: [PATCH 140/171] Bump mocha from 10.8.2 to 11.0.1 (#1352) Bumps [mocha](https://github.com/mochajs/mocha) from 10.8.2 to 11.0.1. - [Release notes](https://github.com/mochajs/mocha/releases) - [Changelog](https://github.com/mochajs/mocha/blob/main/CHANGELOG.md) - [Commits](https://github.com/mochajs/mocha/compare/v10.8.2...v11.0.1) --- updated-dependencies: - dependency-name: mocha dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Romain Beaumont --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b6854c1a..a4838ec87 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "minecraft-packets": "^1.1.5", "minecraft-protocol": "file:.", "minecraft-wrap": "^1.2.3", - "mocha": "^10.0.0", + "mocha": "^11.0.1", "power-assert": "^1.0.0", "standard": "^17.0.0", "prismarine-registry": "^1.8.0" From e74d11f66a835c08337b47dc5a2a7848c7e6e94c Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 8 Jan 2025 12:43:39 -0500 Subject: [PATCH 141/171] 1.21.4 (#1366) * 1.21.4 * update readme * Update ci.yml --------- Co-authored-by: Romain Beaumont --- docs/README.md | 2 +- src/version.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/README.md b/docs/README.md index f8cb78a32..fbcaa4366 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6), 1.21 (1.21, 1.21.1, 1.21.3) + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6), 1.21 (1.21, 1.21.1, 1.21.3, 1.21.4) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. diff --git a/src/version.js b/src/version.js index 29d71e768..dfed3bb26 100644 --- a/src/version.js +++ b/src/version.js @@ -2,5 +2,5 @@ module.exports = { defaultVersion: '1.21.1', - supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.6', '1.21.1', '1.21.3'] + supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.6', '1.21.1', '1.21.3', '1.21.4'] } From 556b9ddb376e089327c3fbb4ecbcabd32c7fd117 Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Wed, 8 Jan 2025 19:24:50 +0100 Subject: [PATCH 142/171] Release 1.53.0 (#1367) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 1d980a811..7a81520b9 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,9 @@ # History +## 1.53.0 +* [1.21.4 (#1366)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/e74d11f66a835c08337b47dc5a2a7848c7e6e94c) (thanks @extremeheat) +* [Bump mocha from 10.8.2 to 11.0.1 (#1352)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/502513b432695bd9e0fdff039bd8a7de02b307e0) (thanks @dependabot[bot]) + ## 1.52.0 * [Fix server_data payload for 1.19+, fix kicks messages on 1.20.3+ (#1364)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/8e131c359ebd5509136fd849a82cc59cd0dc1e58) (thanks @extremeheat) diff --git a/package.json b/package.json index a4838ec87..1072c0917 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.52.0", + "version": "1.53.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From c879d0e753f4f16fe5889ba53c9c004cc8832a56 Mon Sep 17 00:00:00 2001 From: Jacob Koshy <42344274+jacobk999@users.noreply.github.com> Date: Sun, 19 Jan 2025 09:21:41 -0500 Subject: [PATCH 143/171] fix: use node-rsa for decryption for higher node compatibility (#1319) Co-authored-by: extremeheat --- src/server/login.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/server/login.js b/src/server/login.js index bc5785114..60cb4d49b 100644 --- a/src/server/login.js +++ b/src/server/login.js @@ -7,6 +7,7 @@ const chatPlugin = require('./chat') const { concat } = require('../transforms/binaryStream') const { mojangPublicKeyPem } = require('./constants') const debug = require('debug')('minecraft-protocol') +const NodeRSA = require('node-rsa') const nbt = require('prismarine-nbt') /** @@ -112,6 +113,9 @@ module.exports = function (client, server, options) { } } + const keyRsa = new NodeRSA(server.serverKey.exportKey('pkcs1'), 'private', { encryptionScheme: 'pkcs1' }) + keyRsa.setOptions({ environment: 'browser' }) + if (packet.hasVerifyToken === false) { // 1.19, hasVerifyToken is set and equal to false IF chat signing is enabled // This is the default action starting in 1.19.1. @@ -123,10 +127,7 @@ module.exports = function (client, server, options) { } else { const encryptedToken = packet.hasVerifyToken ? packet.crypto.verifyToken : packet.verifyToken try { - const decryptedToken = crypto.privateDecrypt({ - key: server.serverKey.exportKey(), - padding: crypto.constants.RSA_PKCS1_PADDING - }, encryptedToken) + const decryptedToken = keyRsa.decrypt(encryptedToken) if (!client.verifyToken.equals(decryptedToken)) { client.end('DidNotEncryptVerifyTokenProperly') @@ -137,13 +138,9 @@ module.exports = function (client, server, options) { return } } - let sharedSecret try { - sharedSecret = crypto.privateDecrypt({ - key: server.serverKey.exportKey(), - padding: crypto.constants.RSA_PKCS1_PADDING - }, packet.sharedSecret) + sharedSecret = keyRsa.decrypt(packet.sharedSecret) } catch (e) { client.end('DidNotEncryptVerifyTokenProperly') return From 1d7c2a8c4855519ded86a4db317d154a8c4ec346 Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Sun, 19 Jan 2025 15:35:42 +0100 Subject: [PATCH 144/171] Release 1.54.0 (#1368) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 7a81520b9..94a2422a9 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.54.0 +* [fix: use node-rsa for decryption for higher node compatibility (#1319)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/c879d0e753f4f16fe5889ba53c9c004cc8832a56) (thanks @jacobk999) + ## 1.53.0 * [1.21.4 (#1366)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/e74d11f66a835c08337b47dc5a2a7848c7e6e94c) (thanks @extremeheat) * [Bump mocha from 10.8.2 to 11.0.1 (#1352)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/502513b432695bd9e0fdff039bd8a7de02b307e0) (thanks @dependabot[bot]) diff --git a/package.json b/package.json index 1072c0917..ecf4e71ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.53.0", + "version": "1.54.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From c9cf36354914a57bac9e17e2076670b37c04d4a9 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 22 Jan 2025 13:33:18 -0500 Subject: [PATCH 145/171] Add `npm update` to version error message --- src/client/autoVersion.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/autoVersion.js b/src/client/autoVersion.js index c437ecf3a..3fe155267 100644 --- a/src/client/autoVersion.js +++ b/src/client/autoVersion.js @@ -29,7 +29,7 @@ module.exports = function (client, options) { .sort(function (a, b) { return b.version - a.version }) const versions = (minecraftData.postNettyVersionsByProtocolVersion.pc[protocolVersion] || []).concat(guessFromName) if (versions.length === 0) { - client.emit('error', new Error(`unsupported/unknown protocol version: ${protocolVersion}, update minecraft-data`)) + client.emit('error', new Error(`Unsupported protocol version '${protocolVersion}'; try updating your packages with 'npm update'`)) } const minecraftVersion = versions[0].minecraftVersion From 080aa52c5bd70a5f9c4ecc37480497dd335a9e83 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 22 Jan 2025 14:06:38 -0500 Subject: [PATCH 146/171] Add npm update to version error message --- src/transforms/serializer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/serializer.js b/src/transforms/serializer.js index 7cc127e51..dc936a22e 100644 --- a/src/transforms/serializer.js +++ b/src/transforms/serializer.js @@ -24,7 +24,7 @@ function createProtocol (state, direction, version, customPackets, compiled = tr throw new Error(`No data available for version ${version}`) } else if (versionInfo && versionInfo.version !== mcData.version.version) { // The protocol version returned by node-minecraft-data constructor does not match the data in minecraft-data's protocolVersions.json - throw new Error(`Do not have protocol data for protocol version ${versionInfo.version} (attempted to use ${mcData.version.version} data)`) + throw new Error(`Unsupported protocol version '${versionInfo.version}' (attempted to use '${mcData.version.version}' data); try updating your packages with 'npm update'`) } if (compiled) { From e9eb551ba30ec2e742c49e6927be6402b413bb76 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sun, 26 Jan 2025 22:57:56 +0100 Subject: [PATCH 147/171] Update to node 22 (#1371) * node 22 * Update package.json --- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/npm-publish.yml | 2 +- package.json | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93ef806e2..2c80125a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,10 +14,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Use Node.js 18.x + - name: Use Node.js 22.x uses: actions/setup-node@v1.4.4 with: - node-version: 18.x + node-version: 22.x - run: npm i && npm run lint PrepareSupportedVersions: runs-on: ubuntu-latest @@ -26,10 +26,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Use Node.js 18.x + - name: Use Node.js 22.x uses: actions/setup-node@v1.4.4 with: - node-version: 18.x + node-version: 22.x - id: set-matrix run: | node -e " @@ -45,10 +45,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Use Node.js 18.x + - name: Use Node.js 22.x uses: actions/setup-node@v1 with: - node-version: 18.x + node-version: 22.x - name: Setup Java JDK uses: actions/setup-java@v1.4.3 with: diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 4ef5c8e1e..f5e18ab9c 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -13,7 +13,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@master with: - node-version: 18.0.0 + node-version: 22.0.0 - id: publish uses: JS-DevTools/npm-publish@v1 with: diff --git a/package.json b/package.json index ecf4e71ac..afb7a5881 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "author": "Andrew Kelley", "license": "BSD-3-Clause", "engines": { - "node": ">=14" + "node": ">=22" }, "browser": "src/browser.js", "devDependencies": { From 5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d Mon Sep 17 00:00:00 2001 From: h5mcbox <33593244+h5mcbox@users.noreply.github.com> Date: Sat, 22 Feb 2025 15:01:47 +0800 Subject: [PATCH 148/171] Fix #1369 online-mode error 1.20.5-1.21.4 (#1375) --- src/server/login.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/login.js b/src/server/login.js index 60cb4d49b..9d63912fb 100644 --- a/src/server/login.js +++ b/src/server/login.js @@ -98,7 +98,8 @@ module.exports = function (client, server, options) { client.write('encryption_begin', { serverId, publicKey: client.publicKey, - verifyToken: client.verifyToken + verifyToken: client.verifyToken, + shouldAuthenticate: true }) } else { loginClient() From 3bd4dc1b2002cd7badfa5b9cf8dda35cd6cc9ac1 Mon Sep 17 00:00:00 2001 From: h5mcbox <33593244+h5mcbox@users.noreply.github.com> Date: Mon, 17 Mar 2025 11:23:39 +0800 Subject: [PATCH 149/171] Fix `client.end()` (#1376) --- src/createServer.js | 1 - src/server.js | 2 +- src/server/handshake.js | 7 ++++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/createServer.js b/src/createServer.js index 50ae0a38c..4a56c0ad9 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -46,7 +46,6 @@ function createServer (options = {}) { server.onlineModeExceptions = Object.create(null) server.favicon = favicon server.options = options - server._supportFeature = mcData.supportFeature options.registryCodec = options.registryCodec || mcData.registryCodec || mcData.loginPacket?.dimensionCodec // The RSA keypair can take some time to generate diff --git a/src/server.js b/src/server.js index 46df10422..f66757384 100644 --- a/src/server.js +++ b/src/server.js @@ -29,7 +29,7 @@ class Server extends EventEmitter { client._end = client.end client.end = function end (endReason, fullReason) { if (client.state === states.PLAY) { - fullReason ||= this._supportFeature('chatPacketsUseNbtComponents') + fullReason ||= client._supportFeature('chatPacketsUseNbtComponents') ? nbt.comp({ text: nbt.string(endReason) }) : JSON.stringify({ text: endReason }) client.write('kick_disconnect', { reason: fullReason }) diff --git a/src/server/handshake.js b/src/server/handshake.js index e3d5a75ec..1e51b00c8 100644 --- a/src/server/handshake.js +++ b/src/server/handshake.js @@ -9,8 +9,11 @@ module.exports = function (client, server, { version, fallbackVersion }) { client.protocolVersion = packet.protocolVersion if (version === false) { - if (require('minecraft-data')(client.protocolVersion)) { + const mcData = require('minecraft-data')(client.protocolVersion) + if (mcData) { client.version = client.protocolVersion + client._supportFeature = mcData.supportFeature + client._hasBundlePacket = mcData.supportFeature('hasBundlePacket') } else { let fallback if (fallbackVersion !== undefined) { @@ -18,6 +21,8 @@ module.exports = function (client, server, { version, fallbackVersion }) { } if (fallback) { client.version = fallback.version.version + client._supportFeature = fallback.supportFeature + client._hasBundlePacket = fallback.supportFeature('hasBundlePacket') } else { client.end('Protocol version ' + client.protocolVersion + ' is not supported') } From 9c6a4b2504a21fed7d31bd5383284c331d39c237 Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Sun, 23 Mar 2025 21:39:39 +0100 Subject: [PATCH 150/171] Release 1.55.0 (#1382) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 94a2422a9..ed5e9d308 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,12 @@ # History +## 1.55.0 +* [Fix `client.end()` (#1376)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/3bd4dc1b2002cd7badfa5b9cf8dda35cd6cc9ac1) (thanks @h5mcbox) +* [Fix #1369 online-mode error 1.20.5-1.21.4 (#1375)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d) (thanks @h5mcbox) +* [Update to node 22 (#1371)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/e9eb551ba30ec2e742c49e6927be6402b413bb76) (thanks @rom1504) +* [Add npm update to version error message](https://github.com/PrismarineJS/node-minecraft-protocol/commit/080aa52c5bd70a5f9c4ecc37480497dd335a9e83) (thanks @extremeheat) +* [Add `npm update` to version error message](https://github.com/PrismarineJS/node-minecraft-protocol/commit/c9cf36354914a57bac9e17e2076670b37c04d4a9) (thanks @extremeheat) + ## 1.54.0 * [fix: use node-rsa for decryption for higher node compatibility (#1319)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/c879d0e753f4f16fe5889ba53c9c004cc8832a56) (thanks @jacobk999) diff --git a/package.json b/package.json index afb7a5881..687b1c8b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.54.0", + "version": "1.55.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 6a445312d384a7ca739e29d61bc37e4525da21e4 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sun, 6 Apr 2025 15:49:02 +0200 Subject: [PATCH 151/171] Update serializer.js to remove usage of lodash.get (#1390) --- src/transforms/serializer.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/transforms/serializer.js b/src/transforms/serializer.js index dc936a22e..76f7ef271 100644 --- a/src/transforms/serializer.js +++ b/src/transforms/serializer.js @@ -1,21 +1,18 @@ 'use strict' -const ProtoDef = require('protodef').ProtoDef -const Serializer = require('protodef').Serializer -const Parser = require('protodef').FullPacketParser +const { ProtoDef, Serializer, FullPacketParser } = require('protodef') const { ProtoDefCompiler } = require('protodef').Compiler const nbt = require('prismarine-nbt') const minecraft = require('../datatypes/minecraft') const states = require('../states') const merge = require('lodash.merge') -const get = require('lodash.get') const minecraftData = require('minecraft-data') const protocols = {} function createProtocol (state, direction, version, customPackets, compiled = true) { - const key = state + ';' + direction + ';' + version + (compiled ? ';c' : '') + const key = `${state};${direction};${version}${compiled ? ';c' : ''}` if (protocols[key]) { return protocols[key] } const mcData = minecraftData(version) @@ -27,10 +24,12 @@ function createProtocol (state, direction, version, customPackets, compiled = tr throw new Error(`Unsupported protocol version '${versionInfo.version}' (attempted to use '${mcData.version.version}' data); try updating your packages with 'npm update'`) } + const mergedProtocol = merge(mcData.protocol, customPackets?.[mcData.version.majorVersion] ?? {}) + if (compiled) { const compiler = new ProtoDefCompiler() compiler.addTypes(require('../datatypes/compiler-minecraft')) - compiler.addProtocol(merge(mcData.protocol, get(customPackets, [mcData.version.majorVersion])), [state, direction]) + compiler.addProtocol(mergedProtocol, [state, direction]) nbt.addTypesToCompiler('big', compiler) const proto = compiler.compileProtoDefSync() protocols[key] = proto @@ -39,7 +38,7 @@ function createProtocol (state, direction, version, customPackets, compiled = tr const proto = new ProtoDef(false) proto.addTypes(minecraft) - proto.addProtocol(merge(mcData.protocol, get(customPackets, [mcData.version.majorVersion])), [state, direction]) + proto.addProtocol(mergedProtocol, [state, direction]) nbt.addTypesToInterperter('big', proto) protocols[key] = proto return proto @@ -50,7 +49,7 @@ function createSerializer ({ state = states.HANDSHAKING, isServer = false, versi } function createDeserializer ({ state = states.HANDSHAKING, isServer = false, version, customPackets, compiled = true, noErrorLogging = false } = {}) { - return new Parser(createProtocol(state, isServer ? 'toServer' : 'toClient', version, customPackets, compiled), 'packet', noErrorLogging) + return new FullPacketParser(createProtocol(state, isServer ? 'toServer' : 'toClient', version, customPackets, compiled), 'packet', noErrorLogging) } module.exports = { From 90de8b07e6c375c610d52a162bba3d89a47a757a Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Sun, 6 Apr 2025 16:02:57 +0200 Subject: [PATCH 152/171] Release 1.56.0 (#1391) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index ed5e9d308..417dff035 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.56.0 +* [Update serializer.js to remove usage of lodash.get (#1390)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/6a445312d384a7ca739e29d61bc37e4525da21e4) (thanks @rom1504) + ## 1.55.0 * [Fix `client.end()` (#1376)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/3bd4dc1b2002cd7badfa5b9cf8dda35cd6cc9ac1) (thanks @h5mcbox) * [Fix #1369 online-mode error 1.20.5-1.21.4 (#1375)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d) (thanks @h5mcbox) diff --git a/package.json b/package.json index 687b1c8b1..66b9b9bf0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.55.0", + "version": "1.56.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 031f13fea45264775311ae82e5a4efe74ebba96d Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sun, 6 Apr 2025 16:08:28 +0200 Subject: [PATCH 153/171] Update package.json to remove lodash.get --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 66b9b9bf0..a204c6646 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "buffer-equal": "^1.0.0", "debug": "^4.3.2", "endian-toggle": "^0.0.0", - "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", "minecraft-data": "^3.78.0", "minecraft-folder-path": "^1.2.0", From 9e116c3dd4682b17c4e2c80249a2447a093d9284 Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Sun, 6 Apr 2025 16:28:06 +0200 Subject: [PATCH 154/171] Release 1.57.0 (#1392) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 417dff035..675633039 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.57.0 +* [Update package.json to remove lodash.get](https://github.com/PrismarineJS/node-minecraft-protocol/commit/031f13fea45264775311ae82e5a4efe74ebba96d) (thanks @rom1504) + ## 1.56.0 * [Update serializer.js to remove usage of lodash.get (#1390)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/6a445312d384a7ca739e29d61bc37e4525da21e4) (thanks @rom1504) diff --git a/package.json b/package.json index a204c6646..a75eb777f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.56.0", + "version": "1.57.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 15:18:47 +0200 Subject: [PATCH 155/171] Bump @types/node from 22.15.33 to 24.0.4 (#1405) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.33 to 24.0.4. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 24.0.4 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a75eb777f..88fd264b7 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ }, "browser": "src/browser.js", "devDependencies": { - "@types/node": "^22.7.9", + "@types/node": "^24.0.4", "espower-loader": "^1.0.0", "intelli-espower-loader": "^1.0.0", "minecraft-packets": "^1.1.5", From 7207b61f3a809ec9db01869a90c5ccaeafee4ca1 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Tue, 1 Jul 2025 03:38:05 -0400 Subject: [PATCH 156/171] Fixes to protocol Holder implementation (#1355) * Continued updates * bail tests early on failure * improve deserialization errors to include packet headers * Update play.js * Update server.js * Update ci.yml * add debug logging * update chat player_info handling * Fix 1.7 player_info and chat type field * cleanup * Update compiler-minecraft.js - important fix * Update compiler-minecraft.js fix holder type * Update ci.yml --------- Co-authored-by: Romain Beaumont --- docs/README.md | 2 +- examples/server/server.js | 2 +- .../server_helloworld/server_helloworld.js | 2 +- src/client.js | 6 +- src/client/chat.js | 45 +++--- src/client/play.js | 3 +- src/datatypes/compiler-minecraft.js | 128 ++---------------- src/datatypes/minecraft.js | 36 +---- src/server.js | 3 +- test/packetTest.js | 45 +++++- test/serverTest.js | 2 +- 11 files changed, 81 insertions(+), 193 deletions(-) diff --git a/docs/README.md b/docs/README.md index fbcaa4366..ebc80984b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -174,7 +174,7 @@ server.on('playerJoin', function(client) { plainMessage: message, signedChatContent: '', unsignedChatContent: chatText(message), - type: 0, + type: mcData.supportFeature('chatTypeIsHolder') ? { chatType: 1 } : 0, senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random senderName: JSON.stringify({ text: 'me' }), senderTeam: undefined, diff --git a/examples/server/server.js b/examples/server/server.js index 547c095d5..d22078591 100644 --- a/examples/server/server.js +++ b/examples/server/server.js @@ -75,7 +75,7 @@ function sendBroadcastMessage (server, clients, message, sender) { plainMessage: message, signedChatContent: '', unsignedChatContent: chatText(message), - type: 0, + type: mcData.supportFeature('chatTypeIsHolder') ? { chatType: 1 } : 0, senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random senderName: JSON.stringify({ text: sender }), senderTeam: undefined, diff --git a/examples/server_helloworld/server_helloworld.js b/examples/server_helloworld/server_helloworld.js index 3391717e2..1c2b31212 100644 --- a/examples/server_helloworld/server_helloworld.js +++ b/examples/server_helloworld/server_helloworld.js @@ -68,7 +68,7 @@ server.on('playerJoin', function (client) { plainMessage: message, signedChatContent: '', unsignedChatContent: chatText(message), - type: 0, + type: mcData.supportFeature('chatTypeIsHolder') ? { chatType: 1 } : 0, senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random senderName: JSON.stringify({ text: 'me' }), senderTeam: undefined, diff --git a/src/client.js b/src/client.js index 74749698f..5c7a62b01 100644 --- a/src/client.js +++ b/src/client.js @@ -67,14 +67,14 @@ class Client extends EventEmitter { }) this.deserializer.on('error', (e) => { - let parts + 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}` + e.message = e.buffer ? `Parse error for ${e.field} (${e.buffer?.length} bytes, ${e.buffer?.toString('hex').slice(0, 6)}...) : ${e.message}` : `Parse error for ${e.field}: ${e.message}` if (!this.compressor) { this.splitter.pipe(this.deserializer) } else { this.decompressor.pipe(this.deserializer) } this.emit('error', e) }) diff --git a/src/client/chat.js b/src/client/chat.js index f14269bea..8d0869b15 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -106,38 +106,29 @@ module.exports = function (client, options) { }) client.on('player_info', (packet) => { - if (mcData.supportFeature('playerInfoActionIsBitfield')) { // 1.19.3+ - if (packet.action & 2) { // chat session - for (const player of packet.data) { - if (!player.chatSession) continue - client._players[player.UUID] = { - publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }), - publicKeyDER: player.chatSession.publicKey.keyBytes, - sessionUuid: player.chatSession.uuid - } - client._players[player.UUID].sessionIndex = true - client._players[player.UUID].hasChainIntegrity = true + for (const player of packet.data) { + if (player.chatSession) { + client._players[player.uuid] = { + publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }), + publicKeyDER: player.chatSession.publicKey.keyBytes, + sessionUuid: player.chatSession.uuid } + client._players[player.uuid].sessionIndex = true + client._players[player.uuid].hasChainIntegrity = true } - return - } - - if (packet.action === 0) { // add player - for (const player of packet.data) { - if (player.crypto) { - client._players[player.UUID] = { - publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }), - publicKeyDER: player.crypto.publicKey, - signature: player.crypto.signature, - displayName: player.displayName || player.name - } - client._players[player.UUID].hasChainIntegrity = true + if (player.crypto) { + client._players[player.uuid] = { + publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }), + publicKeyDER: player.crypto.publicKey, + signature: player.crypto.signature, + displayName: player.displayName || player.name } + client._players[player.uuid].hasChainIntegrity = true } - } else if (packet.action === 4) { // remove player - for (const player of packet.data) { - delete client._players[player.UUID] + + if (packet.action === 'remove_player') { // Only 1.8-1.9 + delete client._players[player.uuid] } } }) diff --git a/src/client/play.js b/src/client/play.js index 6e06dc152..559607f34 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -66,7 +66,6 @@ module.exports = function (client, options) { } function onReady () { - client.emit('playerJoin') if (mcData.supportFeature('signedChat')) { if (options.disableChatSigning && client.serverFeatures.enforcesSecureChat) { throw new Error('"disableChatSigning" was enabled in client options, but server is enforcing secure chat') @@ -86,8 +85,8 @@ module.exports = function (client, options) { function unsignedChat (message) { client.write('chat', { message }) } - client.chat = client._signedChat || unsignedChat + client.emit('playerJoin') } } } diff --git a/src/datatypes/compiler-minecraft.js b/src/datatypes/compiler-minecraft.js index 864b269f0..9068641a2 100644 --- a/src/datatypes/compiler-minecraft.js +++ b/src/datatypes/compiler-minecraft.js @@ -42,56 +42,14 @@ module.exports = { code += '}' return compiler.wrapCode(code) }], - arrayWithLengthOffset: ['parametrizable', (compiler, array) => { // TODO: remove - let code = '' - if (array.countType) { - code += 'const { value: count, size: countSize } = ' + compiler.callType(array.countType) + '\n' - } else if (array.count) { - code += 'const count = ' + array.count + '\n' - code += 'const countSize = 0\n' - } else { - throw new Error('Array must contain either count or countType') - } - code += 'if (count > 0xffffff) throw new Error("array size is abnormally large, not reading: " + count)\n' - code += 'const data = []\n' - code += 'let size = countSize\n' - code += `for (let i = 0; i < count + ${array.lengthOffset}; i++) {\n` - code += ' const elem = ' + compiler.callType(array.type, 'offset + size') + '\n' - code += ' data.push(elem.value)\n' - code += ' size += elem.size\n' - code += '}\n' - code += 'return { value: data, size }' - return compiler.wrapCode(code) - }], - bitflags: ['parametrizable', (compiler, { type, flags, shift, big }) => { - let fstr = JSON.stringify(flags) - if (Array.isArray(flags)) { - fstr = '{' - for (const [k, v] of Object.entries(flags)) fstr += `"${v}": ${big ? (1n << BigInt(k)) : (1 << k)}` + (big ? 'n,' : ',') - fstr += '}' - } else if (shift) { - fstr = '{' - for (const key in flags) fstr += `"${key}": ${1 << flags[key]},` - fstr += '}' - } - return compiler.wrapCode(` - const { value: _value, size } = ${compiler.callType(type, 'offset')} - const value = { _value } - const flags = ${fstr} - for (const key in flags) { - value[key] = (_value & flags[key]) == flags[key] - } - return { value, size } - `.trim()) - }], registryEntryHolder: ['parametrizable', (compiler, opts) => { return compiler.wrapCode(` const { value: n, size: nSize } = ${compiler.callType('varint')} if (n !== 0) { return { value: { ${opts.baseName}: n - 1 }, size: nSize } } else { - const holder = ${compiler.callType(opts.otherwise.type)} - return { value: { ${opts.otherwise.name}: holder.data }, size: nSize + holder.size } + const holder = ${compiler.callType(opts.otherwise.type, 'offset + nSize')} + return { value: { ${opts.otherwise.name}: holder.value }, size: nSize + holder.size } } `.trim()) }], @@ -145,46 +103,14 @@ if (n !== 0) { code += 'return offset' return compiler.wrapCode(code) }], - arrayWithLengthOffset: ['parametrizable', (compiler, array) => { - let code = '' - if (array.countType) { - code += 'offset = ' + compiler.callType('value.length', array.countType) + '\n' - } else if (array.count === null) { - throw new Error('Array must contain either count or countType') - } - code += 'for (let i = 0; i < value.length; i++) {\n' - code += ' offset = ' + compiler.callType('value[i]', array.type) + '\n' - code += '}\n' - code += 'return offset' - return compiler.wrapCode(code) - }], - bitflags: ['parametrizable', (compiler, { type, flags, shift, big }) => { - let fstr = JSON.stringify(flags) - if (Array.isArray(flags)) { - fstr = '{' - for (const [k, v] of Object.entries(flags)) fstr += `"${v}": ${big ? (1n << BigInt(k)) : (1 << k)}` + (big ? 'n,' : ',') - fstr += '}' - } else if (shift) { - fstr = '{' - for (const key in flags) fstr += `"${key}": ${1 << flags[key]},` - fstr += '}' - } - return compiler.wrapCode(` - const flags = ${fstr} - let val = value._value ${big ? '|| 0n' : ''} - for (const key in flags) { - if (value[key]) val |= flags[key] - } - return (ctx.${type})(val, buffer, offset) - `.trim()) - }], registryEntryHolder: ['parametrizable', (compiler, opts) => { const baseName = `value.${opts.baseName}` const otherwiseName = `value.${opts.otherwise.name}` return compiler.wrapCode(` -if (${baseName}) { +if (${baseName} != null) { offset = ${compiler.callType(`${baseName} + 1`, 'varint')} } else if (${otherwiseName}) { + offset += 1 offset = ${compiler.callType(`${otherwiseName}`, opts.otherwise.type)} } else { throw new Error('registryEntryHolder type requires "${baseName}" or "${otherwiseName}" fields to be set') @@ -196,7 +122,7 @@ return offset const baseName = `value.${opts.base.name}` const otherwiseName = `value.${opts.otherwise.name}` return compiler.wrapCode(` -if (${baseName}) { +if (${baseName} != null) { offset = ${compiler.callType(0, 'varint')} offset = ${compiler.callType(`${baseName}`, opts.base.type)} } else if (${otherwiseName}) { @@ -234,53 +160,15 @@ return offset code += 'return size' return compiler.wrapCode(code) }], - arrayWithLengthOffset: ['parametrizable', (compiler, array) => { - let code = '' - if (array.countType) { - code += 'let size = ' + compiler.callType('value.length', array.countType) + '\n' - } else if (array.count) { - code += 'let size = 0\n' - } else { - throw new Error('Array must contain either count or countType') - } - if (!isNaN(compiler.callType('value[i]', array.type))) { - code += 'size += value.length * ' + compiler.callType('value[i]', array.type) + '\n' - } else { - code += 'for (let i = 0; i < value.length; i++) {\n' - code += ' size += ' + compiler.callType('value[i]', array.type) + '\n' - code += '}\n' - } - code += 'return size' - return compiler.wrapCode(code) - }], - bitflags: ['parametrizable', (compiler, { type, flags, shift, big }) => { - let fstr = JSON.stringify(flags) - if (Array.isArray(flags)) { - fstr = '{' - for (const [k, v] of Object.entries(flags)) fstr += `"${v}": ${big ? (1n << BigInt(k)) : (1 << k)}` + (big ? 'n,' : ',') - fstr += '}' - } else if (shift) { - fstr = '{' - for (const key in flags) fstr += `"${key}": ${1 << flags[key]},` - fstr += '}' - } - return compiler.wrapCode(` - const flags = ${fstr} - let val = value._value ${big ? '|| 0n' : ''} - for (const key in flags) { - if (value[key]) val |= flags[key] - } - return (ctx.${type})(val) - `.trim()) - }], registryEntryHolder: ['parametrizable', (compiler, opts) => { const baseName = `value.${opts.baseName}` const otherwiseName = `value.${opts.otherwise.name}` return compiler.wrapCode(` let size = 0 -if (${baseName}) { +if (${baseName} != null) { size += ${compiler.callType(`${baseName} + 1`, 'varint')} } else if (${otherwiseName}) { + size += 1 size += ${compiler.callType(`${otherwiseName}`, opts.otherwise.type)} } else { throw new Error('registryEntryHolder type requires "${baseName}" or "${otherwiseName}" fields to be set') @@ -293,7 +181,7 @@ return size const otherwiseName = `value.${opts.otherwise.name}` return compiler.wrapCode(` let size = 0 -if (${baseName}) { +if (${baseName} != null) { size += ${compiler.callType(0, 'varint')} size += ${compiler.callType(`${baseName}`, opts.base.type)} } else if (${otherwiseName}) { diff --git a/src/datatypes/minecraft.js b/src/datatypes/minecraft.js index 39f5d867c..09e90355a 100644 --- a/src/datatypes/minecraft.js +++ b/src/datatypes/minecraft.js @@ -11,8 +11,7 @@ module.exports = { compressedNbt: [readCompressedNbt, writeCompressedNbt, sizeOfCompressedNbt], restBuffer: [readRestBuffer, writeRestBuffer, sizeOfRestBuffer], entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata], - topBitSetTerminatedArray: [readTopBitSetTerminatedArray, writeTopBitSetTerminatedArray, sizeOfTopBitSetTerminatedArray], - arrayWithLengthOffset: [readArrayWithLengthOffset, writeArrayWithLengthOffset, sizeOfArrayWithLengthOffset] + topBitSetTerminatedArray: [readTopBitSetTerminatedArray, writeTopBitSetTerminatedArray, sizeOfTopBitSetTerminatedArray] } const PartialReadError = require('protodef').utils.PartialReadError @@ -181,36 +180,3 @@ function sizeOfTopBitSetTerminatedArray (value, { type }) { } return size } - -// -const { getCount, sendCount, calcCount, tryDoc } = require('protodef/src/utils') - -function readArrayWithLengthOffset (buffer, offset, typeArgs, rootNode) { - const results = { - value: [], - size: 0 - } - let value - let { count, size } = getCount.call(this, buffer, offset, typeArgs, rootNode) - offset += size - results.size += size - for (let i = 0; i < count + typeArgs.lengthOffset; i++) { - ({ size, value } = tryDoc(() => this.read(buffer, offset, typeArgs.type, rootNode), i)) - results.size += size - offset += size - results.value.push(value) - } - return results -} - -// no changes -function writeArrayWithLengthOffset (value, buffer, offset, typeArgs, rootNode) { - offset = sendCount.call(this, value.length, buffer, offset, typeArgs, rootNode) - return value.reduce((offset, v, index) => tryDoc(() => this.write(v, buffer, offset, typeArgs.type, rootNode), index), offset) -} - -function sizeOfArrayWithLengthOffset (value, typeArgs, rootNode) { - let size = calcCount.call(this, value.length, typeArgs, rootNode) - size = value.reduce((size, v, index) => tryDoc(() => size + this.sizeOf(v, typeArgs.type, rootNode), index), size) - return size + typeArgs -} diff --git a/src/server.js b/src/server.js index f66757384..01b85a997 100644 --- a/src/server.js +++ b/src/server.js @@ -34,7 +34,8 @@ class Server extends EventEmitter { : JSON.stringify({ text: endReason }) client.write('kick_disconnect', { reason: fullReason }) } else if (client.state === states.LOGIN) { - client.write('disconnect', { reason: fullReason || endReason }) + fullReason ||= JSON.stringify({ text: endReason }) + client.write('disconnect', { reason: fullReason }) } client._end(endReason) } diff --git a/test/packetTest.js b/test/packetTest.js index 7f89b95f5..2f5d65228 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -96,6 +96,47 @@ function getFixedPacketPayload (version, packetName) { } } } + if (packetName === 'player_info') { + if (version.majorVersion === '1.7') return { playerName: 'test', online: true, ping: 1 } + if (version['>=']('1.19.3')) { + return { + action: { + _value: 63, + add_player: true, + initialize_chat: true, + update_game_mode: true, + update_listed: true, + update_latency: true, + update_display_name: true + }, + data: [ + { + uuid: 'a01e3843-e521-3998-958a-f459800e4d11', + player: { name: 'Player', properties: [] }, + chatSession: undefined, + gamemode: 0, + listed: 1, + latency: 0, + displayName: undefined + } + ] + } + } else { + return { + action: 'add_player', + data: [ + { + uuid: 'a01e3843-e521-3998-958a-f459800e4d11', + name: 'Player', + properties: [], + gamemode: 0, + ping: 0, + displayName: undefined + } + ] + } + } + } } const values = { @@ -337,7 +378,9 @@ const values = { keySignature: [] } }, - IDSet: { ids: [2, 5] } + IDSet: { ids: [2, 5] }, + ItemSoundHolder: { soundId: 1 }, + ChatTypesHolder: { chatType: 1 } } function getValue (_type, packet) { diff --git a/test/serverTest.js b/test/serverTest.js index 197603422..586ee1a9a 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -83,7 +83,7 @@ for (const supportedVersion of mc.supportedVersions) { plainMessage: message, signedChatContent: '', unsignedChatContent: JSON.stringify({ text: message }), - type: mcData.supportFeature('incrementedChatType') ? { registryIndex: 1 } : 0, + type: mcData.supportFeature('chatTypeIsHolder') ? { chatType: 1 } : 0, senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random senderName: JSON.stringify({ text: sender }), senderTeam: undefined, From 6c2204a813690ead420e2b8c7f0ef32ca357d176 Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Tue, 1 Jul 2025 09:39:41 +0200 Subject: [PATCH 157/171] Release 1.58.0 (#1407) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 675633039..7cb1f4afd 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,9 @@ # History +## 1.58.0 +* [Fixes to protocol Holder implementation (#1355)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/7207b61f3a809ec9db01869a90c5ccaeafee4ca1) (thanks @extremeheat) +* [Bump @types/node from 22.15.33 to 24.0.4 (#1405)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085) (thanks @dependabot[bot]) + ## 1.57.0 * [Update package.json to remove lodash.get](https://github.com/PrismarineJS/node-minecraft-protocol/commit/031f13fea45264775311ae82e5a4efe74ebba96d) (thanks @rom1504) diff --git a/package.json b/package.json index 88fd264b7..d2a878392 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.57.0", + "version": "1.58.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 0f6da566975a262e4e6f12a11e9b311496682508 Mon Sep 17 00:00:00 2001 From: Divy Date: Sun, 27 Jul 2025 02:03:35 -0700 Subject: [PATCH 158/171] fix: Wrap base64-encoded PEM with 64-char line boundary (#1292) According to [RFC7468](https://datatracker.ietf.org/doc/html/rfc7468) > Generators MUST wrap the base64-encoded lines so that each line consists of exactly 64 characters except for the final line, which will encode the remainder of the data (within the 64-character line boundary), and they MUST NOT emit extraneous whitespace. Parsers can avoid branching and prevent timing sidechannel attacks. Ref https://arxiv.org/pdf/2108.04600.pdf Fixes compatibility with Deno as it enforces stricter handling of PEM. --- src/client/encrypt.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/encrypt.js b/src/client/encrypt.js index b9d21bab9..63cc2bd96 100644 --- a/src/client/encrypt.js +++ b/src/client/encrypt.js @@ -79,7 +79,7 @@ module.exports = function (client, options) { function mcPubKeyToPem (mcPubKeyBuffer) { let pem = '-----BEGIN PUBLIC KEY-----\n' let base64PubKey = mcPubKeyBuffer.toString('base64') - const maxLineLength = 65 + const maxLineLength = 64 while (base64PubKey.length > 0) { pem += base64PubKey.substring(0, maxLineLength) + '\n' base64PubKey = base64PubKey.substring(maxLineLength) From 3918d745ea52177148859e3e13011fcb918e794d Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Sat, 2 Aug 2025 11:28:42 +0200 Subject: [PATCH 159/171] Release 1.59.0 (#1414) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 7cb1f4afd..b4a235791 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.59.0 +* [fix: Wrap base64-encoded PEM with 64-char line boundary (#1292)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/0f6da566975a262e4e6f12a11e9b311496682508) (thanks @littledivy) + ## 1.58.0 * [Fixes to protocol Holder implementation (#1355)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/7207b61f3a809ec9db01869a90c5ccaeafee4ca1) (thanks @extremeheat) * [Bump @types/node from 22.15.33 to 24.0.4 (#1405)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/1e38d8fc1e1bbe7aa834055cfd38ed0fa22c2085) (thanks @dependabot[bot]) diff --git a/package.json b/package.json index d2a878392..b91c6ec12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.58.0", + "version": "1.59.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 2467716b6f52d2c83a6c2173bcfa686d4a437868 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sat, 2 Aug 2025 06:13:17 -0400 Subject: [PATCH 160/171] 1.21.5 (#1408) * 1.21.5 * Update client.js debug logging * Update packetTest.js slots * Update ci.yml --------- Co-authored-by: Romain Beaumont --- .gitignore | 3 ++- docs/README.md | 2 +- examples/server/server.js | 5 +++++ .../server_helloworld/server_helloworld.js | 4 ++++ src/client.js | 13 +++++++----- src/client/chat.js | 13 +++++++++--- src/datatypes/checksums.js | 20 +++++++++++++++++++ src/server/chat.js | 9 +++++++++ src/version.js | 4 ++-- test/clientTest.js | 11 +++++----- test/packetTest.js | 14 ++++++++++++- test/serverTest.js | 2 ++ 12 files changed, 82 insertions(+), 18 deletions(-) create mode 100644 src/datatypes/checksums.js diff --git a/.gitignore b/.gitignore index f6816b81a..2661e97d9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ test/npm-debug.log test/server_* package-lock.json versions/ -src/client/*.json \ No newline at end of file +src/client/*.json +test_* \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index ebc80984b..f1658b20f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6), 1.21 (1.21, 1.21.1, 1.21.3, 1.21.4) + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6), 1.21 (1.21, 1.21.1, 1.21.3, 1.21.4, 1.21.5) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. diff --git a/examples/server/server.js b/examples/server/server.js index d22078591..f503bb4cb 100644 --- a/examples/server/server.js +++ b/examples/server/server.js @@ -11,6 +11,10 @@ const options = { const server = mc.createServer(options) const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket + +// Global chat index counter for 1.21.5+ +let nextChatIndex = 1 + function chatText (text) { return mcData.supportFeature('chatPacketsUseNbtComponents') ? nbt.comp({ text: nbt.string(text) }) @@ -72,6 +76,7 @@ server.on('listening', function () { function sendBroadcastMessage (server, clients, message, sender) { if (mcData.supportFeature('signedChat')) { server.writeToClients(clients, 'player_chat', { + globalIndex: nextChatIndex++, plainMessage: message, signedChatContent: '', unsignedChatContent: chatText(message), diff --git a/examples/server_helloworld/server_helloworld.js b/examples/server_helloworld/server_helloworld.js index 1c2b31212..d213e6ec0 100644 --- a/examples/server_helloworld/server_helloworld.js +++ b/examples/server_helloworld/server_helloworld.js @@ -10,6 +10,9 @@ const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket const nbt = require('prismarine-nbt') +// Global chat index counter for 1.21.5+ +let nextChatIndex = 1 + function chatText (text) { return mcData.supportFeature('chatPacketsUseNbtComponents') ? nbt.comp({ text: nbt.string(text) }) @@ -65,6 +68,7 @@ server.on('playerJoin', function (client) { } if (mcData.supportFeature('signedChat')) { client.write('player_chat', { + globalIndex: nextChatIndex++, plainMessage: message, signedChatContent: '', unsignedChatContent: chatText(message), diff --git a/src/client.js b/src/client.js index 5c7a62b01..e369e77d0 100644 --- a/src/client.js +++ b/src/client.js @@ -1,9 +1,10 @@ 'use strict' const EventEmitter = require('events').EventEmitter -const debug = require('debug')('minecraft-protocol') const compression = require('./transforms/compression') const framing = require('./transforms/framing') const states = require('./states') +const debug = require('debug')('minecraft-protocol') +const debugSkip = process.env.DEBUG_SKIP?.split(',') ?? [] const createSerializer = require('./transforms/serializer').createSerializer const createDeserializer = require('./transforms/serializer').createDeserializer @@ -89,8 +90,8 @@ class Client extends EventEmitter { 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) { + if (debug.enabled && !debugSkip.includes(parsed.metadata.name)) { + debug('read packet ' + state + '.' + parsed.metadata.name) const s = JSON.stringify(parsed.data, null, 2) debug(s && s.length > 10000 ? parsed.data : s) } @@ -238,8 +239,10 @@ class Client extends EventEmitter { write (name, params) { if (!this.serializer.writable) { return } - debug('writing packet ' + this.state + '.' + name) - debug(params) + if (debug.enabled && !debugSkip.includes(name)) { + debug('writing packet ' + this.state + '.' + name) + debug(params) + } this.serializer.write({ name, params }) } diff --git a/src/client/chat.js b/src/client/chat.js index 8d0869b15..a50f4b988 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -1,4 +1,5 @@ const crypto = require('crypto') +const { computeChatChecksum } = require('../datatypes/checksums') const concat = require('../transforms/binaryStream').concat const { processNbtMessage } = require('prismarine-chat') const messageExpireTime = 420000 // 7 minutes (ms) @@ -192,6 +193,7 @@ module.exports = function (client, options) { const verified = !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired if (verified) client._signatureCache.push(packet.signature) client.emit('playerChat', { + globalIndex: packet.globalIndex, plainMessage: packet.plainMessage, unsignedContent: processMessage(packet.unsignedChatContent), type: packet.type, @@ -363,14 +365,16 @@ module.exports = function (client, options) { if (mcData.supportFeature('useChatSessions')) { // 1.19.3+ const { acknowledged, acknowledgements } = getAcknowledgements() const canSign = client.profileKeys && client._session - client.write((mcData.supportFeature('seperateSignedChatCommandPacket') && canSign) ? 'chat_command_signed' : 'chat_command', { + const chatPacket = { command, timestamp: options.timestamp, salt: options.salt, argumentSignatures: canSign ? signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements) : [], messageCount: client._lastSeenMessages.pending, + checksum: computeChatChecksum(client._lastSeenMessages), // 1.21.5+ acknowledged - }) + } + client.write((mcData.supportFeature('seperateSignedChatCommandPacket') && canSign) ? 'chat_command_signed' : 'chat_command', chatPacket) client._lastSeenMessages.pending = 0 } else { client.write('chat_command', { @@ -383,6 +387,7 @@ module.exports = function (client, options) { messageSender: e.sender, messageSignature: e.signature })), + checksum: computeChatChecksum(client._lastSeenMessages), lastRejectedMessage: client._lastRejectedMessage }) } @@ -398,6 +403,7 @@ module.exports = function (client, options) { salt: options.salt, signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined, offset: client._lastSeenMessages.pending, + checksum: computeChatChecksum(client._lastSeenMessages), // 1.21.5+ acknowledged }) client._lastSeenMessages.pending = 0 @@ -416,7 +422,8 @@ module.exports = function (client, options) { messageSender: e.sender, messageSignature: e.signature })), - lastRejectedMessage: client._lastRejectedMessage + lastRejectedMessage: client._lastRejectedMessage, + checksum: computeChatChecksum(client._lastSeenMessages) // 1.21.5+ }) client._lastSeenMessages.pending = 0 } else if (client.serverFeatures.chatPreview) { diff --git a/src/datatypes/checksums.js b/src/datatypes/checksums.js new file mode 100644 index 000000000..1d516c70e --- /dev/null +++ b/src/datatypes/checksums.js @@ -0,0 +1,20 @@ +// Compute chat checksum using Java's Arrays.hashCode algorithm to match vanilla client +function computeChatChecksum (lastSeenMessages) { + if (!lastSeenMessages || lastSeenMessages.length === 0) return 1 + + let checksum = 1 + for (const message of lastSeenMessages) { + if (message.signature) { + let sigHash = 1 + for (let i = 0; i < message.signature.length; i++) { + sigHash = (31 * sigHash + message.signature[i]) & 0xffffffff + } + checksum = (31 * checksum + sigHash) & 0xffffffff + } + } + // Convert to byte + const result = checksum & 0xff + return result === 0 ? 1 : result +} + +module.exports = { computeChatChecksum } diff --git a/src/server/chat.js b/src/server/chat.js index dc9dc3b04..2258fdc68 100644 --- a/src/server/chat.js +++ b/src/server/chat.js @@ -1,4 +1,5 @@ const crypto = require('crypto') +const { computeChatChecksum } = require('../datatypes/checksums') const concat = require('../transforms/binaryStream').concat const debug = require('debug')('minecraft-protocol') const messageExpireTime = 300000 // 5 min (ms) @@ -104,6 +105,14 @@ module.exports = function (client, server, options) { } lastTimestamp = packet.timestamp + // Validate checksum for 1.21.5+ + if (client.supportFeature('chatGlobalIndexAndChecksum') && options.enforceChatChecksum && packet.checksum !== undefined) { + const expectedChecksum = computeChatChecksum(client._lastSeenMessages || []) + if (packet.checksum !== 0 && packet.checksum !== expectedChecksum) { + return raise('multiplayer.disconnect.chat_validation_failed') + } + } + // Checks here: 1) make sure client can chat, 2) chain/session is OK, 3) signature is OK, 4) log if expired if (client.settings.disabledChat) return raise('chat.disabled.options') if (client.supportFeature('chainedChatWithHashing')) validateMessageChain(packet) // 1.19.1 diff --git a/src/version.js b/src/version.js index dfed3bb26..8629df338 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.21.1', - supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.6', '1.21.1', '1.21.3', '1.21.4'] + defaultVersion: '1.21.5', + supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.6', '1.21.1', '1.21.3', '1.21.4', '1.21.5'] } diff --git a/test/clientTest.js b/test/clientTest.js index ef46d3462..63c2be290 100644 --- a/test/clientTest.js +++ b/test/clientTest.js @@ -100,6 +100,8 @@ for (const supportedVersion of mc.supportedVersions) { }) }) + // chat/Style.java + const CLICK_EVENT = mcData.version['>=']('1.21.5') ? 'click_event' : 'clickEvent' it('connects successfully - offline mode', function (done) { const client = applyClientHelpers(mc.createClient({ username: 'Player', @@ -198,10 +200,9 @@ for (const supportedVersion of mc.supportedVersions) { if (chatCount === 1) { assert.strictEqual(plainMessage, 'hello everyone; I have logged in.') - assert.deepEqual(sender.clickEvent, { - action: 'suggest_command', - value: '/tell Player ' - }) + const clickEvent = sender[CLICK_EVENT] + assert.strictEqual(clickEvent.action, 'suggest_command') + assert.ok(['/tell Player ', '/msg Player '].includes(clickEvent.value || clickEvent.command)) assert.strictEqual(sender.text, 'Player') } else if (chatCount === 2) { const plainSender = client.parseMessage(sender).toString() @@ -222,7 +223,7 @@ for (const supportedVersion of mc.supportedVersions) { const message = JSON.parse(data.formattedMessage) if (chatCount === 1) { assert.strictEqual(message.translate, 'chat.type.text') - assert.deepEqual(message.with[0].clickEvent, { + assert.deepEqual(message.with[0][CLICK_EVENT], { action: 'suggest_command', value: mcData.version.version > 340 ? '/tell Player ' : '/msg Player ' }) diff --git a/test/packetTest.js b/test/packetTest.js index 2f5d65228..0ec49ccdb 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -213,12 +213,17 @@ const values = { vec4f: { x: 0, y: 0, z: 0, w: 0 }, + vec3i: { + x: 0, y: 0, z: 0 + }, count: 1, // TODO : might want to set this to a correct value bool: true, f64: 99999.2222, f32: -333.444, slot: slotValue, Slot: slotValue, + UntrustedSlot: slotValue, + HashedSlot: slotValue, SlotComponent: { type: 'hide_tooltip' }, @@ -380,7 +385,14 @@ const values = { }, IDSet: { ids: [2, 5] }, ItemSoundHolder: { soundId: 1 }, - ChatTypesHolder: { chatType: 1 } + ChatTypesHolder: { chatType: 1 }, + ExactComponentMatcher: [], + HashedStack: { + itemId: 1, + count: 1, + addedComponents: [], + removedComponents: [] + } } function getValue (_type, packet) { diff --git a/test/serverTest.js b/test/serverTest.js index 586ee1a9a..b2e515408 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -76,10 +76,12 @@ for (const supportedVersion of mc.supportedVersions) { // more to be added } } + let nextChatIndex = 1 function sendBroadcastMessage (server, clients, message, sender) { if (mcData.supportFeature('signedChat')) { server.writeToClients(clients, 'player_chat', { + globalIndex: nextChatIndex++, // 1.21.5+ plainMessage: message, signedChatContent: '', unsignedChatContent: JSON.stringify({ text: message }), From ca5af6307a817b655a76c82f56a4c6c78e272453 Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Sat, 2 Aug 2025 12:28:55 +0200 Subject: [PATCH 161/171] Release 1.60.0 (#1415) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index b4a235791..1acddc77e 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.60.0 +* [1.21.5 (#1408)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/2467716b6f52d2c83a6c2173bcfa686d4a437868) (thanks @extremeheat) + ## 1.59.0 * [fix: Wrap base64-encoded PEM with 64-char line boundary (#1292)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/0f6da566975a262e4e6f12a11e9b311496682508) (thanks @littledivy) diff --git a/package.json b/package.json index b91c6ec12..d51ecb327 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.59.0", + "version": "1.60.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 74aea5a5d6a495bfdfc7e97b164524f6a0c6a203 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sun, 3 Aug 2025 08:06:40 +0200 Subject: [PATCH 162/171] fix leak on entering config state twice (#1381) --- src/client/play.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/play.js b/src/client/play.js index 559607f34..4dc1c3139 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -53,7 +53,7 @@ module.exports = function (client, options) { client.write('configuration_acknowledged', {}) } client.state = states.CONFIGURATION - client.on('select_known_packs', () => { + client.once('select_known_packs', () => { client.write('select_known_packs', { packs: [] }) }) // Server should send finish_configuration on its own right after sending the client a dimension codec From 5242498797e1e9877f9b0056ff608fb68e0a3508 Mon Sep 17 00:00:00 2001 From: notsapinho <52896767+notsapinho@users.noreply.github.com> Date: Sun, 3 Aug 2025 03:07:08 -0300 Subject: [PATCH 163/171] types: add missing config state (#1397) --- src/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.d.ts b/src/index.d.ts index e61d5403b..bf52ca63d 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -237,6 +237,7 @@ declare module 'minecraft-protocol' { HANDSHAKING = 'handshaking', LOGIN = 'login', PLAY = 'play', + CONFIGURATION = 'configuration', STATUS = 'status', } From 2119b04f52605f62df2d029908d92d6443aa68db Mon Sep 17 00:00:00 2001 From: PiotrW01 <96948655+PiotrW01@users.noreply.github.com> Date: Sun, 3 Aug 2025 08:23:02 +0200 Subject: [PATCH 164/171] Fix undefined chat message signatures causing crash (#1413) * Fix undefined chat message signatures causing crash * Update src/client/chat.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update chat.js lint --------- Co-authored-by: extremeheat Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/client/chat.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/client/chat.js b/src/client/chat.js index a50f4b988..002187099 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -59,8 +59,14 @@ module.exports = function (client, options) { if (player.hasChainIntegrity) { const length = Buffer.byteLength(message, 'utf8') - const acknowledgements = previousMessages.length > 0 ? ['i32', previousMessages.length, 'buffer', Buffer.concat(...previousMessages.map(msg => msg.signature || client._signatureCache[msg.id]))] : ['i32', 0] - + const validBuffers = previousMessages + .filter(msg => msg.signature || client._signatureCache[msg.id]) // Filter out invalid messages + .map(msg => msg.signature || client._signatureCache[msg.id]) + .filter(buf => Buffer.isBuffer(buf)) + + const acknowledgements = validBuffers.length > 0 + ? ['i32', validBuffers.length, 'buffer', Buffer.concat(validBuffers)] + : ['i32', 0] const signable = concat('i32', 1, 'UUID', uuid, 'UUID', player.sessionUuid, 'i32', index, 'i64', salt, 'i64', timestamp / 1000n, 'i32', length, 'pstring', message, ...acknowledgements) player.hasChainIntegrity = crypto.verify('RSA-SHA256', signable, player.publicKey, currentSignature) From b404bcaed4c039c5558e889c8617aa866cd7bddb Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Wed, 6 Aug 2025 00:17:20 +0200 Subject: [PATCH 165/171] Release 1.60.1 (#1418) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 1acddc77e..ec6bb957f 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,10 @@ # History +## 1.60.1 +* [Fix undefined chat message signatures causing crash (#1413)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/2119b04f52605f62df2d029908d92d6443aa68db) (thanks @PiotrW01) +* [types: add missing config state (#1397)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/5242498797e1e9877f9b0056ff608fb68e0a3508) (thanks @notsapinho) +* [fix leak on entering config state twice (#1381)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/74aea5a5d6a495bfdfc7e97b164524f6a0c6a203) (thanks @zardoy) + ## 1.60.0 * [1.21.5 (#1408)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/2467716b6f52d2c83a6c2173bcfa686d4a437868) (thanks @extremeheat) diff --git a/package.json b/package.json index d51ecb327..13540f898 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.60.0", + "version": "1.60.1", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 0bfd970e954f1ab5e579ebe51cbefbf07d952939 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sun, 17 Aug 2025 16:56:59 -0400 Subject: [PATCH 166/171] 1.21.6 (#1416) * start 1.21.6 * fix packettest * update * fix * Update ci.yml --------- Co-authored-by: Romain Beaumont --- .github/CROSS_REPO_TRIGGER.md | 96 +++++++++++++++++++ .github/helper/updator.js | 80 ++++++++++++++++ .../workflows/update-from-minecraft-data.yml | 61 ++++++++++++ docs/README.md | 1 + src/version.js | 4 +- test/cyclePacketTest.js | 5 +- test/packetTest.js | 23 +++++ 7 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 .github/CROSS_REPO_TRIGGER.md create mode 100644 .github/helper/updator.js create mode 100644 .github/workflows/update-from-minecraft-data.yml diff --git a/.github/CROSS_REPO_TRIGGER.md b/.github/CROSS_REPO_TRIGGER.md new file mode 100644 index 000000000..ad0ae6e88 --- /dev/null +++ b/.github/CROSS_REPO_TRIGGER.md @@ -0,0 +1,96 @@ +# Cross-Repository Workflow Trigger Setup + +This document explains how to set up cross-repository workflow triggering between the minecraft-data repository and this repository. + +## Overview + +The workflow `update-from-minecraft-data.yml` can be triggered from the minecraft-data repository in two ways: + +1. **Manual Workflow Dispatch** - Triggered manually or programmatically +2. **Repository Dispatch** - Triggered via webhook/API call + +## Setup in minecraft-data repository + +### Method 1: Workflow Dispatch (Recommended) + +Add this step to a workflow in the minecraft-data repository: + +```yaml +- name: Trigger update in node-minecraft-protocol + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.CROSS_REPO_TOKEN }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: 'extremeheat', + repo: 'node-minecraft-protocol', + workflow_id: 'update-from-minecraft-data.yml', + ref: 'master', // or the target branch + inputs: { + trigger_source: 'minecraft-data', + trigger_reason: 'data_update', + data_version: '${{ steps.get_version.outputs.version }}' // or your version variable + } + }); +``` + +### Method 2: Repository Dispatch + +```yaml +- name: Trigger update in node-minecraft-protocol + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.CROSS_REPO_TOKEN }} + script: | + await github.rest.repos.createDispatchEvent({ + owner: 'extremeheat', + repo: 'node-minecraft-protocol', + event_type: 'minecraft-data-update', + client_payload: { + repository: 'minecraft-data', + reason: 'data_update', + version: '${{ steps.get_version.outputs.version }}' + } + }); +``` + +## Required Secrets + +You need to create a Personal Access Token (PAT) with the following permissions: +- `repo` scope (for private repositories) +- `public_repo` scope (for public repositories) +- `actions:write` permission + +Add this token as a secret named `CROSS_REPO_TOKEN` in the minecraft-data repository. + +## Token Setup Steps + +1. Go to GitHub Settings > Developer settings > Personal access tokens > Tokens (classic) +2. Generate a new token with appropriate permissions +3. Add the token as `CROSS_REPO_TOKEN` secret in minecraft-data repository settings + +## Customizing the Updator Script + +The updator script (`.github/helper/updator.js`) can be customized to: + +- Download and process minecraft-data updates +- Update protocol definitions +- Run tests to verify compatibility +- Create pull requests for review +- Send notifications + +## Testing + +You can test the workflow manually by: + +1. Going to the Actions tab in this repository +2. Selecting "Update from minecraft-data" workflow +3. Clicking "Run workflow" +4. Providing test inputs + +## Security Considerations + +- Use repository secrets for sensitive tokens +- Limit token permissions to minimum required +- Consider using short-lived tokens or GitHub Apps for enhanced security +- Review and approve automatic commits/PRs if needed diff --git a/.github/helper/updator.js b/.github/helper/updator.js new file mode 100644 index 000000000..6697f3e26 --- /dev/null +++ b/.github/helper/updator.js @@ -0,0 +1,80 @@ +#!/usr/bin/env node +/** + * Updator script triggered from minecraft-data repository + * This script can be customized to handle updates from minecraft-data + */ +const github = require('gh-helpers')() +const fs = require('fs') +const cp = require('child_process') +const { join } = require('path') +const exec = (cmd) => github.mock ? console.log('> ', cmd) : (console.log('> ', cmd), cp.execSync(cmd, { stdio: 'inherit' })) + +console.log('Starting update process...') +const triggerBranch = process.env.TRIGGER_SOURCE +const newVersion = process.env.DATA_VERSION +const onBehalfOf = process.env.TRIGGER_REASON || 'workflow_dispatch' +console.log('Trigger reason:', onBehalfOf) +console.log('New version:', newVersion) + +if (!newVersion) { + console.error('No new version provided. Exiting...') + process.exit(1) +} + +async function main () { + const currentSupportedPath = require.resolve('../../src/version.js') + const readmePath = join(__dirname, '../../docs/README.md') + const ciPath = join(__dirname, '../../.github/workflows/ci.yml') + + // Update the version.js + const currentSupportedVersion = require('../../src/version.js') + const currentContents = fs.readFileSync(currentSupportedPath, 'utf8') + console.log('Current supported version:', currentContents) + const newContents = currentContents.includes(newVersion) + ? currentContents + : currentContents + .replace(`: '${currentSupportedVersion.defaultVersion}'`, `: '${newVersion}'`) + .replace(`, '${currentSupportedVersion.defaultVersion}'`, `, '${currentSupportedVersion.defaultVersion}', '${newVersion}'`) + + // Update the README.md + const currentContentsReadme = fs.readFileSync(readmePath, 'utf8') + if (!currentContentsReadme.includes(newVersion)) { + const newReadmeContents = currentContentsReadme.replace(' ', `, ${newVersion} `) + fs.writeFileSync(readmePath, newReadmeContents) + console.log('Updated README with new version:', newVersion) + } + fs.writeFileSync(currentSupportedPath, newContents) + + // Update the CI workflow + const currentContentsCI = fs.readFileSync(ciPath, 'utf8') + if (!currentContentsCI.includes(newVersion)) { + const newCIContents = currentContentsCI.replace( + ' run: npm install', ` + run: npm install + - run: cd node_modules && cd minecraft-data && mv minecraft-data minecraft-data-old && git clone -b ${triggerBranch} https://github.com/PrismarineJS/minecraft-data --depth 1 && node bin/generate_data.js + - run: curl -o node_modules/protodef/src/serializer.js https://raw.githubusercontent.com/extremeheat/node-protodef/refs/heads/dlog/src/serializer.js && curl -o node_modules/protodef/src/compiler.js https://raw.githubusercontent.com/extremeheat/node-protodef/refs/heads/dlog/src/compiler.js +`) + fs.writeFileSync(ciPath, newCIContents) + console.log('Updated CI workflow with new version:', newVersion) + } + + const branchName = 'pc' + newVersion.replace(/[^a-zA-Z0-9_]/g, '.') + exec(`git checkout -b ${branchName}`) + exec('git add --all') + exec(`git commit -m "Update to version ${newVersion}"`) + exec(`git push origin ${branchName}`) + // createPullRequest(title: string, body: string, fromBranch: string, intoBranch?: string): Promise<{ number: number, url: string }>; + const pr = await github.createPullRequest( + `${newVersion} updates`, + `Automatically generated PR for Minecraft version ${newVersion}.\n\nRef: ${onBehalfOf}`, + branchName, + 'master' + ) + console.log(`Pull request created: ${pr.url} (PR #${pr.number})`) + console.log('Update process completed successfully!') +} + +main().catch(err => { + console.error('Error during update process:', err) + process.exit(1) +}) diff --git a/.github/workflows/update-from-minecraft-data.yml b/.github/workflows/update-from-minecraft-data.yml new file mode 100644 index 000000000..667d6b84f --- /dev/null +++ b/.github/workflows/update-from-minecraft-data.yml @@ -0,0 +1,61 @@ +name: Update from minecraft-data + +# This workflow can be triggered from external repositories (like minecraft-data) +# via workflow_dispatch API call or repository_dispatch webhook +on: + # Allow manual triggering + workflow_dispatch: + inputs: + trigger_source: + description: 'Repository branch that triggered this update' + required: false + default: 'minecraft-data' + type: string + trigger_reason: + description: 'What PR or issue triggered this update' + required: false + default: '' + type: string + data_version: + description: 'MC Version that triggered this update' + required: false + type: string + + # Listen for repository dispatch events (webhook-based triggers) + repository_dispatch: + types: [minecraft-data-update] + +jobs: + update: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # Use a token that has write permissions if you need to push changes + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm install gh-helpers + + - name: Set environment variables + run: | + echo "TRIGGER_SOURCE=${{ github.event.inputs.trigger_source || github.event.client_payload.repository || 'minecraft-data' }}" >> $GITHUB_ENV + echo "TRIGGER_REASON=${{ github.event.inputs.trigger_reason || github.event.client_payload.reason || 'repository_dispatch' }}" >> $GITHUB_ENV + echo "DATA_VERSION=${{ github.event.inputs.data_version || github.event.client_payload.version || 'unknown' }}" >> $GITHUB_ENV + + - name: Run updator script + run: node .github/helper/updator.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TRIGGER_SOURCE: ${{ env.TRIGGER_SOURCE }} + TRIGGER_REASON: ${{ env.TRIGGER_REASON }} + DATA_VERSION: ${{ env.DATA_VERSION }} + diff --git a/docs/README.md b/docs/README.md index f1658b20f..41a87b6a2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,6 +14,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6), 1.21 (1.21, 1.21.1, 1.21.3, 1.21.4, 1.21.5) + , 1.21.6 * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. diff --git a/src/version.js b/src/version.js index 8629df338..c575165d4 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.21.5', - supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.6', '1.21.1', '1.21.3', '1.21.4', '1.21.5'] + defaultVersion: '1.21.6', + supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.6', '1.21.1', '1.21.3', '1.21.4', '1.21.5', '1.21.6'] } diff --git a/test/cyclePacketTest.js b/test/cyclePacketTest.js index cd8693e97..1a224d8da 100644 --- a/test/cyclePacketTest.js +++ b/test/cyclePacketTest.js @@ -25,6 +25,8 @@ for (const supportedVersion of supportedVersions) { const parsedBuffer = convertObjectToBuffer(parsed) const areEq = buffer.equals(parsedBuffer) if (!areEq) { + console.log(`Error when testing ${+packetIx + 1} ${packetName} packet`) + console.direct(parsed, { depth: null }) console.log('original buffer', buffer.toString('hex')) console.log('cycled buffer', parsedBuffer.toString('hex')) } @@ -46,7 +48,8 @@ for (const supportedVersion of supportedVersions) { // server -> client if (data !== undefined) { Object.entries(data['from-server']).forEach(([packetName, packetData]) => { - it(`${packetName} packet`, () => { + it(`${packetName} packet`, function () { + if (packetName === 'sound_effect') return this.skip() // sound_effect structure is out of date in minecraft-packets for (const i in packetData) { testBuffer(packetData[i].raw, [packetName, i]) } diff --git a/test/packetTest.js b/test/packetTest.js index 0ec49ccdb..81ee16958 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -56,6 +56,22 @@ const nbtValue = { } function getFixedPacketPayload (version, packetName) { + if (packetName === 'teams') { + if (version['>=']('1.21.6')) { + return { + team: 'test_team', + mode: 'add', + name: nbtValue, + flags: 'always', + nameTagVisibility: 'always', + collisionRule: 'always', + formatting: 0, // no formatting + prefix: nbtValue, + suffix: nbtValue, + players: ['player1', 'player2'] + } + } + } if (packetName === 'declare_recipes') { if (version['>=']('1.21.3')) { return { @@ -319,6 +335,9 @@ const values = { }) return results }, + registryEntryHolder (typeArgs, context) { + return { [typeArgs.baseName]: 1 } + }, soundSource: 'master', packedChunkPos: { x: 10, @@ -392,6 +411,10 @@ const values = { count: 1, addedComponents: [], removedComponents: [] + }, + RecipeBookSetting: { + open: false, + filtering: false } } From c561917bf7e7966911321512c2a6895a3f9da074 Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Sun, 17 Aug 2025 22:58:35 +0200 Subject: [PATCH 167/171] Release 1.61.0 (#1422) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index ec6bb957f..ddbe6aa85 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.61.0 +* [1.21.6 (#1416)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/0bfd970e954f1ab5e579ebe51cbefbf07d952939) (thanks @extremeheat) + ## 1.60.1 * [Fix undefined chat message signatures causing crash (#1413)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/2119b04f52605f62df2d029908d92d6443aa68db) (thanks @PiotrW01) * [types: add missing config state (#1397)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/5242498797e1e9877f9b0056ff608fb68e0a3508) (thanks @notsapinho) diff --git a/package.json b/package.json index 13540f898..7b1f0f5b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.60.1", + "version": "1.61.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 8a99613672298b2e9a1e66ca41f8bb720cf1a439 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sun, 7 Sep 2025 09:56:51 +0200 Subject: [PATCH 168/171] Add support for Minecraft 1.21.8 (#1427) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for Minecraft 1.21.8 - Added 1.21.8 to supported versions list in src/version.js - Updated documentation in docs/README.md to include 1.21.8 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Set 1.21.8 as the new default version 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- docs/README.md | 2 +- src/version.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/README.md b/docs/README.md index 41a87b6a2..06129f370 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,7 +14,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6), 1.21 (1.21, 1.21.1, 1.21.3, 1.21.4, 1.21.5) - , 1.21.6 + , 1.21.6, 1.21.8 * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. diff --git a/src/version.js b/src/version.js index c575165d4..7525bd703 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.21.6', - supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.6', '1.21.1', '1.21.3', '1.21.4', '1.21.5', '1.21.6'] + defaultVersion: '1.21.8', + supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.6', '1.21.1', '1.21.3', '1.21.4', '1.21.5', '1.21.6', '1.21.8'] } From ad1f4ca5997873a0e3dcb0d8123ecf7e9690672f Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sun, 7 Sep 2025 04:00:51 -0400 Subject: [PATCH 169/171] Update update-from-minecraft-data.yml workflow (#1426) * update workflow * update workflow * fix git config * update workflow * fix token * wrong wf * update workflow * update workflow * dispatch to mineflayer * fix --------- Co-authored-by: Romain Beaumont --- .github/helper/package.json | 5 ++ .github/helper/updator.js | 62 ++++++++++++------- .github/workflows/commands.yml | 2 + .../workflows/update-from-minecraft-data.yml | 57 +++++------------ docs/README.md | 14 +++-- 5 files changed, 74 insertions(+), 66 deletions(-) create mode 100644 .github/helper/package.json diff --git a/.github/helper/package.json b/.github/helper/package.json new file mode 100644 index 000000000..5f88a010b --- /dev/null +++ b/.github/helper/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "gh-helpers": "^1.0.0" + } +} \ No newline at end of file diff --git a/.github/helper/updator.js b/.github/helper/updator.js index 6697f3e26..9245b80f2 100644 --- a/.github/helper/updator.js +++ b/.github/helper/updator.js @@ -1,25 +1,23 @@ #!/usr/bin/env node /** - * Updator script triggered from minecraft-data repository - * This script can be customized to handle updates from minecraft-data + * Updator script triggered from minecraft-data repository to auto generate PR */ -const github = require('gh-helpers')() const fs = require('fs') const cp = require('child_process') +const assert = require('assert') +const github = require('gh-helpers')() const { join } = require('path') const exec = (cmd) => github.mock ? console.log('> ', cmd) : (console.log('> ', cmd), cp.execSync(cmd, { stdio: 'inherit' })) console.log('Starting update process...') -const triggerBranch = process.env.TRIGGER_SOURCE -const newVersion = process.env.DATA_VERSION -const onBehalfOf = process.env.TRIGGER_REASON || 'workflow_dispatch' -console.log('Trigger reason:', onBehalfOf) -console.log('New version:', newVersion) +// Sanitize and validate environment variables all non alpha numeric / underscore / dot +const newVersion = process.env.NEW_MC_VERSION?.replace(/[^a-zA-Z0-9_.]/g, '_') +const triggerBranch = process.env.MCDATA_BRANCH?.replace(/[^a-zA-Z0-9_.]/g, '_') +const mcdataPrURL = process.env.MCDATA_PR_URL +console.log({ newVersion, triggerBranch, mcdataPrURL }) -if (!newVersion) { - console.error('No new version provided. Exiting...') - process.exit(1) -} +assert(newVersion) +assert(triggerBranch) async function main () { const currentSupportedPath = require.resolve('../../src/version.js') @@ -39,7 +37,7 @@ async function main () { // Update the README.md const currentContentsReadme = fs.readFileSync(readmePath, 'utf8') if (!currentContentsReadme.includes(newVersion)) { - const newReadmeContents = currentContentsReadme.replace(' ', `, ${newVersion} `) + const newReadmeContents = currentContentsReadme.replace('\n', `, ${newVersion}\n`) fs.writeFileSync(readmePath, newReadmeContents) console.log('Updated README with new version:', newVersion) } @@ -49,8 +47,7 @@ async function main () { const currentContentsCI = fs.readFileSync(ciPath, 'utf8') if (!currentContentsCI.includes(newVersion)) { const newCIContents = currentContentsCI.replace( - ' run: npm install', ` - run: npm install + 'run: npm install', `run: npm install - run: cd node_modules && cd minecraft-data && mv minecraft-data minecraft-data-old && git clone -b ${triggerBranch} https://github.com/PrismarineJS/minecraft-data --depth 1 && node bin/generate_data.js - run: curl -o node_modules/protodef/src/serializer.js https://raw.githubusercontent.com/extremeheat/node-protodef/refs/heads/dlog/src/serializer.js && curl -o node_modules/protodef/src/compiler.js https://raw.githubusercontent.com/extremeheat/node-protodef/refs/heads/dlog/src/compiler.js `) @@ -58,20 +55,43 @@ async function main () { console.log('Updated CI workflow with new version:', newVersion) } - const branchName = 'pc' + newVersion.replace(/[^a-zA-Z0-9_]/g, '.') + const branchName = 'pc' + newVersion.replace(/[^a-zA-Z0-9_]/g, '_') exec(`git checkout -b ${branchName}`) + exec('git config user.name "github-actions[bot]"') + exec('git config user.email "41898282+github-actions[bot]@users.noreply.github.com"') exec('git add --all') exec(`git commit -m "Update to version ${newVersion}"`) - exec(`git push origin ${branchName}`) + exec(`git push origin ${branchName} --force`) // createPullRequest(title: string, body: string, fromBranch: string, intoBranch?: string): Promise<{ number: number, url: string }>; const pr = await github.createPullRequest( - `${newVersion} updates`, - `Automatically generated PR for Minecraft version ${newVersion}.\n\nRef: ${onBehalfOf}`, + `🎈 ${newVersion}`, + `This automated PR sets up the relevant boilerplate for Minecraft version ${newVersion}. + +Ref: ${mcdataPrURL} + +* You can help contribute to this PR by opening a PR against this ${branchName} branch instead of master. + `, branchName, 'master' ) - console.log(`Pull request created: ${pr.url} (PR #${pr.number})`) - console.log('Update process completed successfully!') + console.log(`Pull request created`, pr) + + // Ask mineflayer to handle new update + const nodeDispatchPayload = { + owner: 'PrismarineJS', + repo: 'mineflayer', + workflow: 'handle-update.yml', + branch: 'master', + inputs: { + new_mc_version: newVersion, + mcdata_branch: triggerBranch, + mcdata_pr_url: mcdataPrURL, + nmp_branch: branchName, + nmp_pr_url: pr.url + } + } + console.log('Sending workflow dispatch', nodeDispatchPayload) + await github.sendWorkflowDispatch(nodeDispatchPayload) } main().catch(err => { diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index d2286e26e..40807af7f 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -5,6 +5,8 @@ on: types: [created] pull_request: # Handle renamed PRs types: [edited] +permissions: + contents: write jobs: comment-trigger: diff --git a/.github/workflows/update-from-minecraft-data.yml b/.github/workflows/update-from-minecraft-data.yml index 667d6b84f..752aad543 100644 --- a/.github/workflows/update-from-minecraft-data.yml +++ b/.github/workflows/update-from-minecraft-data.yml @@ -1,30 +1,22 @@ name: Update from minecraft-data -# This workflow can be triggered from external repositories (like minecraft-data) -# via workflow_dispatch API call or repository_dispatch webhook on: - # Allow manual triggering workflow_dispatch: inputs: - trigger_source: - description: 'Repository branch that triggered this update' - required: false - default: 'minecraft-data' + new_mc_version: + description: New minecraft version number + required: true type: string - trigger_reason: - description: 'What PR or issue triggered this update' - required: false - default: '' + mcdata_branch: + description: minecraft-data branch for this version + required: true type: string - data_version: - description: 'MC Version that triggered this update' + mcdata_pr_url: + description: minecraft-data PR number to open a PR here against required: false + default: '' type: string - # Listen for repository dispatch events (webhook-based triggers) - repository_dispatch: - types: [minecraft-data-update] - jobs: update: runs-on: ubuntu-latest @@ -33,29 +25,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - # Use a token that has write permissions if you need to push changes - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - cache: 'npm' - - - name: Install dependencies - run: npm install gh-helpers - - - name: Set environment variables - run: | - echo "TRIGGER_SOURCE=${{ github.event.inputs.trigger_source || github.event.client_payload.repository || 'minecraft-data' }}" >> $GITHUB_ENV - echo "TRIGGER_REASON=${{ github.event.inputs.trigger_reason || github.event.client_payload.reason || 'repository_dispatch' }}" >> $GITHUB_ENV - echo "DATA_VERSION=${{ github.event.inputs.data_version || github.event.client_payload.version || 'unknown' }}" >> $GITHUB_ENV - + token: ${{ secrets.PAT_PASSWORD }} + - name: Run updator script - run: node .github/helper/updator.js + run: cd .github/helper && npm install && node updator.js env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TRIGGER_SOURCE: ${{ env.TRIGGER_SOURCE }} - TRIGGER_REASON: ${{ env.TRIGGER_REASON }} - DATA_VERSION: ${{ env.DATA_VERSION }} - + GITHUB_TOKEN: ${{ secrets.PAT_PASSWORD }} + MCDATA_BRANCH: ${{ github.event.inputs.mcdata_branch }} + MCDATA_PR_URL: ${{ github.event.inputs.mcdata_pr_url }} + NEW_MC_VERSION: ${{ github.event.inputs.new_mc_version }} diff --git a/docs/README.md b/docs/README.md index 06129f370..028e05bf7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,10 +11,16 @@ Parse and serialize minecraft packets, plus authentication and encryption. ## Features - * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), - 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6), 1.21 (1.21, 1.21.1, 1.21.3, 1.21.4, 1.21.5) - , 1.21.6, 1.21.8 + * Supports Minecraft PC version + 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), + 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), + 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2),1.14 (1.14, 1.14.1, 1.14.3, 1.14.4), + 1.15 (1.15, 1.15.1, 1.15.2), 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), + 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), + 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6), + 1.21, 1.21.1, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.8 + + * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. From efee28e9b7ff2aa5ebaaf62bb30bad26c2310a8d Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Sun, 7 Sep 2025 10:01:59 +0200 Subject: [PATCH 170/171] Release 1.62.0 (#1428) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index ddbe6aa85..b5e08d096 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.62.0 +* [Add support for Minecraft 1.21.8 (#1427)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/8a99613672298b2e9a1e66ca41f8bb720cf1a439) (thanks @rom1504) + ## 1.61.0 * [1.21.6 (#1416)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/0bfd970e954f1ab5e579ebe51cbefbf07d952939) (thanks @extremeheat) diff --git a/package.json b/package.json index 7b1f0f5b6..63e1c75d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.61.0", + "version": "1.62.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sun, 7 Sep 2025 19:35:42 +0300 Subject: [PATCH 171/171] fix emitting playerJoin too early (#1424) --- src/server/login.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server/login.js b/src/server/login.js index 9d63912fb..ec40ed913 100644 --- a/src/server/login.js +++ b/src/server/login.js @@ -190,7 +190,6 @@ module.exports = function (client, server, options) { client.once('login_acknowledged', onClientLoginAck) } else { client.state = states.PLAY - server.emit('playerJoin', client) } client.settings = {} @@ -217,6 +216,9 @@ module.exports = function (client, server, options) { pluginChannels(client, options) if (client.supportFeature('signedChat')) chatPlugin(client, server, options) server.emit('login', client) + if (!client.supportFeature('hasConfigurationState')) { + server.emit('playerJoin', client) + } } function onClientLoginAck () {