Add nethernet transport
This commit is contained in:
parent
1706c922b3
commit
9f356cb72e
11 changed files with 252 additions and 33 deletions
45
examples/client/nethernet.js
Normal file
45
examples/client/nethernet.js
Normal 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++
|
||||
})
|
||||
20
examples/server/nethernet.js
Normal file
20
examples/server/nethernet.js
Normal 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()} !`)
|
||||
})
|
||||
})
|
||||
11
package.json
11
package.json
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue