[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 <romain.rom1@gmail.com>
This commit is contained in:
Rob9315 2022-07-25 22:14:26 +02:00 committed by GitHub
parent dda12949ef
commit 0ecba87dfe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 330 additions and 123 deletions

View file

@ -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",

View file

@ -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)
}
}

View file

@ -27,6 +27,7 @@ module.exports = function (client, options) {
client.write('login_start', {
username: client.username
})
// TODO: add signature option
}
}
}

View file

@ -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)

View file

@ -7,7 +7,8 @@ const states = mc.states
const testDataWrite = [
{ name: 'keep_alive', params: { keepAliveId: 957759560 } },
{ name: 'chat', params: { message: '<Bob> Hello World!' } },
// TODO: 1.19+ `chat` -> `player_chat` feature toggle
// { name: 'chat', params: { message: '<Bob> 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
]

View file

@ -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\]: <Server> .*/)) 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) {

View file

@ -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 }

View file

@ -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) {

View file

@ -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":"<player2> hi"}')
player2.once('chat', fn)
function fn (packet) {
if (/<player2>/.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":"<player2> hi"}')
function fn(message) {
if (/<player2>/.test(message)) {
OnceChat(player2, fn)
return
}
assert.strictEqual(packet.message, '{"text":"<player1> hello"}')
player1.once('chat', function (packet) {
assert.strictEqual(packet.message, '{"text":"player2 left the game."}')
assert.strictEqual(message, '{"text":"<player1> 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()