mirror of
https://github.com/PrismarineJS/node-minecraft-protocol.git
synced 2024-11-14 19:04:59 -05:00
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 <romain.rom1@gmail.com> * 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 <romain.rom1@gmail.com> Co-authored-by: Rob9315 <dev.robk@gmail.com>
This commit is contained in:
parent
60379eb7d2
commit
d7c5053a13
18 changed files with 494 additions and 314 deletions
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
14
docs/API.md
14
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
|
||||
|
||||
|
|
|
@ -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 <hostname> <port> <user> [<password>]')
|
||||
}
|
||||
|
||||
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 <host> <port> <username>')
|
||||
console.error('Usage (offline mode): node client_chat.js <host> <port> 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())
|
||||
})
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
10
src/index.d.ts
vendored
10
src/index.d.ts
vendored
|
@ -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> | 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 {
|
||||
|
|
3
src/server/constants.js
Normal file
3
src/server/constants.js
Normal file
|
@ -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-----'
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
24
src/transforms/binaryStream.js
Normal file
24
src/transforms/binaryStream.js
Normal file
|
@ -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) => <Buffer 00 00 00 16 00 00 00 00 00 00 00 02>
|
||||
module.exports = { concat }
|
|
@ -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']
|
||||
}
|
||||
|
|
|
@ -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
|
||||
]
|
||||
|
|
|
@ -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) {
|
||||
|
|
39
test/common/clientHelpers.js
Normal file
39
test/common/clientHelpers.js
Normal file
|
@ -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
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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' }],
|
||||
|
@ -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":"<player2> hi"}')
|
||||
player2.once('chat', fn)
|
||||
function fn (packet) {
|
||||
if (/<player2>/.test(packet.message)) {
|
||||
player2.once('chat', fn)
|
||||
return
|
||||
}
|
||||
assert.strictEqual(packet.message, '{"text":"<player1> 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
|
||||
})
|
||||
}))
|
||||
|
||||
const p1Join = await player1.nextMessage('player2')
|
||||
assert.strictEqual(p1Join, '{"text":"player2 joined the game."}')
|
||||
|
||||
player2.chat('hi')
|
||||
const p2hi = await player1.nextMessage('player2')
|
||||
assert.strictEqual(p2hi, '{"text":"<player2> hi"}')
|
||||
|
||||
player1.chat('hello')
|
||||
const p1hello = await player2.nextMessage('player1')
|
||||
assert.strictEqual(p1hello, '{"text":"<player1> hello"}')
|
||||
|
||||
player2.end()
|
||||
const p2leaving = await player1.nextMessage('player2')
|
||||
assert.strictEqual(p2leaving, '{"text":"player2 left the game."}')
|
||||
player1.end()
|
||||
})
|
||||
})
|
||||
|
||||
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' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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,50 +415,30 @@ 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."}'))
|
||||
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()
|
||||
|
|
Loading…
Reference in a new issue