Send skin data, protocol updates (#88)

* Add skin data

* Serialization updates
* Dynamic shield item id
* NBT reading/writing on void type uses 0 length, fix some third party servers

* Fix proxy empty chunk issue

* Fix scoreboards
compiler needs ../

* fix indentation

* Fix set_score packet

* Fix readme title auth doc

* Implement new compiler vars
This commit is contained in:
extremeheat 2021-05-24 10:17:09 -04:00 committed by GitHub
commit f0fbf4f859
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 5557 additions and 5164 deletions

View file

@ -2,7 +2,6 @@ const { ClientStatus, Connection } = require('./connection')
const { createDeserializer, createSerializer } = require('./transforms/serializer')
const { RakClient } = require('./rak')
const { serialize } = require('./datatypes/util')
const fs = require('fs')
const debug = require('debug')('minecraft-protocol')
const Options = require('./options')
const auth = require('./client/auth')
@ -152,33 +151,16 @@ class Client extends Connection {
this.status = ClientStatus.Disconnected
}
tryRencode (name, params, actual) {
const packet = this.serializer.createPacketBuffer({ name, params })
console.assert(packet.equals(actual))
if (!packet.equals(actual)) {
const ours = packet.toString('hex').match(/.{1,16}/g).join('\n')
const theirs = actual.toString('hex').match(/.{1,16}/g).join('\n')
fs.writeFileSync('ours.txt', ours)
fs.writeFileSync('theirs.txt', theirs)
fs.writeFileSync('ours.json', serialize(params))
fs.writeFileSync('theirs.json', serialize(this.deserializer.parsePacketBuffer(packet).data.params))
throw new Error(name + ' Packet comparison failed!')
}
}
readPacket (packet) {
const des = this.deserializer.parsePacketBuffer(packet)
const pakData = { name: des.data.name, params: des.data.params }
this.inLog('-> C', pakData.name/*, serialize(pakData.params).slice(0, 100) */)
this.inLog('-> C', pakData.name, this.options.loggging ? serialize(pakData.params) : '')
this.emit('packet', des)
if (debugging) {
// Packet verifying (decode + re-encode + match test)
if (pakData.name) {
this.tryRencode(pakData.name, pakData.params, packet)
this.deserializer.verify(packet, this.serializer)
}
}
@ -193,6 +175,12 @@ class Client extends Connection {
break
case 'start_game':
this.startGameData = pakData.params
this.startGameData.itemstates.forEach(state => {
if (state.name === 'minecraft:shield') {
this.serializer.proto.setVariable('ShieldItemID', state.runtime_id)
this.deserializer.proto.setVariable('ShieldItemID', state.runtime_id)
}
})
break
case 'play_status':
if (this.status === ClientStatus.Authenticating) {

View file

@ -40,8 +40,25 @@ class Connection extends EventEmitter {
this.encrypt = cipher.createEncryptor(this, iv)
}
updateItemPalette (palette) {
// In the future, we can send down the whole item palette if we need
// but since it's only one item, we can just make a single variable.
let shieldItemID
for (const state of palette) {
if (state.name === 'minecraft:shield') {
shieldItemID = state.runtime_id
break
}
}
if (shieldItemID) {
this.serializer.proto.setVariable('ShieldItemID', shieldItemID)
this.deserializer.proto.setVariable('ShieldItemID', shieldItemID)
}
}
write (name, params) {
this.outLog('sending', name, params)
if (name === 'start_game') this.updateItemPalette(params.itemstates)
const batch = new Framer()
const packet = this.serializer.createPacketBuffer({ name, params })
batch.addEncodedPacket(packet)
@ -55,6 +72,7 @@ class Connection extends EventEmitter {
queue (name, params) {
this.outLog('Q <- ', name, params)
if (name === 'start_game') this.updateItemPalette(params.itemstates)
const packet = this.serializer.createPacketBuffer({ name, params })
if (name === 'level_chunk') {
// Skip queue, send ASAP
@ -113,7 +131,11 @@ class Connection extends EventEmitter {
sendMCPE (buffer, immediate) {
if (this.connection.connected === false || this.status === ClientStatus.Disconnected) return
this.connection.sendReliable(buffer, immediate)
try {
this.connection.sendReliable(buffer, immediate)
} catch (e) {
debug('while sending to', this.connection, e)
}
}
// These are callbacks called from encryption.js

View file

@ -37,10 +37,10 @@ function connect (client) {
response_status: 'completed',
resourcepackids: []
})
client.queue('request_chunk_radius', { chunk_radius: client.renderDistance || 10 })
})
client.queue('client_cache_status', { enabled: false })
client.queue('request_chunk_radius', { chunk_radius: client.renderDistance || 1 })
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n })
})

View file

@ -38,14 +38,18 @@ function sizeOfNbt (value) {
// Little Endian
function readNbtLE (buffer, offset) {
return protoLE.read(buffer, offset, 'nbt')
const r = protoLE.read(buffer, offset, 'nbt')
if (r.value.type === 'end') return { value: r.value, size: 0 }
return r
}
function writeNbtLE (value, buffer, offset) {
if (value.type === 'end') return offset
return protoLE.write(value, buffer, offset, 'nbt')
}
function sizeOfNbtLE (value) {
if (value.type === 'end') return 0
return protoLE.sizeOf(value, 'nbt')
}

View file

@ -6,7 +6,10 @@ const { PUBLIC_KEY } = require('./constants')
const algorithm = 'ES384'
module.exports = (client, server, options) => {
const skinGeom = fs.readFileSync(DataProvider(options.protocolVersion).getPath('skin_geom.txt'), 'utf-8')
const dp = DataProvider(options.protocolVersion)
const skinTex = fs.readFileSync(dp.getPath('steveSkin.bin')).toString('base64')
const skinGeom = fs.readFileSync(dp.getPath('steveGeometry.json')).toString('base64')
const skinData = JSON.parse(fs.readFileSync(dp.getPath('steve.json'), 'utf-8'))
client.createClientChain = (mojangKey, offline) => {
const privateKey = client.ecdhKeyPair.privateKey
@ -22,12 +25,12 @@ module.exports = (client, server, options) => {
certificateAuthority: true,
identityPublicKey: client.clientX509
}
token = JWT.sign(payload, privateKey, { algorithm, notBefore: 0, issuer: 'self', expiresIn: 60 * 60, header: { x5u: client.clientX509 } })
token = JWT.sign(payload, privateKey, { algorithm, notBefore: 0, issuer: 'self', expiresIn: 60 * 60, header: { x5u: client.clientX509, typ: undefined } })
} else {
token = JWT.sign({
identityPublicKey: mojangKey || PUBLIC_KEY,
certificateAuthority: true
}, privateKey, { algorithm, header: { x5u: client.clientX509 } })
}, privateKey, { algorithm, header: { x5u: client.clientX509, typ: undefined } })
}
client.clientIdentityChain = token
@ -36,49 +39,38 @@ module.exports = (client, server, options) => {
client.createClientUserChain = (privateKey) => {
let payload = {
AnimatedImageData: [],
ArmSize: 'wide',
CapeData: '',
CapeId: '',
CapeImageHeight: 0,
CapeImageWidth: 0,
CapeOnClassicSkin: false,
...skinData,
ClientRandomId: Date.now(),
CurrentInputMode: 1,
DefaultInputMode: 1,
DeviceId: nextUUID(),
DeviceModel: '',
DeviceModel: 'PrismarineJS',
DeviceOS: client.session?.deviceOS || 7,
GameVersion: options.version || '1.16.201',
GuiScale: -1,
LanguageCode: 'en_GB', // TODO locale
PersonaPieces: [],
PersonaSkin: true,
PieceTintColors: [],
PlatformOfflineId: '',
PlatformOnlineId: '', // chat
// PlayFabID is the PlayFab ID produced for the skin. PlayFab is the company that hosts the Marketplace,
// skins and other related features from the game. This ID is the ID of the skin used to store the skin
// inside of PlayFab.
PlayFabId: '5eb65f73-af11-448e-82aa-1b7b165316ad.persona-e199672a8c1a87e0-0', // 1.16.210
PremiumSkin: false,
PlayFabId: nextUUID().replace(/-/g, '').slice(0, 16), // 1.16.210
SelfSignedId: nextUUID(),
ServerAddress: `${options.host}:${options.port}`,
SkinAnimationData: '',
SkinColor: '#ffffcd96',
SkinData: 'AAAAAA==',
SkinData: skinTex,
SkinGeometryData: skinGeom,
SkinId: '5eb65f73-af11-448e-82aa-1b7b165316ad.persona-e199672a8c1a87e0-0',
SkinImageHeight: 1,
SkinImageWidth: 1,
SkinResourcePatch: '',
ThirdPartyName: client.profile.name,
ThirdPartyNameOnly: false,
UIProfile: 0
}
const customPayload = options.skinData || {}
payload = { ...payload, ...customPayload }
payload.ServerAddress = `${options.host}:${options.port}`
client.clientUserChain = JWT.sign(payload, privateKey, { algorithm, header: { x5u: client.clientX509 } })
client.clientUserChain = JWT.sign(payload, privateKey, { algorithm, header: { x5u: client.clientX509, typ: undefined }, noTimestamp: true /* pocketmine.. */ })
}
}

View file

@ -30,6 +30,7 @@ class RelayPlayer extends Player {
this.outLog = this.downOutLog
this.inLog = this.downInLog
this.chunkSendCache = []
}
// Called when we get a packet from backend server (Backend -> PROXY -> Client)
@ -45,16 +46,23 @@ class RelayPlayer extends Player {
if (name === 'play_status' && params.status === 'login_success') return // We already sent this, this needs to be sent ASAP or client will disconnect
if (debugging) { // some packet encode/decode testing stuff
const rpacket = this.server.serializer.createPacketBuffer({ name, params })
if (!rpacket.equals(packet)) {
console.warn('New', rpacket.toString('hex'))
console.warn('Old', packet.toString('hex'))
console.log('Failed to re-encode', name, params)
process.exit(1)
}
this.server.deserializer.verify(des, this.server.serializer)
}
this.emit('clientbound', des.data)
// If we're sending a chunk, but player isn't yet initialized, wait until it is.
// This is wrong and should not be an issue to send chunks before the client
// is in the world; need to investigate further, but for now it's fine.
if (name === 'level_chunk' && this.status !== 3) {
this.chunkSendCache.push([name, params])
return
} else if (this.status === 3 && this.chunkSendCache.length) {
for (const chunk of this.chunkSendCache) {
this.queue(...chunk)
}
this.chunkSendCache = []
}
this.queue(name, params)
}
@ -82,33 +90,36 @@ class RelayPlayer extends Player {
// Called when the server gets a packet from the downstream player (Client -> PROXY -> Backend)
readPacket (packet) {
if (this.startRelaying) { // The downstream client conn is established & we got a packet to send to upstream server
if (!this.upstream) { // Upstream is still connecting/handshaking
// The downstream client conn is established & we got a packet to send to upstream server
if (this.startRelaying) {
// Upstream is still connecting/handshaking
if (!this.upstream) {
this.downInLog('Got downstream connected packet but upstream is not connected yet, added to q', this.upQ.length)
this.upQ.push(packet) // Put into a queue
return
}
this.flushUpQueue() // Send queued packets
// Send queued packets
this.flushUpQueue()
this.downInLog('recv', packet)
// TODO: If we fail to parse a packet, proxy it raw and log an error
const des = this.server.deserializer.parsePacketBuffer(packet)
if (debugging) { // some packet encode/decode testing stuff
const rpacket = this.server.serializer.createPacketBuffer(des.data)
if (!rpacket.equals(packet)) {
console.warn('New', rpacket.toString('hex'))
console.warn('Old', packet.toString('hex'))
console.log('Failed to re-encode', des.data)
process.exit(1)
}
this.server.deserializer.verify(des, this.server.serializer)
}
this.emit('serverbound', des.data)
switch (des.data.name) {
case 'client_cache_status':
// Force the chunk cache off.
this.upstream.queue('client_cache_status', { enabled: false })
break
case 'set_local_player_as_initialized':
this.status = 3
break
default:
// Emit the packet as-is back to the upstream server
this.downInLog('Relaying', des.data)
@ -149,6 +160,10 @@ class Relay extends Server {
client.outLog = ds.upOutLog
client.inLog = ds.upInLog
client.once('join', () => { // Intercept once handshaking done
// Tell the server to disable chunk cache for this connection as a client.
// Wait a bit for the server to ack and process, the continue with proxying
// otherwise the player can get stuck in an empty world.
client.write('client_cache_status', { enabled: false })
ds.upstream = client
ds.flushUpQueue()
this.conLog('Connected to upstream server')

View file

@ -11,6 +11,18 @@ class Parser extends FullPacketParser {
throw e
}
}
verify (deserialized, serializer) {
const { name, params } = deserialized.data
const oldBuffer = deserialized.fullBuffer
const newBuffer = serializer.createPacketBuffer({ name, params })
if (!newBuffer.equals(oldBuffer)) {
console.warn('New', newBuffer.toString('hex'))
console.warn('Old', oldBuffer.toString('hex'))
console.log('Failed to re-encode', name, params)
process.exit(1)
}
}
}
// Compiles the ProtoDef schema at runtime
@ -31,11 +43,8 @@ function getProtocol (version) {
compiler.addTypes(require(join(__dirname, '../datatypes/compiler-minecraft')))
compiler.addTypes(require('prismarine-nbt/compiler-zigzag'))
const compile = (compiler, file) => {
global.native = compiler.native // eslint-disable-line
const { PartialReadError } = require('protodef/src/utils') // eslint-disable-line
return require(file)() // eslint-disable-line
}
global.PartialReadError = require('protodef/src/utils').PartialReadError
const compile = (compiler, file) => require(file)(compiler.native)
return new CompiledProtodef(
compile(compiler.sizeOfCompiler, join(__dirname, `../../data/${version}/size.js`)),