From 0ecba87dfe3e07d90ba4567799b993e3c0a35383 Mon Sep 17 00:00:00 2001 From: Rob9315 Date: Mon, 25 Jul 2022 22:14:26 +0200 Subject: [PATCH] [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 --- package.json | 3 +- src/client/encrypt.js | 24 +++++- src/client/setProtocol.js | 1 + src/server/login.js | 49 +++++++++-- test/benchmark.js | 3 +- test/clientTest.js | 77 ++++++++++++++--- test/common/util.js | 94 +++++++++++++++++++- test/packetTest.js | 26 +++++- test/serverTest.js | 176 +++++++++++++++++--------------------- 9 files changed, 330 insertions(+), 123 deletions(-) diff --git a/package.json b/package.json index 988b3b9..f97bb08 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,8 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^3.7.3", + + "minecraft-data": "^3.8.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", diff --git a/src/client/encrypt.js b/src/client/encrypt.js index d9ff607..f4ff95c 100644 --- a/src/client/encrypt.js +++ b/src/client/encrypt.js @@ -42,13 +42,29 @@ 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')) { + // todo: add signature encryption + // starting 1.19.1 we will not be able to join + // the default server configuration without it + client.write('encryption_begin', { + sharedSecret: encryptedSharedSecretBuffer, + hasVerifyToken: true, + crypto: { + verifyToken: encryptedVerifyTokenBuffer + } + }) + } else { + client.write('encryption_begin', { + sharedSecret: encryptedSharedSecretBuffer, + verifyToken: encryptedVerifyTokenBuffer + }) + } client.setEncryption(sharedSecret) } } diff --git a/src/client/setProtocol.js b/src/client/setProtocol.js index 5eea652..9bbf595 100644 --- a/src/client/setProtocol.js +++ b/src/client/setProtocol.js @@ -27,6 +27,7 @@ module.exports = function (client, options) { client.write('login_start', { username: client.username }) + // TODO: add signature option } } } diff --git a/src/server/login.js b/src/server/login.js index e9f0b58..d5e9533 100644 --- a/src/server/login.js +++ b/src/server/login.js @@ -55,18 +55,48 @@ module.exports = function (client, server, options) { } function onEncryptionKeyResponse (packet) { + const mcData = require('minecraft-data')(client.version) + + let packetVerifyToken + let signature + + if (mcData.supportFeature('signatureEncryption')) { + if (packet.hasVerifyToken) { + packetVerifyToken = packet.crypto.verifyToken + } else { + signature = packet.crypto + } + } else { + packetVerifyToken = packet.verifyToken + } + 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 (packetVerifyToken) { + try { + const verifyToken = crypto.privateDecrypt({ + key: server.serverKey.exportKey(), + padding: crypto.constants.RSA_PKCS1_PADDING + }, packetVerifyToken) + if (!bufferEqual(client.verifyToken, verifyToken)) { + client.end('DidNotEncryptVerifyTokenProperly') + return + } + sharedSecret = crypto.privateDecrypt({ + key: server.serverKey.exportKey(), + padding: crypto.constants.RSA_PKCS1_PADDING + }, packet.sharedSecret) + } catch (e) { client.end('DidNotEncryptVerifyTokenProperly') return } - sharedSecret = crypto.privateDecrypt({ key: server.serverKey.exportKey(), padding: crypto.constants.RSA_PKCS1_PADDING }, packet.sharedSecret) - } catch (e) { - client.end('DidNotEncryptVerifyTokenProperly') + } else { + // todo: signature encryption + client.end('signature encryption not implemented') + console.error(signature) return } + client.setEncryption(sharedSecret) const isException = !!server.onlineModeExceptions[client.username.toLowerCase()] @@ -114,7 +144,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) diff --git a/test/benchmark.js b/test/benchmark.js index e31c901..4dc87f6 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 079b6e6..3ef0336 100644 --- a/test/clientTest.js +++ b/test/clientTest.js @@ -12,7 +12,7 @@ const Wrap = require('minecraft-wrap').Wrap const download = util.promisify(require('minecraft-wrap').download) -const { getPort } = require('./common/util') +const { getPort, chat } = require('./common/util') for (const supportedVersion of mc.supportedVersions) { let PORT = null @@ -104,6 +104,9 @@ for (const supportedVersion of mc.supportedVersions) { }) client.on('error', err => done(err)) const lineListener = function (line) { + // 1.19+ also prints Server like a player + if (line.match(/\[Server thread\/INFO\]: .*/)) return + const match = line.match(/\[Server thread\/INFO\]: <(.+?)> (.+)/) if (!match) return assert.strictEqual(match[1], 'Player') @@ -114,13 +117,13 @@ for (const supportedVersion of mc.supportedVersions) { let chatCount = 0 client.on('login', function (packet) { assert.strictEqual(packet.gameMode, 0) - client.write('chat', { - message: 'hello everyone; I have logged in.' - }) + chat(client, 'hello everyone; I have logged in.') }) + + // pre 1.19 named 'chat' + client.on('chat', function (packet) { chatCount += 1 - assert.ok(chatCount <= 2) const message = JSON.parse(packet.message) if (chatCount === 1) { assert.strictEqual(message.translate, 'chat.type.text') @@ -138,11 +141,65 @@ for (const supportedVersion of mc.supportedVersions) { ? message.with[1].extra[0].text : message.with[1].extra[0]) : message.with[1].text, 'hello') + } + resolve() + }) + + // 1.19+ named 'player_chat' + + client.on('player_chat', function (packet) { + chatCount += 1 + + const sender = JSON.parse(packet.senderName) + const chatContent = JSON.parse(packet.signedChatContent) + + switch (chatCount) { + case 1: + assert.deepStrictEqual(sender, { + insertion: 'Player', + clickEvent: { + action: 'suggest_command', + value: '/tell Player ' + }, + hoverEvent: { + action: 'show_entity', + contents: { + type: 'minecraft:player', + id: 'a01e3843-e521-3998-958a-f459800e4d11', + name: { + text: 'Player' + } + } + }, + text: 'Player' + }) + assert.deepStrictEqual(chatContent, { + text: 'hello everyone; I have logged in.' + }) + assert.strictEqual(packet.type, 0) + break + + case 2: + assert.deepStrictEqual(sender, { + text: 'Server' + }) + assert.deepStrictEqual(chatContent, { + text: 'hello' + }) + assert.strictEqual(packet.type, 3) + } + + resolve() + }) + + function resolve () { + assert.ok(chatCount <= 2) + if (chatCount === 2) { wrap.removeListener('line', lineListener) client.end() done() } - }) + } }) it('does not crash for ' + SURVIVE_TIME + 'ms', function (done) { @@ -153,9 +210,7 @@ for (const supportedVersion of mc.supportedVersions) { }) client.on('error', err => done(err)) client.on('login', function () { - client.write('chat', { - message: 'hello everyone; I have logged in.' - }) + chat(client, 'hello everyone; I have logged in.') setTimeout(function () { client.end() done() @@ -231,9 +286,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.' - }) + chat(client, 'hello everyone; I have logged in.') }) let chatCount = 0 client.on('chat', function (packet) { diff --git a/test/common/util.js b/test/common/util.js index 9289993..b67c4f0 100644 --- a/test/common/util.js +++ b/test/common/util.js @@ -9,4 +9,96 @@ const getPort = () => new Promise(resolve => { }) }) -module.exports = { getPort } +function serverchat (client, message) { + const [event, data] = makeBroadcast(client.version, message) + client.write(event, data) +} + +function makeBroadcast (version, message) { + const mcData = require('minecraft-data')(version) + if (mcData.supportFeature('signedChat')) { + return ['player_chat', { + signedChatContent: JSON.stringify({ text: message }), + unsignedChatContent: undefined, + type: 0, + senderUuid: '0', + senderName: 'Server', + timestamp: Date.now() * 1000, + salt: 0, + signature: [] + }] + } else { + return ['chat', { + message: JSON.stringify({ text: message }), + position: 0, + sender: '0' + }] + } +} + +function chat (client, message) { + const mcData = require('minecraft-data')(client.version) + if (mcData.supportFeature('signedChat')) { + client.write('chat_message', { + message, + timestamp: Date.now() * 1000, + salt: 0, + signature: [], + signedPreview: true + }) + } else { + client.write('chat', { + message + }) + } +} + +function clientXChat (x, client, fn) { + const mcData = require('minecraft-data')(client.version) + if (mcData.supportFeature('signedChat')) { + x.bind(client)('player_chat', (packet) => { + const message = packet.unsignedChatContent || packet.signedChatContent + fn(message) + }) + } else { + x.bind(client)('chat', (packet) => { + const message = packet.message + fn(message) + }) + } +} + +function serverXChat (x, server, fn) { + const mcData = require('minecraft-data')(server.version) + if (mcData.supportFeature('signedChat')) { + x.bind(server)('chat_message', (packet) => { + const message = packet.message + fn(message) + }) + } else { + x.bind(server)('chat', (packet) => { + const message = packet.message + fn(message) + }) + } +} + +function OnceChat (client, fn) { + clientXChat(client.once, client, fn) +} +function OnChat (client, fn) { + clientXChat(client.on, client, fn) +} +async function OnceChatPromise (client) { + const mcData = require('minecraft-data')(client.version) + const { once } = require('events') + if (mcData.supportFeature('signedChat')) { + const [packet] = await once(client, 'player_chat') + return packet.unsignedChatContent || packet.signedChatContent + } else { + const [packet] = await once(client, 'chat') + return packet.message + } +} + +module.exports = { getPort, serverchat, makeBroadcast, chat, OnceChat, OnceChatPromise, OnChat, clientXChat, serverXChat } diff --git a/test/packetTest.js b/test/packetTest.js index e70004f..0568252 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -40,6 +40,7 @@ const values = { i16: -123, u16: 123, varint: 1, + varlong: -20, 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) { diff --git a/test/serverTest.js b/test/serverTest.js index e66a776..8f2443a 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -4,7 +4,7 @@ const mc = require('../') const assert = require('power-assert') const { once } = require('events') -const { getPort } = require('./common/util') +const { getPort, serverchat, makeBroadcast, chat, OnceChat, OnceChatPromise, serverXChat } = require('./common/util') const w = { piglin_safe: { @@ -66,6 +66,49 @@ for (const supportedVersion of mc.supportedVersions) { const mcData = require('minecraft-data')(supportedVersion) const version = mcData.version + 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 + } + } + describe('mc-server ' + version.minecraftVersion, function () { this.beforeAll(async function() { @@ -191,7 +234,7 @@ for (const supportedVersion of mc.supportedVersions) { online: 0, sample: [] }, - description: { + description: { extra: [ { color: 'red', text: 'Red text' } ], bold: true, text: 'Example chat mesasge' @@ -279,32 +322,9 @@ 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 + client.write('login', loginPacket(client, server)) + serverXChat(client.on, client, function (message) { + message = '<' + client.username + '>' + ' ' + message broadcast(message) }) }) @@ -318,27 +338,31 @@ for (const supportedVersion of mc.supportedVersions) { }) player1.on('login', 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) + + OnceChat(player1, (message) => { + assert.strictEqual(message, '{"text":"player2 joined the game."}') + + OnceChat(player1, (message => { + assert.strictEqual(message, '{"text":" hi"}') + + function fn(message) { + if (//.test(message)) { + OnceChat(player2, fn) return } - assert.strictEqual(packet.message, '{"text":" hello"}') - player1.once('chat', function (packet) { - assert.strictEqual(packet.message, '{"text":"player2 left the game."}') + assert.strictEqual(message, '{"text":" hello"}') + OnceChat(player1, (message) => { + assert.strictEqual(message, '{"text":"player2 left the game."}') player1.end() }) player2.end() } - player1.write('chat', { message: 'hello' }) - }) - player2.write('chat', { message: 'hi' }) + OnceChat(player2, fn) + + chat(player1, 'hello') + })) + chat(player2, 'hi') }) const player2 = mc.createClient({ username: 'player2', @@ -349,13 +373,13 @@ for (const supportedVersion of mc.supportedVersions) { }) }) - function broadcast (message, exclude) { + function broadcast(message, exclude) { let client for (const clientId in server.clients) { if (server.clients[clientId] === undefined) continue client = server.clients[clientId] - if (client !== exclude) client.write('chat', { message: JSON.stringify({ text: message }), position: 0, sender: '0' }) + if (client !== exclude) serverchat(client, message) } } }) @@ -404,30 +428,7 @@ 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) + client.write('login', loginPacket(client, server)) }) server.on('close', function () { resolve() @@ -455,30 +456,7 @@ 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 () { @@ -494,11 +472,17 @@ for (const supportedVersion of mc.supportedVersions) { 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' }) + await Promise.all([once(player1, 'login'), once(player2, 'login')]) - 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."}')) + const [event, data] = makeBroadcast(server.version, 'A message from the server.') + + server.writeToClients(Object.values(server.clients), event, data) + + let results = await Promise.all([OnceChatPromise(player1), OnceChatPromise(player2)]) + + for (const msg of results) { + assert.strictEqual(msg, '{"text":"A message from the server."}') + } player1.end() player2.end()