Add nethernet transport

This commit is contained in:
unknown 2024-10-04 01:11:54 +01:00 committed by LucienHH
commit 9f356cb72e
11 changed files with 252 additions and 33 deletions

View file

@ -0,0 +1,45 @@
process.env.DEBUG = '*'
const readline = require('readline')
const { createClient } = require('bedrock-protocol')
async function pickSession (availableSessions) {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
console.log('Available Sessions:')
availableSessions.forEach((session, index) => console.log(`${index + 1}. ${session.customProperties.hostName} ${session.customProperties.worldName} (${session.customProperties.version})`))
rl.question('Please select a session by number: ', (answer) => {
const sessionIndex = parseInt(answer) - 1
if (sessionIndex >= 0 && sessionIndex < availableSessions.length) {
const selectedSession = availableSessions[sessionIndex]
console.log(`You selected: ${selectedSession.customProperties.hostName} ${selectedSession.customProperties.worldName} (${selectedSession.customProperties.version})`)
resolve(selectedSession)
} else {
console.log('Invalid selection. Please try again.')
resolve(pickSession())
}
rl.close()
})
})
}
const client = createClient({
transport: 'nethernet', // Use the Nethernet transport
world: {
pickSession
}
})
let ix = 0
client.on('packet', (args) => {
console.log(`Packet ${ix} recieved`)
ix++
})

View file

@ -0,0 +1,20 @@
/* eslint-disable */
process.env.DEBUG = 'minecraft-protocol'
const bedrock = require('bedrock-protocol')
const server = bedrock.createServer({
transport: 'nethernet',
useSignalling: true, // disable for LAN connections only
motd: {
motd: 'Funtime Server',
levelName: 'Wonderland'
}
})
server.on('connect', client => {
client.on('join', () => { // The client has joined the server.
const date = new Date() // Once client is in the server, send a colorful kick message
client.disconnect(`Good ${date.getHours() < 12 ? '§emorning§r' : '§3afternoon§r'}\n\nMy time is ${date.toLocaleString()} !`)
})
})

View file

