Start work on multi-version support, test cleanup (#43)

* some cleanup

* start work on multi-version support

* undelete some old examples, can update them later

* move old examples
This commit is contained in:
extremeheat 2021-03-12 14:20:25 -05:00 committed by GitHub
commit df8612e355
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 368 additions and 3517 deletions

10
.gitignore vendored
View file

@ -2,7 +2,11 @@ node_modules/
npm-debug.log
package-lock.json
__*
data/*.js
src/**/*.json
src/**/*.txt
dist/
# Runtime generated data
data/*/sample
data/**/read.js
data/**/write.js
data/**/size.js
samples/*.txt
samples/*.json

File diff suppressed because one or more lines are too long

View file

@ -8,11 +8,14 @@
const fs = require('fs')
const { ProtoDefCompiler } = require('protodef').Compiler
// Parse the YML files and turn to JSON
function genProtoSchema() {
const { parse, compile } = require('protodef-yaml/compiler')
let version
// Create the packet_map.yml from proto.yml
const parsed = parse('./proto.yml')
version = parsed['!version']
const packets = []
for (const key in parsed) {
if (key.startsWith('%container')) {
@ -30,31 +33,39 @@ function genProtoSchema() {
l1 += ` 0x${id.toString(16).padStart(2, '0')}: ${name}\n`
l2 += ` if ${name}: ${fname}\n`
}
const t = `!import: types.yaml\nmcpe_packet:\n name: varint =>\n${l1}\n params: name ?\n${l2}`
// TODO: skip creating packet_map.yml and just generate the ProtoDef map JSON directly
const t = `#Auto-generated from proto.yml, do not modify\n!import: types.yaml\nmcpe_packet:\n name: varint =>\n${l1}\n params: name ?\n${l2}`
fs.writeFileSync('./packet_map.yml', t)
compile('./proto.yml', 'protocol.json')
return version
}
genProtoSchema()
fs.writeFileSync('../newproto.json', JSON.stringify({ types: require('./protocol.json') }, null, 2))
fs.unlinkSync('./protocol.json') //remove temp file
function createProtocol() {
// Compile the ProtoDef JSON into JS
function createProtocol(version) {
const compiler = new ProtoDefCompiler()
const protocol = require('../newproto.json').types
const protocol = require(`../${version}/protocol.json`).types
compiler.addTypes(require('../../src/datatypes/compiler-minecraft'))
compiler.addTypes(require('prismarine-nbt/compiler-zigzag'))
compiler.addTypesToCompile(protocol)
fs.writeFileSync('../read.js', 'module.exports = ' + compiler.readCompiler.generate())
fs.writeFileSync('../write.js', 'module.exports = ' + compiler.writeCompiler.generate())
fs.writeFileSync('../size.js', 'module.exports = ' + compiler.sizeOfCompiler.generate())
fs.writeFileSync(`../${version}/read.js`, 'module.exports = ' + compiler.readCompiler.generate())
fs.writeFileSync(`../${version}/write.js`, 'module.exports = ' + compiler.writeCompiler.generate())
fs.writeFileSync(`../${version}/size.js`, 'module.exports = ' + compiler.sizeOfCompiler.generate())
const compiledProto = compiler.compileProtoDefSync()
return compiledProto
}
console.log('Generating JS...')
createProtocol()
function main() {
const version = genProtoSchema()
fs.writeFileSync(`../${version}/protocol.json`, JSON.stringify({ types: require('./protocol.json') }, null, 2))
fs.unlinkSync('./protocol.json') //remove temp file
fs.unlinkSync('./packet_map.yml') //remove temp file
console.log('Generating JS...')
createProtocol(version)
}
main()

View file

@ -1,317 +0,0 @@
!import: types.yaml
mcpe_packet:
name: varint =>
0x01: login
0x02: play_status
0x03: server_to_client_handshake
0x04: client_to_server_handshake
0x05: disconnect
0x06: resource_packs_info
0x07: resource_pack_stack
0x08: resource_pack_client_response
0x09: text
0x0a: set_time
0x0b: start_game
0x0c: add_player
0x0d: add_entity
0x0e: remove_entity
0x0f: add_item_entity
0x11: take_item_entity
0x12: move_entity
0x13: move_player
0x14: rider_jump
0x15: update_block
0x16: add_painting
0x17: tick_sync
0x18: level_sound_event_old
0x19: level_event
0x1a: block_event
0x1b: entity_event
0x1c: mob_effect
0x1d: update_attributes
0x1e: inventory_transaction
0x1f: mob_equipment
0x20: mob_armor_equipment
0x21: interact
0x22: block_pick_request
0x23: entity_pick_request
0x24: player_action
0x26: hurt_armor
0x27: set_entity_data
0x28: set_entity_motion
0x29: set_entity_link
0x2a: set_health
0x2b: set_spawn_position
0x2c: animate
0x2d: respawn
0x2e: container_open
0x2f: container_close
0x30: player_hotbar
0x31: inventory_content
0x32: inventory_slot
0x33: container_set_data
0x34: crafting_data
0x35: crafting_event
0x36: gui_data_pick_item
0x37: adventure_settings
0x38: block_entity_data
0x39: player_input
0x3a: level_chunk
0x3b: set_commands_enabled
0x3c: set_difficulty
0x3d: change_dimension
0x3e: set_player_game_type
0x3f: player_list
0x40: simple_event
0x41: event
0x42: spawn_experience_orb
0x43: clientbound_map_item_data
0x44: map_info_request
0x45: request_chunk_radius
0x46: chunk_radius_update
0x47: item_frame_drop_item
0x48: game_rules_changed
0x49: camera
0x4a: boss_event
0x4b: show_credits
0x4c: available_commands
0x4d: command_request
0x4e: command_block_update
0x4f: command_output
0x50: update_trade
0x51: update_equipment
0x52: resource_pack_data_info
0x53: resource_pack_chunk_data
0x54: resource_pack_chunk_request
0x55: transfer
0x56: play_sound
0x57: stop_sound
0x58: set_title
0x59: add_behavior_tree
0x5a: structure_block_update
0x5b: show_store_offer
0x5c: purchase_receipt
0x5d: player_skin
0x5e: sub_client_login
0x5f: initiate_web_socket_connection
0x60: set_last_hurt_by
0x61: book_edit
0x62: npc_request
0x63: photo_transfer
0x64: modal_form_request
0x65: modal_form_response
0x66: server_settings_request
0x67: server_settings_response
0x68: show_profile
0x69: set_default_game_type
0x6a: remove_objective
0x6b: set_display_objective
0x6c: set_score
0x6d: lab_table
0x6e: update_block_synced
0x6f: move_entity_delta
0x70: set_scoreboard_identity
0x71: set_local_player_as_initialized
0x72: update_soft_enum
0x73: network_stack_latency
0x75: script_custom_event
0x76: spawn_particle_effect
0x77: available_entity_identifiers
0x78: level_sound_event_v2
0x79: network_chunk_publisher_update
0x7a: biome_definition_list
0x7b: level_sound_event
0x7c: level_event_generic
0x7d: lectern_update
0x7e: video_stream_connect
0x7f: add_ecs_entity
0x80: remove_ecs_entity
0x81: client_cache_status
0x82: on_screen_texture_animation
0x83: map_create_locked_copy
0x84: structure_template_data_export_request
0x85: structure_template_data_export_response
0x86: update_block_properties
0x87: client_cache_blob_status
0x88: client_cache_miss_response
0x89: education_settings
0x8b: multiplayer_settings
0x8c: settings_command
0x8d: anvil_damage
0x8e: completed_using_item
0x8f: network_settings
0x90: player_auth_input
0x91: creative_content
0x92: player_enchant_options
0x93: item_stack_request
0x94: item_stack_response
0x95: player_armor_damage
0x97: update_player_game_type
0x9a: position_tracking_db_request
0x99: position_tracking_db_broadcast
0x9c: packet_violation_warning
0x9d: motion_prediction_hints
0x9e: animate_entity
0x9f: camera_shake
0xa0: player_fog
0xa1: correct_player_move_prediction
0xa2: item_component
0xa3: filter_text_packet
params: name ?
if login: packet_login
if play_status: packet_play_status
if server_to_client_handshake: packet_server_to_client_handshake
if client_to_server_handshake: packet_client_to_server_handshake
if disconnect: packet_disconnect
if resource_packs_info: packet_resource_packs_info
if resource_pack_stack: packet_resource_pack_stack
if resource_pack_client_response: packet_resource_pack_client_response
if text: packet_text
if set_time: packet_set_time
if start_game: packet_start_game
if add_player: packet_add_player
if add_entity: packet_add_entity
if remove_entity: packet_remove_entity
if add_item_entity: packet_add_item_entity
if take_item_entity: packet_take_item_entity
if move_entity: packet_move_entity
if move_player: packet_move_player
if rider_jump: packet_rider_jump
if update_block: packet_update_block
if add_painting: packet_add_painting
if tick_sync: packet_tick_sync
if level_sound_event_old: packet_level_sound_event_old
if level_event: packet_level_event
if block_event: packet_block_event
if entity_event: packet_entity_event
if mob_effect: packet_mob_effect
if update_attributes: packet_update_attributes
if inventory_transaction: packet_inventory_transaction
if mob_equipment: packet_mob_equipment
if mob_armor_equipment: packet_mob_armor_equipment
if interact: packet_interact
if block_pick_request: packet_block_pick_request
if entity_pick_request: packet_entity_pick_request
if player_action: packet_player_action
if hurt_armor: packet_hurt_armor
if set_entity_data: packet_set_entity_data
if set_entity_motion: packet_set_entity_motion
if set_entity_link: packet_set_entity_link
if set_health: packet_set_health
if set_spawn_position: packet_set_spawn_position
if animate: packet_animate
if respawn: packet_respawn
if container_open: packet_container_open
if container_close: packet_container_close
if player_hotbar: packet_player_hotbar
if inventory_content: packet_inventory_content
if inventory_slot: packet_inventory_slot
if container_set_data: packet_container_set_data
if crafting_data: packet_crafting_data
if crafting_event: packet_crafting_event
if gui_data_pick_item: packet_gui_data_pick_item
if adventure_settings: packet_adventure_settings
if block_entity_data: packet_block_entity_data
if player_input: packet_player_input
if level_chunk: packet_level_chunk
if set_commands_enabled: packet_set_commands_enabled
if set_difficulty: packet_set_difficulty
if change_dimension: packet_change_dimension
if set_player_game_type: packet_set_player_game_type
if player_list: packet_player_list
if simple_event: packet_simple_event
if event: packet_event
if spawn_experience_orb: packet_spawn_experience_orb
if clientbound_map_item_data: packet_clientbound_map_item_data
if map_info_request: packet_map_info_request
if request_chunk_radius: packet_request_chunk_radius
if chunk_radius_update: packet_chunk_radius_update
if item_frame_drop_item: packet_item_frame_drop_item
if game_rules_changed: packet_game_rules_changed
if camera: packet_camera
if boss_event: packet_boss_event
if show_credits: packet_show_credits
if available_commands: packet_available_commands
if command_request: packet_command_request
if command_block_update: packet_command_block_update
if command_output: packet_command_output
if update_trade: packet_update_trade
if update_equipment: packet_update_equipment
if resource_pack_data_info: packet_resource_pack_data_info
if resource_pack_chunk_data: packet_resource_pack_chunk_data
if resource_pack_chunk_request: packet_resource_pack_chunk_request
if transfer: packet_transfer
if play_sound: packet_play_sound
if stop_sound: packet_stop_sound
if set_title: packet_set_title
if add_behavior_tree: packet_add_behavior_tree
if structure_block_update: packet_structure_block_update
if show_store_offer: packet_show_store_offer
if purchase_receipt: packet_purchase_receipt
if player_skin: packet_player_skin
if sub_client_login: packet_sub_client_login
if initiate_web_socket_connection: packet_initiate_web_socket_connection
if set_last_hurt_by: packet_set_last_hurt_by
if book_edit: packet_book_edit
if npc_request: packet_npc_request
if photo_transfer: packet_photo_transfer
if modal_form_request: packet_modal_form_request
if modal_form_response: packet_modal_form_response
if server_settings_request: packet_server_settings_request
if server_settings_response: packet_server_settings_response
if show_profile: packet_show_profile
if set_default_game_type: packet_set_default_game_type
if remove_objective: packet_remove_objective
if set_display_objective: packet_set_display_objective
if set_score: packet_set_score
if lab_table: packet_lab_table
if update_block_synced: packet_update_block_synced
if move_entity_delta: packet_move_entity_delta
if set_scoreboard_identity: packet_set_scoreboard_identity
if set_local_player_as_initialized: packet_set_local_player_as_initialized
if update_soft_enum: packet_update_soft_enum
if network_stack_latency: packet_network_stack_latency
if script_custom_event: packet_script_custom_event
if spawn_particle_effect: packet_spawn_particle_effect
if available_entity_identifiers: packet_available_entity_identifiers
if level_sound_event_v2: packet_level_sound_event_v2
if network_chunk_publisher_update: packet_network_chunk_publisher_update
if biome_definition_list: packet_biome_definition_list
if level_sound_event: packet_level_sound_event
if level_event_generic: packet_level_event_generic
if lectern_update: packet_lectern_update
if video_stream_connect: packet_video_stream_connect
if add_ecs_entity: packet_add_ecs_entity
if remove_ecs_entity: packet_remove_ecs_entity
if client_cache_status: packet_client_cache_status
if on_screen_texture_animation: packet_on_screen_texture_animation
if map_create_locked_copy: packet_map_create_locked_copy
if structure_template_data_export_request: packet_structure_template_data_export_request
if structure_template_data_export_response: packet_structure_template_data_export_response
if update_block_properties: packet_update_block_properties
if client_cache_blob_status: packet_client_cache_blob_status
if client_cache_miss_response: packet_client_cache_miss_response
if education_settings: packet_education_settings
if multiplayer_settings: packet_multiplayer_settings
if settings_command: packet_settings_command
if anvil_damage: packet_anvil_damage
if completed_using_item: packet_completed_using_item
if network_settings: packet_network_settings
if player_auth_input: packet_player_auth_input
if creative_content: packet_creative_content
if player_enchant_options: packet_player_enchant_options
if item_stack_request: packet_item_stack_request
if item_stack_response: packet_item_stack_response
if player_armor_damage: packet_player_armor_damage
if update_player_game_type: packet_update_player_game_type
if position_tracking_db_request: packet_position_tracking_db_request
if position_tracking_db_broadcast: packet_position_tracking_db_broadcast
if packet_violation_warning: packet_packet_violation_warning
if motion_prediction_hints: packet_motion_prediction_hints
if animate_entity: packet_animate_entity
if camera_shake: packet_camera_shake
if player_fog: packet_player_fog
if correct_player_move_prediction: packet_correct_player_move_prediction
if item_component: packet_item_component
if filter_text_packet: packet_filter_text_packet

View file

@ -1,4 +1,9 @@
# Created from MiNET docs
# Created from MiNET and gophertunnel docs
# The version below is the latest version this protocol schema was updated for.
# The output protocol.json will be in the folder for the version
!version: 1.16.201
# Some ProtoDef aliases
string: ["pstring",{"countType":"varint"}]
ByteArray: ["buffer",{"countType":"varint"}]
SignedByteArray: ["buffer",{"countType":"zigzag32"}]
@ -12,8 +17,10 @@ byterot: native
MapInfo: native
nbt: native
# load the packet map file
!import: packet_map.yml
#todo: docs
!StartDocs: Packets
# # Login Sequence

File diff suppressed because it is too large Load diff

45
data/provider.js Normal file
View file

@ -0,0 +1,45 @@
const { Versions } = require('../src/options')
const { getFiles } = require('../src/datatypes/util')
const fileMap = {}
// Walks all the directories for each of the supported versions in options.js
// then builds a file map for each version
// { 'protocol.json': { '1.16.200': '1.16.200/protocol.json', '1.16.210': '1.16.210/...' } }
function loadVersions() {
for (const version in Versions) {
let files = []
try {
files = getFiles(__dirname + '/' + version)
} catch {}
for (let file of files) {
const rfile = file.replace(__dirname + '/' + version + '/', '')
fileMap[rfile] ??= []
fileMap[rfile].push([Versions[version], file])
fileMap[rfile].sort().reverse()
}
}
}
module.exports = (protocolVersion) => {
return {
// Returns the most recent file based on the specified protocolVersion
// e.g. if `version` is 1.16 and a file for 1.16 doesn't exist, load from 1.15 file
getPath(file) {
if (!fileMap[file]) {
throw Error('Unknown file ' + file)
}
for (const [ pver, path ] of fileMap[file]) {
if (pver <= protocolVersion) {
// console.debug('for', file, 'returining', path)
return path
}
}
throw Error('unknown file ' + file)
}
}
}
loadVersions()
// console.log('file map', fileMap)
// module.exports(Versions['1.16.210']).open('creativeitems.json')

38
examples/createRelay.js Normal file
View file

@ -0,0 +1,38 @@
const { Relay } = require('../src/relay')
function createRelay() {
console.log('Creating relay')
/**
* Example to create a non-transparent proxy (or 'Relay') connection to destination server
* In Relay we de-code and re-encode packets
*/
const relay = new Relay({
/* Hostname and port for clients to listen to */
hostname: '0.0.0.0',
port: 19130,
/**
* Who does the authentication
* If set to `client`, all connecting clients will be sent a message with a link to authenticate
* If set to `server`, the server will authenticate and only one client will be able to join
* (Default) If set to `none`, no authentication will be done
*/
auth: 'server',
/**
* Sets if packets will automatically be forwarded. If set to false, you must listen for on('packet')
* events and
*/
auto: true,
/* Where to send upstream packets to */
destination: {
hostname: '127.0.0.1',
port: 19132,
// encryption: true
}
})
relay.create()
}
createRelay()