@ -21,17 +21,24 @@
],
"license": "MIT",
"dependencies": {
"@jsprismarine/jsbinaryutils": "^5.5.3",
"debug": "^4.3.1",
"json-bigint": "^1.0.0",
"jsonwebtoken": "^9.0.0",
"jsp-raknet": "^2.1.3",
"minecraft-data": "^3.0.0",
"minecraft-folder-path": "^1.2.0",
"prismarine-auth": "^2.0.0",
"node-fetch": "^2.6.1",
"prismarine-auth": "github:LucienHH/prismarine-auth#playfab",
"prismarine-nbt": "^2.0.0",
"prismarine-realms": "^1.1.0",
"protodef": "^1.14.0",
"raknet-native": "^1.0.3",
"uuid-1345": "^1.0.2"
"sdp-transform": "^2.14.2",
"uuid-1345": "^1.0.2",
"werift": "^0.20.0",
"ws": "^8.18.0",
"xbox-rta": "^2.1.0"
},
"optionalDependencies": {
"raknet-node": "^0.5.0"

View file

@ -5,6 +5,7 @@ const debug = require('debug')('minecraft-protocol')
const Options = require('./options')
const auth = require('./client/auth')
const initRaknet = require('./rak')
const { NethernetClient } = require('./nethernet')
const { KeyExchange } = require('./handshake/keyExchange')
const Login = require('./handshake/login')
const LoginVerify = require('./handshake/loginVerify')
@ -49,10 +50,21 @@ class Client extends Connection {
Login(this, null, this.options)
LoginVerify(this, null, this.options)
const { RakClient } = initRaknet(this.options.raknetBackend)
const host = this.options.host
const port = this.options.port
this.connection = new RakClient({ useWorkers: this.options.useRaknetWorkers, host, port }, this)
const networkId = this.options.networkId
if (this.options.transport === 'nethernet') {
this.connection = new NethernetClient({ networkId })
this.batchHeader = []
this.disableEncryption = true
} else if (this.options.transport === 'raknet') {
const { RakClient } = initRaknet(this.options.raknetBackend)
this.connection = new RakClient({ useWorkers: this.options.useRaknetWorkers, host, port }, this)
this.batchHeader = [0xfe]
this.disableEncryption = false
}
this.emit('connect_allowed')
}
@ -85,7 +97,16 @@ class Client extends Connection {
}
validateOptions () {
if (!this.options.host || this.options.port == null) throw Error('Invalid host/port')
switch (this.options.transport) {
case 'nethernet':
if (!this.options.networkId) throw Error('Invalid networkId')
break
case 'raknet':
if (!this.options.host || this.options.port == null) throw Error('Invalid host/port')
break
default:
throw Error(`Unsupported transport: ${this.options.transport} (nethernet, raknet)`)
}
Options.validateOptions(this.options)
}

View file

@ -48,6 +48,7 @@ class Connection extends EventEmitter {
}
startEncryption (iv) {
if (this.disableEncryption) return
this.encryptionEnabled = true
this.inLog?.('Started encryption', this.sharedSecret, iv)
this.decrypt = cipher.createDecryptor(this, iv)

View file

@ -5,6 +5,8 @@ const assert = require('assert')
const Options = require('./options')
const advertisement = require('./server/advertisement')
const auth = require('./client/auth')
const { NethernetClient } = require('./nethernet')
const { Signal } = require('./websocket/signal')
/** @param {{ version?: number, host: string, port?: number, connectTimeout?: number, skipPing?: boolean }} options */
function createClient (options) {
@ -17,20 +19,34 @@ function createClient (options) {
client.init()
} else {
ping(client.options).then(ad => {
const adVersion = ad.version?.split('.').slice(0, 3).join('.') // Only 3 version units
client.options.version = options.version ?? (Options.Versions[adVersion] ? adVersion : Options.CURRENT_VERSION)
if (client.options.transport === 'raknet') {
const adVersion = ad.version?.split('.').slice(0, 3).join('.') // Only 3 version units
client.options.version = options.version ?? (Options.Versions[adVersion] ? adVersion : Options.CURRENT_VERSION)
if (ad.portV4 && client.options.followPort) {
client.options.port = ad.portV4
if (ad.portV4 && client.options.followPort) {
client.options.port = ad.portV4
}
client.conLog?.(`Connecting to ${client.options.host}:${client.options.port} ${ad.motd} (${ad.levelName}), version ${ad.version} ${client.options.version !== ad.version ? ` (as ${client.options.version})` : ''}`)
} else if (client.options.transport === 'nethernet') {
client.conLog?.(`Connecting to ${client.options.networkId} ${ad.motd} (${ad.levelName})`)
}
client.conLog?.(`Connecting to ${client.options.host}:${client.options.port} ${ad.motd} (${ad.levelName}), version ${ad.version} ${client.options.version !== ad.version ? ` (as ${client.options.version})` : ''}`)
client.init()
}).catch(e => client.emit('error', e))
}).catch(e => {
if (!client.options.useSignalling) {
client.emit('error', e)
} else {
client.conLog?.('Could not ping server through local signalling, trying to connect over franchise signalling instead')
client.init()
}
})
}
}
if (options.realms) {
if (options.world) {
auth.worldAuthenticate(client, client.options).then(onServerInfo).catch(e => client.emit('error', e))
} else if (options.realms) {
auth.realmAuthenticate(client.options).then(onServerInfo).catch(e => client.emit('error', e))
} else {
onServerInfo()
@ -38,7 +54,19 @@ function createClient (options) {
return client
}
function connect (client) {
/** @param {Client} client */
async function connect (client) {
if (client.options.useSignalling) {
client.signalling = new Signal(client.connection.nethernet.networkId, client.options.authflow)
await client.signalling.connect()
client.connection.nethernet.credentials = client.signalling.credentials
client.connection.nethernet.signalHandler = client.signalling.write.bind(client.signalling)
client.signalling.on('signal', signal => client.connection.nethernet.handleSignal(signal))
}
// Actually connect
client.connect()
@ -86,15 +114,29 @@ function connect (client) {
clearInterval(keepalive)
})
}
client.once('close', () => {
if (client.session) client.session.end()
if (client.signalling) client.signalling.destroy()
})
}
async function ping ({ host, port }) {
const con = new RakClient({ host, port })
try {
return advertisement.fromServerName(await con.ping())
} finally {
con.close()
async function ping ({ host, port, networkId }) {
console.log('Pinging', host, port, networkId)
if (networkId) {
const con = new NethernetClient({ networkId })
try {
return advertisement.NethernetServerAdvertisement.fromBuffer(await con.ping())
} finally {
con.close()
}
} else {
const con = new RakClient({ host, port })
try {
return advertisement.fromServerName(await con.ping())
} finally {
con.close()
}
}
}

View file

@ -1,9 +1,53 @@
const { Server } = require('./server')
const { Signal } = require('./websocket/signal')
const assert = require('assert')
const { getRandomUint64 } = require('./datatypes/util')
const { serverAuthenticate } = require('./client/auth')
const { SignalType } = require('./nethernet/signalling')
/** @param {{ port?: number, version?: number, networkId?: string, transport?: string, delayedInit?: boolean }} options */
function createServer (options) {
assert(options)
if (!options.networkId) options.networkId = getRandomUint64()
if (!options.port) options.port = 19132
const server = new Server(options)
server.listen()
function startSignalling () {
if (server.options.transport === 'nethernet') {
server.signalling = new Signal(server.options.networkId, server.options.authflow)
server.signalling.connect()
.then(() => {
server.signalling.on('signal', (signal) => {
switch (signal.type) {
case SignalType.ConnectRequest:
server.transportServer.nethernet.handleOffer(signal, server.signalling.write, server.signalling.credentials)
break
case SignalType.CandidateAdd:
server.transportServer.nethernet.handleCandidate(signal)
break
}
})
})
.catch(e => server.emit('error', e))
}
}
if (server.options.useSignalling) {
serverAuthenticate(server, server.options)
.then(startSignalling)
.then(() => server.listen())
.catch(e => server.emit('error', e))
} else {
server.listen()
}
server.once('close', () => {
if (server.session) server.session.end()
if (server.signalling) server.signalling.destroy()
})
return server
}

View file

@ -1,5 +1,26 @@
const fs = require('fs')
const UUID = require('uuid-1345')
const { parse } = require('json-bigint')
const debug = require('debug')('minecraft-protocol')
async function checkStatus (res) {
if (res.ok) { // res.status >= 200 && res.status < 300
return res.text().then(parse)
} else {
const resp = await res.text()
debug('Request fail', resp)
throw Error(`${res.status} ${res.statusText} ${resp}`)
}
}
function getRandomUint64 () {
const high = Math.floor(Math.random() * 0xFFFFFFFF)
const low = Math.floor(Math.random() * 0xFFFFFFFF)
const result = (BigInt(high) << 32n) | BigInt(low)
return result
}
function getFiles (dir) {
let results = []
@ -45,4 +66,4 @@ function nextUUID () {
const isDebug = process.env.DEBUG?.includes('minecraft-protocol')
module.exports = { getFiles, sleep, waitFor, serialize, uuidFrom, nextUUID, isDebug }
module.exports = { getFiles, sleep, waitFor, serialize, uuidFrom, nextUUID, isDebug, getRandomUint64, checkStatus }

View file

@ -12,6 +12,8 @@ const skippedVersionsOnGithubCI = ['1.16.210', '1.17.10', '1.17.30', '1.18.11',
const testedVersions = process.env.CI ? Object.keys(Versions).filter(v => !skippedVersionsOnGithubCI.includes(v)) : Object.keys(Versions)
const defaultOptions = {
// Choice of raknet or nethernet
transport: 'raknet',
// https://minecraft.wiki/w/Protocol_version#Bedrock_Edition_2
version: CURRENT_VERSION,
// client: If we should send SetPlayerInitialized to the server after getting play_status spawn.

View file

@ -2,8 +2,9 @@ const { EventEmitter } = require('events')
const { createDeserializer, createSerializer } = require('./transforms/serializer')
const { Player } = require('./serverPlayer')
const { sleep } = require('./datatypes/util')
const { ServerAdvertisement } = require('./server/advertisement')
const { ServerAdvertisement, NethernetServerAdvertisement } = require('./server/advertisement')
const Options = require('./options')
const debug = globalThis.isElectron ? console.debug : require('debug')('minecraft-protocol')
class Server extends EventEmitter {
@ -13,12 +14,23 @@ class Server extends EventEmitter {
this.options = { ...Options.defaultOptions, ...options }
this.validateOptions()
this.RakServer = require('./rak')(this.options.raknetBackend).RakServer
if (this.options.transport === 'nethernet') {
this.transportServer = require('./nethernet').NethernetServer
this.advertisement = new NethernetServerAdvertisement(this.options.motd, this.options.version)
this.batchHeader = []
this.disableEncryption = true
} else if (this.options.transport === 'raknet') {
this.transportServer = require('./rak')(this.options.raknetBackend).RakServer
this.advertisement = new ServerAdvertisement(this.options.motd, this.options.port, this.options.version)
this.batchHeader = [0xfe]
this.disableEncryption = false
} else {
throw new Error(`Unsupported transport: ${this.options.transport} (nethernet, raknet)`)
}
this._loadFeatures(this.options.version)
this.serializer = createSerializer(this.options.version)
this.deserializer = createDeserializer(this.options.version)
this.advertisement = new ServerAdvertisement(this.options.motd, this.options.port, this.options.version)
this.advertisement.playersMax = options.maxPlayers ?? 3
/** @type {Object<string, Player>} */
this.clients = {}
@ -119,29 +131,31 @@ class Server extends EventEmitter {
async listen () {
const { host, port, maxPlayers } = this.options
this.raknet = new this.RakServer({ host, port, maxPlayers }, this)
// eslint-disable-next-line new-cap
this.transport = new this.transportServer({ host, port, networkId: this.options.networkId }, this)
try {
await this.raknet.listen()
await this.transport.listen()
} catch (e) {
console.warn(`Failed to bind server on [${this.options.host}]/${this.options.port}, is the port free?`)
throw e
}
this.conLog('Listening on', host, port, this.options.version)
this.raknet.onOpenConnection = this.onOpenConnection
this.raknet.onCloseConnection = this.onCloseConnection
this.raknet.onEncapsulated = this.onEncapsulated
this.raknet.onClose = (reason) => this.close(reason || 'Raknet closed')
this.transport.onOpenConnection = this.onOpenConnection
this.transport.onCloseConnection = this.onCloseConnection
this.transport.onEncapsulated = this.onEncapsulated
this.transport.onClose = (reason) => this.close(reason || 'Transport closed')
this.serverTimer = setInterval(() => {
this.raknet.updateAdvertisement()
this.transport.updateAdvertisement()
}, 1000)
return { host, port }
}
async close (disconnectReason = 'Server closed') {
this.emit('close', disconnectReason)
for (const caddr in this.clients) {
const client = this.clients[caddr]
client.disconnect(disconnectReason)
@ -153,7 +167,7 @@ class Server extends EventEmitter {
// Allow some time for client to get disconnect before closing connection.
await sleep(60)
this.raknet.close()
this.transport.close()
}
}

View file

@ -29,6 +29,8 @@ class Player extends Connection {
}
this.batchHeader = this.server.batchHeader
this.disableEncryption = this.server.disableEncryption
// Compression is server-wide
this.compressionAlgorithm = this.server.compressionAlgorithm
this.compressionLevel = this.server.compressionLevel