View file

@ -1,8 +1,9 @@
process.env.DEBUG = 'minecraft-protocol raknet'
const { Server } = require('../src/server')
const CreativeItems = require('../data/creativeitems.json')
// const CreativeItems = require('../data/creativeitems.json')
const NBT = require('prismarine-nbt')
const fs = require('fs')
const DataProvider = require('../data/provider')
let server = new Server({
@ -10,6 +11,14 @@ let server = new Server({
})
server.create('0.0.0.0', 19132)
function getPath(packetPath) {
return DataProvider(server.options.protocolVersion).getPath(packetPath)
}
function get(packetPath) {
return require(getPath('sample/' + packetPath))
}
let ran = false
server.on('connect', ({ client }) => {
@ -50,35 +59,35 @@ server.on('connect', ({ client }) => {
client.queue('inventory_slot', {"inventory_id":120,"slot":i,"uniqueid":0,"item":{"network_id":0}})
}
client.queue('inventory_transaction', require('../src/packets/inventory_transaction.json'))
client.queue('player_list', require('../src/packets/player_list.json'))
client.queue('start_game', require('../src/packets/start_game.json'))
client.queue('inventory_transaction', get('packets/inventory_transaction.json'))
client.queue('player_list', get('packets/player_list.json'))
client.queue('start_game', get('packets/start_game.json'))
client.queue('item_component', {"entries":[]})
client.queue('set_spawn_position', require('../src/packets/set_spawn_position.json'))
client.queue('set_spawn_position', get('packets/set_spawn_position.json'))
client.queue('set_time', { time: 5433771 })
client.queue('set_difficulty', { difficulty: 1 })
client.queue('set_commands_enabled', { enabled: true })
client.queue('adventure_settings', require('../src/packets/adventure_settings.json'))
client.queue('adventure_settings', get('packets/adventure_settings.json'))
client.queue('biome_definition_list', require('../src/packets/biome_definition_list.json'))
client.queue('available_entity_identifiers', require('../src/packets/available_entity_identifiers.json'))
client.queue('biome_definition_list', get('packets/biome_definition_list.json'))
client.queue('available_entity_identifiers', get('packets/available_entity_identifiers.json'))
client.queue('update_attributes', require('../src/packets/update_attributes.json'))
client.queue('creative_content', require('../src/packets/creative_content.json'))
client.queue('inventory_content', require('../src/packets/inventory_content.json'))
client.queue('update_attributes', get('packets/update_attributes.json'))
client.queue('creative_content', get('packets/creative_content.json'))
client.queue('inventory_content', get('packets/inventory_content.json'))
client.queue('player_hotbar', {"selected_slot":3,"window_id":0,"select_slot":true})
client.queue('crafting_data', require('../src/packets/crafting_data.json'))
client.queue('available_commands', require('../src/packets/available_commands.json'))
client.queue('crafting_data', get('packets/crafting_data.json'))
client.queue('available_commands', get('packets/available_commands.json'))
client.queue('chunk_radius_update', {"chunk_radius":5})
client.queue('set_entity_data', require('../src/packets/set_entity_data.json'))
client.queue('set_entity_data', get('packets/set_entity_data.json'))
client.queue('game_rules_changed', require('../src/packets/game_rules_changed.json'))
client.queue('game_rules_changed', get('packets/game_rules_changed.json'))
client.queue('respawn', {"x":646.9405517578125,"y":65.62001037597656,"z":77.86255645751953,"state":0,"runtime_entity_id":0})
for (const file of fs.readdirSync('../src/chunks')) {
const buffer = Buffer.from(fs.readFileSync('../src/chunks/' + file, 'utf8'), 'hex')
for (const file of fs.readdirSync(`../data/${server.options.version}/sample/chunks`)) {
const buffer = Buffer.from(fs.readFileSync(`../data/${server.options.version}/sample/chunks/` + file, 'utf8'), 'hex')
// console.log('Sending chunk', chunk)
client.sendBuffer(buffer)
}

View file

@ -52,6 +52,8 @@ function verifyAuth(chain) {
console.log('verified with mojang key!', x5u)
}
// TODO: Handle `didVerify` = false
pubKey = decoded.identityPublicKey ? mcPubKeyToPem(decoded.identityPublicKey) : x5u
finalKey = decoded.identityPublicKey || finalKey // non pem
data = { ...data, ...decoded }
@ -87,22 +89,22 @@ function encodeLoginJWT(localChain, mojangChain) {
module.exports = { encodeLoginJWT, decodeLoginJWT }
function testServer() {
const loginPacket = require('./login.json')
// function testServer() {
// const loginPacket = require('./login.json')
// console.log(loginPacket)
const authChains = JSON.parse(loginPacket.data.chain)
const skinChain = loginPacket.data.clientData
// // console.log(loginPacket)
// const authChains = JSON.parse(loginPacket.data.chain)
// const skinChain = loginPacket.data.clientData
try {
var { data, chain } = decodeLoginJWT(authChains.chain, skinChain)
} catch (e) {
console.error(e)
throw new Error('Failed to verify user')
}
// try {
// var { data, chain } = decodeLoginJWT(authChains.chain, skinChain)
// } catch (e) {
// console.error(e)
// throw new Error('Failed to verify user')
// }
console.log('Authed')
// console.log(loginPacket)
}
// console.log('Authed')
// // console.log(loginPacket)
// }
// testServer()
// // testServer()

View file

@ -2,11 +2,15 @@ const JWT = require('jsonwebtoken')
const crypto = require('crypto')
const { Ber } = require('asn1')
const ec_pem = require('ec-pem')
const fs = require('fs')
const DataProvider = require('../../data/provider')
const SALT = '🧂'
const curve = 'secp384r1'
function Encrypt(client, server, options) {
const skinGeom = fs.readFileSync(DataProvider(options.protocolVersion).getPath('skin_geom.txt'), 'utf-8')
client.ecdhKeyPair = crypto.createECDH(curve)
client.ecdhKeyPair.generateKeys()
client.clientX509 = writeX509PublicKey(client.ecdhKeyPair.getPublicKey())
@ -84,7 +88,6 @@ function Encrypt(client, server, options) {
}
client.on('server.client_handshake', startClientboundEncryption)
client.on('client.server_handshake', startServerboundEncryption)
client.createClientChain = (mojangKey) => {
@ -118,7 +121,7 @@ function Encrypt(client, server, options) {
SkinId: '5eb65f73-af11-448e-82aa-1b7b165316ad.persona-e199672a8c1a87e0-0',
SkinData: 'AAAAAA==',
SkinResourcePatch: 'ewogICAiZ2VvbWV0cnkiIDogewogICAgICAiYW5pbWF0ZWRfMTI4eDEyOCIgOiAiZ2VvbWV0cnkuYW5pbWF0ZWRfMTI4eDEyOF9wZXJzb25hLWUxOTk2NzJhOGMxYTg3ZTAtMCIsCiAgICAgICJhbmltYXRlZF9mYWNlIiA6ICJnZW9tZXRyeS5hbmltYXRlZF9mYWNlX3BlcnNvbmEtZTE5OTY3MmE4YzFhODdlMC0wIiwKICAgICAgImRlZmF1bHQiIDogImdlb21ldHJ5LnBlcnNvbmFfZTE5OTY3MmE4YzFhODdlMC0wIgogICB9Cn0K',
SkinGeometryData: require('./geom'),
SkinGeometryData: skinGeom,
"SkinImageHeight": 1,
"SkinImageWidth": 1,
"ArmSize": "wide",

View file

@ -1,232 +0,0 @@
const crypto = require('crypto')
const JWT = require('jsonwebtoken')
const constants = require('./constants')
const { Ber } = require('asn1')
const ec_pem = require('ec-pem');
// function Encrypt(client, options) {
// this.startClientboundEncryption = (pubKeyBuf) => {
// }
// client.on('start_encrypt', this.startClientboundEncryption)
// }
// module.exports = Encrypt
// Server -> Client : sent right after the client sends us a LOGIN_PACKET so
// we can start the encryption process
// @param {key} - The key from the client Login Packet final JWT chain
function startClientboundEncryption(pubKeyBuf) {
// create our ecdh keypair
const type = 'secp256k1'
const alice = crypto.createECDH(type)
const aliceKey = alice.generateKeys()
const alicePublicKey = aliceKey.toString('base64')
const alicePrivateKey = mcPubKeyToPem(alice.getPrivateKey('base64'))
// get our secret key hex encoded
// const aliceSecret = alice.computeSecret(pubKeyBuf, null, 'hex')
// (yawkat:)
// From the public key of the remote and the private key of the local,
// a shared secret is generated using ECDH. The secret key bytes are
// then computed as sha256(server_token + shared_secret). These secret
// key bytes are 32 bytes long.
const salt = Buffer.from('', 'utf-8')
let secret = crypto.createHash('sha256').update(Buffer.concat([salt, pubKeyBuf])).digest()
console.log('alice', alicePrivateKey)
const pem = mcPubKeyToPem(alice.getPrivateKey().toString('base64'))
console.log('pem', pem)
const token = JWT.sign({
salt,
signedToken: alicePublicKey
}, pem, { algorithm: 'ES384' })
console.log('Token', token)
// get our Secret Bytes from the secret key
// alice.setPrivateKey(
// crypto.createHash('sha256').update('alice', 'utf8').digest()
// )
// using (var sha = SHA256.Create())
// {
// secret = sha.ComputeHash(secretPrepend.Concat(agreement.CalculateAgreement(remotePublicKey).ToByteArrayUnsigned()).ToArray());
// }
const bob = crypto.createECDH('secp256k1');
// URI x5u = URI.create(Base64.getEncoder().encodeToString(serverKeyPair.getPublic().getEncoded()));
// JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().claim("salt", Base64.getEncoder().encodeToString(token)).build();
// SignedJWT jwt = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.ES384).x509CertURL(x5u).build(), claimsSet);
// signJwt(jwt, (ECPrivateKey) serverKeyPair.getPrivate());
// return jwt;
}
function testECDH() {
const crypto = require('crypto')
const alice = crypto.createECDH('secp256k1')
const bob = crypto.createECDH('secp256k1')
// Note: This is a shortcut way to specify one of Alice's previous private
// keys. It would be unwise to use such a predictable private key in a real
// application.
alice.setPrivateKey(
crypto.createHash('sha256').update('alice', 'utf8').digest()
);
// Bob uses a newly generated cryptographically strong
// pseudorandom key pair bob.generateKeys();
const alice_secret = alice.computeSecret(bob.getPublicKey(), null, 'hex')
const bob_secret = bob.computeSecret(alice.getPublicKey(), null, 'hex')
// alice_secret and bob_secret should be the same shared secret value
console.log(alice_secret === bob_secret)
}
function testECDH2() {
const type = 'secp256k1'
const alice = crypto.createECDH(type);
const aliceKey = alice.generateKeys();
// Generate Bob's keys...
const bob = crypto.createECDH(type);
const bobKey = bob.generateKeys();
console.log("\nAlice private key:\t", alice.getPrivateKey().toString('hex'));
console.log("Alice public key:\t", aliceKey.toString('hex'))
console.log("\nBob private key:\t", bob.getPrivateKey().toString('hex'));
console.log("Bob public key:\t", bobKey.toString('hex'));
// Exchange and generate the secret...
const aliceSecret = alice.computeSecret(bobKey);
const bobSecret = bob.computeSecret(aliceKey);
console.log("\nAlice shared key:\t", aliceSecret.toString('hex'))
console.log("Bob shared key:\t\t", bobSecret.toString('hex'));
//wow it actually works?!
}
function mcPubKeyToPem(mcPubKeyBuffer) {
console.log(mcPubKeyBuffer)
if (mcPubKeyBuffer[0] == '-') return mcPubKeyBuffer
let pem = '-----BEGIN PUBLIC KEY-----\n'
let base64PubKey = mcPubKeyBuffer.toString('base64')
const maxLineLength = 65
while (base64PubKey.length > 0) {
pem += base64PubKey.substring(0, maxLineLength) + '\n'
base64PubKey = base64PubKey.substring(maxLineLength)
}
pem += '-----END PUBLIC KEY-----\n'
return pem
}
function readX509PublicKey(key) {
var reader = new Ber.Reader(Buffer.from(key, "base64"));
reader.readSequence();
reader.readSequence();
reader.readOID(); // Hey, I'm an elliptic curve
reader.readOID(); // This contains the curve type, could be useful
return Buffer.from(reader.readString(Ber.BitString, true)).slice(1);
}
function testMC() {
// const pubKeyBuf = Buffer.from(constants.PUBLIC_KEY, 'base64')
// const pem = mcPubKeyToPem(pubKeyBuf)
// console.log(mcPubKeyToPem(pubKeyBuf))
// const publicKey = crypto.createPublicKey({ key: pem, format: 'der' })
const pubKeyBuf = readX509PublicKey(constants.PUBLIC_KEY)
// console.log('Mojang pub key', pubKeyBuf.toString('hex'), publicKey)
startClientboundEncryption(pubKeyBuf)
}
function testMC2() {
// const mojangPubKeyBuf = Buffer.from('MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8ELkixyLcwlZryUQcu1TvPOmI2B7vX83ndnWRUaXm74wFfa5f/lwQNTfrLVHa2PmenpGI6JhIMUJaWZrjmMj90NoKNFSNBuKdm8rYiXsfaz3K36x/1U26HpG0ZxK/V1V', 'base64')
// const pem = mcPubKeyToPem(mojangPubKeyBuf)
// const publicKey = crypto.createPublicKey({ key: pem })
const publicKey = readX509PublicKey(constants.PUBLIC_KEY)
const curve = 'secp384r1'
const alice = crypto.createECDH(curve)
// const keys = crypto.generateKeyPair('ec',)
// const bob = crypto.generateKeyPairSync('ec', {
// namedCurve: type
// })
// alice.setPrivateKey(bob.privateKey.export({ type: 'pkcs8', format: 'pem' }))
// alice.setPublicKey(bob.publicKey.export({ type: 'spki', format: 'pem' }))
// console.log(bob)
const aliceKey = alice.generateKeys()
const alicePEM = ec_pem(alice, curve)
const alicePEMPrivate = alicePEM.encodePrivateKey()
const alicePEMPublic = alicePEM.encodePublicKey()
// const alicePublicKey = aliceKey.toString('base64')
// const alicePrivateKey = alice.getPrivateKey().toString('base64')
const aliceSecret = alice.computeSecret(publicKey, null, 'hex')
console.log('Alice private key PEM', alicePEMPrivate)
console.log('Alice public key PEM', alicePEMPublic)
console.log('Alice public key', alice.getPublicKey('base64'))
console.log('Alice secret key', aliceSecret)
var sign = crypto.createSign('RSA-SHA256')
sign.write('something')
sign.end()
// // const pem2 =
// // `-----BEGIN PRIVATE KEY-----
// // ${alice.getPrivateKey('base64')}
// // -----END PRIVATE KEY-----`
// console.log('PEM', bob.privateKey)
const sig = sign.sign(alicePEMPrivate, 'hex')
console.log('Signature', sig)
const token = JWT.sign({
salt: 'HELLO',
signedToken: alice.getPublicKey('base64')
}, alicePEMPrivate, { algorithm: 'ES384' })
console.log('Token', token)
const verified = JWT.verify(token, alicePEMPublic, { algorithms: 'ES384' })
console.log('Verified!', verified)
}
function testMC3() {
var Ber = require('asn1').Ber;
var reader = new Ber.Reader(new Buffer(constants.PUBLIC_KEY, "base64"));
reader.readSequence();
reader.readSequence();
reader.readOID(); // Hey, I'm an elliptic curve
reader.readOID(); // This contains the curve type, could be useful
var pubKey = reader.readString(Ber.BitString, true).slice(1);
var server = crypto.createECDH('secp384r1');
server.generateKeys();
console.log(server.computeSecret(pubKey));
}
// testECDH2()
testMC2()

View file

@ -1,88 +0,0 @@
const crypto = require('crypto')
const JWT = require('jsonwebtoken')
const constants = require('./constants')
const { Ber } = require('asn1')
const ec_pem = require('ec-pem')
function readX509PublicKey(key) {
var reader = new Ber.Reader(Buffer.from(key, "base64"));
reader.readSequence();
reader.readSequence();
reader.readOID(); // Hey, I'm an elliptic curve
reader.readOID(); // This contains the curve type, could be useful
return Buffer.from(reader.readString(Ber.BitString, true)).slice(1);
}
function writeX509PublicKey(key) {
var writer = new Ber.Writer();
writer.startSequence();
writer.startSequence();
writer.writeOID("1.2.840.10045.2.1");
writer.writeOID("1.3.132.0.34");
writer.endSequence();
writer.writeBuffer(Buffer.concat([Buffer.from([0x00]), key]), Ber.BitString);
writer.endSequence();
return writer.buffer.toString("base64");
}
function test(pubKey = constants.PUBLIC_KEY) {
const publicKey = readX509PublicKey(pubKey)
const curve = 'secp384r1'
const alice = crypto.createECDH(curve)
const aliceKey = alice.generateKeys()
const alicePEM = ec_pem(alice, curve)
const alicePEMPrivate = alicePEM.encodePrivateKey()
const alicePEMPublic = alicePEM.encodePublicKey()
const aliceSecret = alice.computeSecret(publicKey, null, 'hex')
console.log('Alice private key PEM', alicePEMPrivate)
console.log('Alice public key PEM', alicePEMPublic)
console.log('Alice public key', alice.getPublicKey('hex'))
console.log('Alice secret key', aliceSecret)
// Test signing manually
const sign = crypto.createSign('RSA-SHA256')
sign.write('🧂')
sign.end()
const sig = sign.sign(alicePEMPrivate, 'hex')
console.log('Signature', sig)
// Test JWT sign+verify
const x509 = writeX509PublicKey(alice.getPublicKey())
const token = JWT.sign({
salt: 'HELLO',
signedToken: alice.getPublicKey('base64')
}, alicePEMPrivate, { algorithm: 'ES384', header: { x5u: x509 } })
console.log('Encoded JWT', token)
// send the jwt to the client...
const verified = JWT.verify(token, alicePEMPublic, { algorithms: 'ES384' })
console.log('Decoded JWT', verified)
// Good
}
/**
* Alice private key PEM -----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDBGgHZwH3BzieyJrdrVTVLmrEoUxpDUSqSzS98lobTXeUxJR/OmywPV
57I8YtnsJlCgBwYFK4EEACKhZANiAATjvTRgjsxKruO7XbduSQoHeR/6ouIm4Rmc
La9EkSpLFpuYZfsdtq9Vcf2t3Q3+jIbXjD/wNo97P4Hr5ghXG8sCVV7jpqadOF8j
SzyfajLGfX9mkS5WWLAg+dpi/KiEo/g=
-----END EC PRIVATE KEY-----
Alice public key PEM -----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4700YI7MSq7ju123bkkKB3kf+qLiJuEZ
nC2vRJEqSxabmGX7HbavVXH9rd0N/oyG14w/8DaPez+B6+YIVxvLAlVe46amnThf
I0s8n2oyxn1/ZpEuVliwIPnaYvyohKP4
-----END PUBLIC KEY-----
Alice public key 04e3bd34608ecc4aaee3bb5db76e490a07791ffaa2e226e1199c2daf44912a4b169b9865fb1db6af5571fdaddd0dfe8c86d78c3ff0368f7b3f81ebe608571bcb02555ee3a6a69d385f234b3c9f6a32c67d7f66912e5658b020f9da62fca884a3f8
Alice secret key 76feb5d420b33907c4841a74baa707b717a29c021b17b6662fd46dba3227cac3e256eee9e890edb0308f66a3119b4914
Signature 3066023100d5ea70b8fc5e441c5e93d9f7dcde031f54291011c950a4aa8625ea9b27f7c798a8bc4de40baf35d487a05db6b5c628c6023100ae06cc2ea65db77138163c546ccf13933faae3d91bd6aa7108b99539cdb1c86f1e8a3704cb099f0b00eebed4ee75ccb2
Encoded JWT eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzYWx0IjoiSEVMTE8iLCJzaWduZWRUb2tlbiI6IkJPTzlOR0NPekVxdTQ3dGR0MjVKQ2dkNUgvcWk0aWJoR1p3dHIwU1JLa3NXbTVobCt4MjJyMVZ4L2EzZERmNk1odGVNUC9BMmozcy9nZXZtQ0ZjYnl3SlZYdU9tcHAwNFh5TkxQSjlxTXNaOWYyYVJMbFpZc0NENTJtTDhxSVNqK0E9PSIsImlhdCI6MTYxMTc4MDYwNX0._g8k086U7nD-Tthn8jGWuuM3Q4EfhgqCfFA1Q5ePmjqhqMHOJvmrCz6tWsCytr2i-a2M51fb9K_YDAHbZ66Kos9ZkjF4Tqz5fPS880fM9woZ_1xjh7nGcOQ6sbY81zyi
Decoded JWT {
salt: 'HELLO',
signedToken: 'BOO9NGCOzEqu47tdt25JCgd5H/qi4ibhGZwtr0SRKksWm5hl+x22r1Vx/a3dDf6MhteMP/A2j3s/gevmCFcbywJVXuOmpp04XyNLPJ9qMsZ9f2aRLlZYsCD52mL8qISj+A==',
iat: 1611780605
}
*/
test()

View file

@ -1,49 +0,0 @@
function test() {
const chain = require('./sampleChain.json').chain
let data = {}
// There are three JWT tokens sent to us, one signed by the client
// one signed by Mojang with the Mojang token we have and another one
// from Xbox with addition user profile data
// We verify that at least one of the tokens in the chain has been properly
// signed by Mojang by checking the x509 public key in the JWT headers
let didVerify = false
let pubKey = mcPubKeyToPem(constants.PUBLIC_KEY_NEW)
console.log(pubKey)
for (var token of chain) {
// const decoded = jwt.decode(token, pubKey, 'ES384')
console.log('Decoding...', token)
const decoded = JWT.verify(token, pubKey, { algorithms: 'ES384' })
console.log('Decoded...')
console.log('Decoded', decoded)
// Check if signed by Mojang key
const [header] = token.split('.')
const hdec = Buffer.from(header, 'base64').toString('utf-8')
const hjson = JSON.parse(hdec)
if (hjson.x5u == constants.PUBLIC_KEY && !data.extraData?.XUID) {
didVerify = true
console.log('verified with mojang key!', hjson.x5u)
}
pubKey = mcPubKeyToPem(decoded.identityPublicKey)
data = { ...data, ...decoded }
}
console.log('Result', data)
}
function test2() {
const chain = require('./login.json')
const token = chain.data.clientData
// console.log(token)
const pubKey = mcPubKeyToPem(constants.CDATA_PUBLIC_KEY)
const decoded = JWT.verify(token, pubKey, { algorithms: 'ES384' })
// console.log('Decoded', decoded)
fs.writeFileSync('clientData.json', JSON.stringify(decoded))
}

View file

@ -1,22 +1,25 @@
const fs = require('fs')
const debug = require('debug')('minecraft-protocol')
const auth = require('./client/auth')
const Options = require('./options')
const { Connection } = require('./connection')
const { createDeserializer, createSerializer } = require('./transforms/serializer')
const { Encrypt } = require('./auth/encryption')
const auth = require('./client/auth')
const Options = require('./options')
const { RakClient } = require('./Rak')
const { serialize } = require('./datatypes/util')
const debugging = false
class Client extends Connection {
/** @param {{ version: number, hostname: string, port: number }} options */
constructor(options) {
super()
this.options = { ...Options.defaultOptions, ...options }
this.serializer = createSerializer()
this.deserializer = createDeserializer()
this.validateOptions()
this.serializer = createSerializer(this.options.version)
this.deserializer = createDeserializer(this.options.version)
Encrypt(this, null, options)
Encrypt(this, null, this.options)
if (options.password) {
auth.authenticatePassword(this, options)
@ -28,26 +31,29 @@ class Client extends Connection {
this.startQueue()
this.inLog = (...args) => console.info('C ->', ...args)
this.outLog = (...args) => console.info('C <-', ...args)
// this.on('decrypted', this.onDecryptedPacket)
}
validateOptions() {
// console.log('Options', this.options)
if (!this.options.hostname || this.options.port == null) throw Error('Invalid hostname/port')
if (this.options.version < Options.MIN_VERSION) {
throw new Error(`Unsupported protocol version < ${Options.MIN_VERSION} : ${this.options.version}`)
if (!Options.Versions[this.options.version]) {
console.warn('Supported versions: ', Options.Versions)
throw Error(`Unsupported version ${this.options.version}`)
}
this.options.protocolVersion = Options.Versions[this.options.version]
if (this.options.protocolVersion < Options.MIN_VERSION) {
throw new Error(`Protocol version < ${Options.MIN_VERSION} : ${this.options.protocolVersion}, too old`)
}
}
onEncapsulated = (encapsulated, inetAddr) => {
// log(inetAddr.address, ': Encapsulated', encapsulated)
const buffer = Buffer.from(encapsulated.buffer)
this.handle(buffer)
}
connect = async (sessionData) => {
const hostname = this.options.hostname || '127.0.0.1'
const port = this.options.port || 19132
const hostname = this.options.hostname
const port = this.options.port
this.connection = new RakClient({ useWorkers: true, hostname, port })
this.connection.onConnected = () => this.sendLogin()
@ -64,14 +70,13 @@ class Client extends Connection {
]
const encodedChain = JSON.stringify({ chain })
// const skinChain = JSON.stringify({})
const bodyLength = this.clientUserChain.length + encodedChain.length + 8
debug('Auth chain', chain)
this.write('login', {
protocol_version: this.options.version,
protocol_version: this.options.protocolVersion,
payload_size: bodyLength,
chain: encodedChain,
client_data: this.clientUserChain
@ -83,7 +88,7 @@ class Client extends Connection {
// We're talking over UDP, so there is no connection to close, instead
// we stop communicating with the server
console.warn(`Server requested ${packet.hide_disconnect_reason ? 'silent disconnect' : 'disconnect'}: ${packet.message}`)
process.exit(1)
process.exit(1) // TODO: handle
}
close() {
@ -109,32 +114,28 @@ class Client extends Connection {
}
readPacket(packet) {
// console.log('packet', 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)*/)
// No idea what this exotic 0xA0 packet is, it's not implemented anywhere
// and seems empty. Possible gibberish from the raknet impl
if (pakData.name == '160' || !pakData.name) { // eslint-ignore-line
console.warn('?? Ignoring extraneous packet ', des)
return
}
// Packet verifying (decode + re-encode + match test)
if (pakData.name) {
this.tryRencode(pakData.name, pakData.params, packet)
}
// console.info('->', JSON.stringify(pakData, (k,v) => typeof v == 'bigint' ? v.toString() : v))
// Packet dumping
try {
if (!fs.existsSync(`./packets/${pakData.name}.json`)) {
fs.writeFileSync(`./packets/${pakData.name}.json`, serialize(pakData.params, 2))
fs.writeFileSync(`./packets/${pakData.name}.txt`, packet.toString('hex'))
if (debugging) {
// Packet verifying (decode + re-encode + match test)
if (pakData.name) {
this.tryRencode(pakData.name, pakData.params, packet)
}
} catch { }
// console.info('->', JSON.stringify(pakData, (k,v) => typeof v == 'bigint' ? v.toString() : v))
// Packet dumping
try {
const root = __dirname + `../data/${this.options.version}/sample/`
if (!fs.existsSync(root + `packets/${pakData.name}.json`)) {
fs.writeFileSync(root + `packets/${pakData.name}.json`, serialize(pakData.params, 2))
fs.writeFileSync(root + `packets/${pakData.name}.txt`, packet.toString('hex'))
}
} catch { }
}
// Abstract some boilerplate before sending to listeners
switch (des.data.name) {
case 'server_to_client_handshake':
this.emit('client.server_handshake', des.data.params)
@ -142,27 +143,22 @@ class Client extends Connection {
case 'disconnect': // Client kicked
this.onDisconnectRequest(des.data.params)
break
case 'crafting_data':
fs.writeFileSync('crafting.json', JSON.stringify(des.data.params, (k, v) => typeof v == 'bigint' ? v.toString() : v))
break
case 'start_game':
fs.writeFileSync('start_game.json', JSON.stringify(des.data.params, (k, v) => typeof v == 'bigint' ? v.toString() : v))
break
case 'level_chunk':
// fs.writeFileSync(`./chunks/chunk-${chunks++}.txt`, packet.toString('hex'))
break
// case 'crafting_data':
// fs.writeFileSync('crafting.json', JSON.stringify(des.data.params, (k, v) => typeof v == 'bigint' ? v.toString() : v))
// break
// case 'start_game':
// fs.writeFileSync('start_game.json', JSON.stringify(des.data.params, (k, v) => typeof v == 'bigint' ? v.toString() : v))
// break
// case 'level_chunk':
// // fs.writeFileSync(`./chunks/chunk-${chunks++}.txt`, packet.toString('hex'))
// break
default:
// console.log('Sending to listeners')
}
this.emit(des.data.name, des.data.params)
// Emit packet
this.emit(des.data.name, des.data.params)
}
}
var chunks = 0;
function serialize(obj = {}, fmt) {
return JSON.stringify(obj, (k, v) => typeof v == 'bigint' ? v.toString() : v, fmt)
}
module.exports = { Client }

View file

@ -1,16 +1,5 @@
const XboxLiveAuth = require('@xboxreplay/xboxlive-auth')
const debug = require('debug')('minecraft-protocol')
const fetch = require('node-fetch')
const authConstants = require('./authConstants')
const { MsAuthFlow } = require('./authFlow.js')
const getFetchOptions = {
headers: {
'Content-Type': 'application/json',
'User-Agent': 'node-minecraft-protocol'
}
}
/**
* Obtains Minecaft profile data using a Minecraft access token and starts the join sequence
* @param {object} client - The client passed to protocol
@ -71,33 +60,25 @@ async function authenticateDeviceCode (client, options) {
}
}
function checkStatus (res) {
if (res.ok) { // res.status >= 200 && res.status < 300
return res.json()
} else {
throw Error(res.statusText)
}
}
module.exports = {
authenticatePassword,
authenticateDeviceCode
}
async function msaTest () {
// MsAuthFlow.resetTokenCaches()
// async function msaTest () {
// // MsAuthFlow.resetTokenCaches()
await authenticateDeviceCode({
connect(...args) {
console.log('Connecting', args)
},
emit(...e) {
console.log('Event', e)
}
}, {})
}
// await authenticateDeviceCode({
// connect(...args) {
// console.log('Connecting', args)
// },
// emit(...e) {
// console.log('Event', e)
// }
// }, {})
// }
// debug with node microsoftAuth.js
if (!module.parent) {
msaTest()
}
// // debug with node microsoftAuth.js
// if (!module.parent) {
// msaTest()
// }

View file

@ -2,11 +2,26 @@ const BinaryStream = require('@jsprismarine/jsbinaryutils').default
const BatchPacket = require('./datatypes/BatchPacket')
const cipher = require('./transforms/encryption')
const { EventEmitter } = require('events')
const Reliability = require('jsp-raknet/protocol/reliability')
const Versions = require('./options')
const debug = require('debug')('minecraft-protocol')
class Connection extends EventEmitter {
versionLessThan(version) {
if (typeof version === 'string') {
return Versions[version] < this.options.version
} else {
return version < this.options.version
}
}
versionGreaterThan(version) {
if (typeof version === 'string') {
return Versions[version] > this.options.version
} else {
return version > this.options.version
}
}
startEncryption(iv) {
this.encryptionEnabled = true
this.inLog('Started encryption', this.sharedSecret, iv)
@ -15,14 +30,10 @@ class Connection extends EventEmitter {
this.q2 = []
}
write(name, params) { // TODO: Batch
// console.log('Need to encode', name, params)
var s = this.connect ? 'C' : 'S'
if (this.downQ) s += 'P'
this.outLog('NB <- ' + s, name,params)
write(name, params) {
this.outLog('sending', name, params)
const batch = new BatchPacket()
const packet = this.serializer.createPacketBuffer({ name, params })
// console.log('Sending buf', packet.toString('hex').)
batch.addEncodedPacket(packet)
if (this.encryptionEnabled) {
@ -51,8 +62,6 @@ class Connection extends EventEmitter {
//TODO: can we just build Batch before the queue loop?
const batch = new BatchPacket()
this.outLog('<- BATCH', this.q2)
// For now, we're over conservative so send max 3 packets
// per batch and hold the rest for the next tick
const sending = []
for (let i = 0; i < this.q.length; i++) {
const packet = this.q.shift()
@ -65,28 +74,10 @@ class Connection extends EventEmitter {
} else {
this.sendDecryptedBatch(batch)
}
// this.q2 = []
}
}, 20)
}
writeRaw(name, buffer) { // skip protodef serializaion
// temporary hard coded stuff
const batch = new BatchPacket()
if (name == 'biome_definition_list') {
// so we can send nbt straight from file without parsing
const stream = new BinaryStream()
stream.writeUnsignedVarInt(0x7a)
stream.append(buffer)
batch.addEncodedPacket(stream.getBuffer())
}
if (this.encryptionEnabled) {
this.sendEncryptedBatch(batch)
} else {
this.sendDecryptedBatch(batch)
}
}
/**
* Sends a MCPE packet buffer
@ -121,21 +112,11 @@ class Connection extends EventEmitter {
// TODO: Rename this to sendEncapsulated
sendMCPE(buffer, immediate) {
this.connection.sendReliable(buffer, immediate)
// if (this.worker) {
// this.outLog('-> buf', buffer)
// this.worker.postMessage({ type: 'queueEncapsulated', packet: buffer, immediate })
// } else {
// const sendPacket = new EncapsulatedPacket()
// sendPacket.reliability = Reliability.ReliableOrdered
// sendPacket.buffer = buffer
// this.connection.addEncapsulatedToQueue(sendPacket)
// if (immediate) this.connection.sendQueue()
// }
}
// These are callbacks called from encryption.js
onEncryptedPacket = (buf) => {
this.outLog('ENC BUF', buf)
this.outLog('Enc buf', buf)
const packet = Buffer.concat([Buffer.from([0xfe]), buf]) // add header
this.outLog('Sending wrapped encrypted batch', packet)
@ -143,8 +124,6 @@ class Connection extends EventEmitter {
}
onDecryptedPacket = (buf) => {
// console.log('🟢 Decrypted', buf)
const stream = new BinaryStream(buf)
const packets = BatchPacket.getPackets(stream)
@ -168,10 +147,7 @@ class Connection extends EventEmitter {
}
}
}
// console.log('[client] handled incoming ', buffer)
}
}
function serialize(obj = {}, fmt) {
return JSON.stringify(obj, (k, v) => typeof v == 'bigint' ? v.toString() : v, fmt)
}
module.exports = { Connection }

View file

@ -1,12 +0,0 @@
module.exports = {
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
},
waitFor(cb, withTimeout) {
return Promise.race([
new Promise((res, rej) => cb(res)),
sleep(withTimeout)
])
}
}

37
src/datatypes/util.js Normal file
View file

@ -0,0 +1,37 @@
const fs = require('fs');
function getFiles(dir) {
var results = [];
var list = fs.readdirSync(dir);
list.forEach(function (file) {
file = dir + '/' + file;
var stat = fs.statSync(file);
if (stat && stat.isDirectory()) {
/* Recurse into a subdirectory */
results = results.concat(getFiles(file));
} else {
/* Is a file */
results.push(file);
}
});
return results;
}
module.exports = {
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
},
waitFor(cb, withTimeout) {
return Promise.race([
new Promise((res, rej) => cb(res)),
sleep(withTimeout)
])
},
serialize(obj = {}, fmt) {
return JSON.stringify(obj, (k, v) => typeof v == 'bigint' ? v.toString() : v, fmt)
},
getFiles
}

View file

@ -1,12 +1,16 @@
// Minimum supported version (< will be kicked)
const MIN_VERSION = 422
const MIN_VERSION = '1.16.201'
// Currently supported verson
const CURRENT_VERSION = 422
const CURRENT_VERSION = '1.16.201'
const defaultOptions = {
// https://minecraft.gamepedia.com/Protocol_version#Bedrock_Edition_2
version: CURRENT_VERSION
}
module.exports = { defaultOptions, MIN_VERSION, CURRENT_VERSION }
const Versions = {
'1.16.210': 428,
'1.16.201': 422
}
module.exports = { defaultOptions, MIN_VERSION, CURRENT_VERSION, Versions }

View file

@ -3,6 +3,7 @@ const Listener = require('jsp-raknet/listener')
const EncapsulatedPacket = require('jsp-raknet/protocol/encapsulated_packet')
const RakClient = require('jsp-raknet/client')
const ConnWorker = require('./rakWorker')
const { waitFor } = require('./datatypes/util')
try {
var { Client, Server, PacketPriority, PacketReliability, McPingMessage } = require('raknet-native')
} catch (e) {
@ -12,14 +13,14 @@ try {
class RakNativeClient extends EventEmitter {
constructor(options) {
super()
this.onConnected = () => {}
this.onCloseConnection = () => {}
this.onEncapsulated = () => {}
this.onConnected = () => { }
this.onCloseConnection = () => { }
this.onEncapsulated = () => { }
this.raknet = new Client(options.hostname, options.port, 'minecraft')
this.raknet.on('encapsulated', thingy => {
// console.log('Encap',thingy)
const { buffer, address, guid }=thingy
const { buffer, address, guid } = thingy
this.onEncapsulated(buffer, address)
})
this.raknet.on('connected', () => {
@ -51,18 +52,17 @@ class RakNativeClient extends EventEmitter {
class RakNativeServer extends EventEmitter {
constructor(options = {}) {
super()
console.log('opts',options)
this.onOpenConnection = () => {}
this.onCloseConnection = () => {}
this.onEncapsulated = () => {}
this.onOpenConnection = () => { }
this.onCloseConnection = () => { }
this.onEncapsulated = () => { }
this.raknet = new Server(options.hostname, options.port, {
maxConnections: options.maxConnections || 3,
minecraft: { },
minecraft: {},
message: new McPingMessage().toBuffer()
})
this.raknet.on('openConnection', (client) => {
client.sendReliable = function(buffer, immediate) {
client.sendReliable = function (buffer, immediate) {
const priority = immediate ? PacketPriority.IMMEDIATE_PRIORITY : PacketPriority.MEDIUM_PRIORITY
return this.send(buffer, priority, PacketReliability.RELIABLE_ORDERED, 0)
}
@ -70,12 +70,13 @@ class RakNativeServer extends EventEmitter {
})
this.raknet.on('closeConnection', (client) => {
console.log('!!! Client CLOSED CONNECTION!')
console.warn('! Client closed connection')
// TODO: need to handle this properly..
this.onCloseConnection(client)
})
this.raknet.on('encapsulated', (thingy) => {
const { buffer, address, guid }=thingy
const { buffer, address, guid } = thingy
// console.log('ENCAP',thingy)
this.onEncapsulated(buffer, address)
})
@ -89,8 +90,8 @@ class RakNativeServer extends EventEmitter {
class RakJsClient extends EventEmitter {
constructor(options = {}) {
super()
this.onConnected = () => {}
this.onEncapsulated = () => {}
this.onConnected = () => { }
this.onEncapsulated = () => { }
if (options.useWorkers) {
this.connect = this.workerConnect
this.sendReliable = this.workerSendReliable
@ -145,9 +146,9 @@ class RakJsServer extends EventEmitter {
constructor(options = {}) {
super()
this.options = options
this.onOpenConnection = () => {}
this.onCloseConnection = () => {}
this.onEncapsulated = () => {}
this.onOpenConnection = () => { }
this.onCloseConnection = () => { }
this.onEncapsulated = () => { }
if (options.useWorkers) {
throw Error('nyi')
@ -159,13 +160,13 @@ class RakJsServer extends EventEmitter {
async plainListen() {
this.raknet = new Listener()
await this.raknet.listen(this.options.hostname, this.options.port)
this.raknet.on('openConnection', (conn) => {
conn.sendReliable = function(buffer, immediate) {
this.raknet.on('openConnection', (conn) => {
conn.sendReliable = function (buffer, immediate) {
const sendPacket = new EncapsulatedPacket()
sendPacket.reliability = Reliability.ReliableOrdered
sendPacket.buffer = buffer
this.connection.addEncapsulatedToQueue(sendPacket)
if (immediate) this.raknet.sendQueue()
if (immediate) this.raknet.sendQueue()
}
this.onOpenConnection(conn)
})
@ -174,7 +175,7 @@ class RakJsServer extends EventEmitter {
}
}
module.exports = {
RakClient: Client ? RakNativeClient : RakJsClient,
module.exports = {
RakClient: Client ? RakNativeClient : RakJsClient,
RakServer: Server ? RakNativeServer : RakJsServer
}

View file

@ -4,6 +4,7 @@ const { Client } = require("./client")
const { Server } = require("./server")
const { Player } = require("./serverPlayer")
const debug = require('debug')('minecraft-protocol relay')
const { serialize } = require('./datatypes/util')
/** @typedef {{ hostname: string, port: number, auth: 'client' | 'server' | null, destination?: { hostname: string, port: number } }} Options */
@ -67,7 +68,6 @@ class RelayPlayer extends Player {
console.warn('Old', packet.toString('hex'))
console.log('Failed to re-encode', name, params)
process.exit(1)
throw Error('re-encoding fail for' + name + ' - ' + JSON.stringify(params))
}
}
@ -159,7 +159,7 @@ class Relay extends Server {
client.once('join', () => { // Intercept once handshaking done
ds.upstream = client
ds.flushUpQueue()
console.log('UPSTREAM HAS JOINED')
console.log('Connected to upstream server')
client.readPacket = (packet) => ds.readUpstream(packet)
})
this.upstreams.set(clientAddr.hash, client)
@ -167,7 +167,7 @@ class Relay extends Server {
closeUpstreamConnection(clientAddr) {
const up = this.upstreams.get(clientAddr.hash)
if (!up) throw Error(`unable to close non-existant connection ${clientAddr.hash}`)
if (!up) throw Error(`unable to close non-open connection ${clientAddr.hash}`)
up.close()
this.upstreams.delete(clientAddr.hash)
debug('relay closed connection', clientAddr)
@ -188,43 +188,5 @@ class Relay extends Server {
}
}
function serialize(obj = {}, fmt) {
return JSON.stringify(obj, (k, v) => typeof v == 'bigint' ? v.toString() : v, fmt)
}
function createRelay() {
console.log('Creating relay')
/**
* Example to create a non-transparent proxy (or 'Relay') connection to destination server
* In Relay we de-code and re-encode packets
*/
const relay = new Relay({
/* Hostname and port for clients to listen to */
hostname: '0.0.0.0',
port: 19130,
/**
* Who does the authentication
* If set to `client`, all connecting clients will be sent a message with a link to authenticate
* If set to `server`, the server will authenticate and only one client will be able to join
* (Default) If set to `none`, no authentication will be done
*/
auth: 'server',
/**
* Sets if packets will automatically be forwarded. If set to false, you must listen for on('packet')
* events and
*/
auto: true,
/* Where to send upstream packets to */
destination: {
hostname: '127.0.0.1',
port: 19132,
// encryption: true
}
})
relay.create()
}
createRelay()
// Too many things called 'Proxy' ;)
module.exports = { Relay }

View file

@ -9,18 +9,23 @@ class Server extends EventEmitter {
constructor(options) {
super()
this.options = { ...Options.defaultOptions, ...options }
this.serializer = createSerializer()
this.deserializer = createDeserializer()
this.validateOptions()
this.serializer = createSerializer(this.options.version)
this.deserializer = createDeserializer(this.options.version)
this.clients = {}
this.clientCount = 0
this.validateOptions()
this.inLog = (...args) => console.debug('S', ...args)
this.outLog = (...args) => console.debug('S', ...args)
this.inLog = (...args) => console.debug('C -> S', ...args)
this.outLog = (...args) => console.debug('S -> C', ...args)
}
validateOptions() {
if (this.options.version < Options.MIN_VERSION) {
throw new Error(`Unsupported protocol version < ${Options.MIN_VERSION} : ${this.options.version}`)
if (!Options.Versions[this.options.version]) {
console.warn('Supported versions: ', Options.Versions)
throw Error(`Unsupported version ${this.options.version}`)
}
this.options.protocolVersion = Options.Versions[this.options.version]
if (this.options.protocolVersion < Options.MIN_VERSION) {
throw new Error(`Protocol version < ${Options.MIN_VERSION} : ${this.options.protocolVersion}, too old`)
}
}
@ -39,7 +44,7 @@ class Server extends EventEmitter {
}
onEncapsulated = (buffer, address) => {
debug(address, 'Encapsulated', buffer)
this.inLog('encapsulated', address, buffer)
const client = this.clients[address]
if (!client) {
throw new Error(`packet from unknown inet addr: ${address}`)
@ -57,6 +62,4 @@ class Server extends EventEmitter {
}
}
const hash = (inetAddr) => inetAddr.address + '/' + inetAddr.port
module.exports = { Server }

View file

@ -16,15 +16,14 @@ class Player extends Connection {
this.server = server
this.serializer = server.serializer
this.deserializer = server.deserializer
// console.log('serializer/des',this.serializer,this.deserializer)
this.connection = connection
this.options = server.options
Encrypt(this, server, this.options)
this.startQueue()
this.status = ClientStatus.Authenticating
this.inLog = (...args) => console.info('S ->', ...args)
this.outLog = (...args) => console.info('S <-', ...args)
this.inLog = (...args) => console.info('S -> C', ...args)
this.outLog = (...args) => console.info('C -> S', ...args)
}
getData() {
@ -33,17 +32,17 @@ class Player extends Connection {
onLogin(packet) {
let body = packet.data
debug('Body', body)
// debug('Login body', body)
this.emit('loggingIn', body)
const clientVer = body.protocol_version
if (this.server.options.version) {
if (this.server.options.version < clientVer) {
this.sendDisconnectStatus(failed_client)
if (this.server.options.protocolVersion) {
if (this.server.options.protocolVersion < clientVer) {
this.sendDisconnectStatus('failed_client')
return
}
} else if (clientVer < MIN_VERSION) {
this.sendDisconnectStatus(failed_client)
this.sendDisconnectStatus('failed_client')
return
}
@ -55,6 +54,7 @@ class Player extends Connection {
var { key, userData, chain } = decodeLoginJWT(authChain.chain, skinChain)
} catch (e) {
console.error(e)
// TODO: disconnect user
throw new Error('Failed to verify user')
}
console.log('Verified user', 'got pub key', key, userData)
@ -66,7 +66,6 @@ class Player extends Connection {
this.version = clientVer
}
/**
* Disconnects a client before it has joined
* @param {string} play_status

View file

@ -1,20 +1,20 @@
const { PassThrough, Transform } = require('readable-stream')
const { Transform } = require('readable-stream')
const crypto = require('crypto')
const aesjs = require('aes-js')
const Zlib = require('zlib')
const CIPHER = 'aes-256-cfb8'
const CIPHER_ALG = 'aes-256-cfb8'
function createCipher(secret, initialValue) {
if (crypto.getCiphers().includes(CIPHER)) {
return crypto.createCipheriv(CIPHER, secret, initialValue)
if (crypto.getCiphers().includes(CIPHER_ALG)) {
return crypto.createCipheriv(CIPHER_ALG, secret, initialValue)
}
return new Cipher(secret, initialValue)
}
function createDecipher(secret, initialValue) {
if (crypto.getCiphers().includes(CIPHER)) {
return crypto.createDecipheriv(CIPHER, secret, initialValue)
if (crypto.getCiphers().includes(CIPHER_ALG)) {
return crypto.createDecipheriv(CIPHER_ALG, secret, initialValue)
}
return new Decipher(secret, initialValue)
}
@ -54,14 +54,11 @@ class Decipher extends Transform {
function computeCheckSum(packetPlaintext, sendCounter, secretKeyBytes) {
let digest = crypto.createHash('sha256');
let counter = Buffer.alloc(8)
// writeLI64(sendCounter, counter, 0);
counter.writeBigInt64LE(sendCounter, 0)
// console.log('Send counter', counter)
digest.update(counter);
digest.update(packetPlaintext);
digest.update(secretKeyBytes);
let hash = digest.digest();
// console.log('Hash', hash.toString('hex'))
return hash.slice(0, 8);
}
@ -74,21 +71,14 @@ function createEncryptor(client, iv) {
function process(chunk) {
const buffer = Zlib.deflateRawSync(chunk, { level: 7 })
// client.outLog('🟡 Compressed', buffer, client.sendCounter)
const packet = Buffer.concat([buffer, computeCheckSum(buffer, client.sendCounter, client.secretKeyBytes)])
client.sendCounter++
// client.outLog('writing to cipher...', packet, client.secretKeyBytes, iv)
client.cipher.write(packet)
}
// const stream = new PassThrough()
client.cipher.on('data', client.onEncryptedPacket)
return (blob) => {
// client.outLog(client.options ? 'C':'S', '🟡 Encrypting', client.sendCounter, blob)
// stream.write(blob)
process(blob)
}
}
@ -99,6 +89,8 @@ function createDecryptor(client, iv) {
client.receiveCounter = client.receiveCounter || 0n
function verify(chunk) {
// TODO: remove the extra logic here, probably fixed with new raknet impl
// console.log('Decryptor: checking checksum', client.receiveCounter, chunk)
// client.outLog('🔵 Inflating', chunk)
// First try to zlib decompress, then see how much bytes get read
@ -139,8 +131,6 @@ function createDecryptor(client, iv) {
client.decipher.on('data', verify)
return (blob) => {
// client.inLog(client.options ? 'C':'S', ' 🔵 Decrypting', client.receiveCounter, blob)
// client.inLog('Using shared key', client.secretKeyBytes, iv)
client.decipher.write(blob)
}
}

View file

@ -1,9 +1,9 @@
const { ProtoDefCompiler, CompiledProtodef } = require('protodef').Compiler
const { FullPacketParser, Serializer } = require('protodef')
function createProtocol() {
const protocol = require('../../data/newproto.json').types
console.log('Proto', protocol)
// Compiles the ProtoDef schema at runtime
function createProtocol(version) {
const protocol = require(`../../data/${version}/protocol.json`).types
var compiler = new ProtoDefCompiler()
compiler.addTypesToCompile(protocol)
compiler.addTypes(require('../datatypes/compiler-minecraft'))
@ -13,8 +13,8 @@ function createProtocol() {
return compiledProto
}
function getProtocol() {
// Loads already generated read/write/sizeof code
function getProtocol(version) {
const compiler = new ProtoDefCompiler()
compiler.addTypes(require('../datatypes/compiler-minecraft'))
compiler.addTypes(require('prismarine-nbt/compiler-zigzag'))
@ -26,22 +26,19 @@ function getProtocol() {
}
return new CompiledProtodef(
compile(compiler.sizeOfCompiler, '../../data/size.js'),
compile(compiler.writeCompiler, '../../data/write.js'),
compile(compiler.readCompiler, '../../data/read.js')
// compiler.sizeOfCompiler.compile(fs.readFileSync(__dirname + '/../../data/size.js', 'utf-8')),
// compiler.writeCompiler.compile(fs.readFileSync(__dirname + '/../../data/write.js', 'utf-8')),
// compiler.readCompiler.compile(fs.readFileSync(__dirname + '/../../data/read.js', 'utf-8'))
compile(compiler.sizeOfCompiler, `../../data/${version}/size.js`),
compile(compiler.writeCompiler, `../../data/${version}/write.js`),
compile(compiler.readCompiler, `../../data/${version}/read.js`)
)
}
function createSerializer() {
var proto = getProtocol()
function createSerializer(version) {
var proto = getProtocol(version)
return new Serializer(proto, 'mcpe_packet');
}
function createDeserializer() {
var proto = getProtocol()
function createDeserializer(version) {
var proto = getProtocol(version)
return new FullPacketParser(proto, 'mcpe_packet');
}