Move protocol to node-nethernet
This commit is contained in:
parent
80c0a131af
commit
bd11068a17
17 changed files with 5 additions and 911 deletions
|
|
@ -21,7 +21,6 @@
|
|||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jsprismarine/jsbinaryutils": "^5.5.3",
|
||||
"debug": "^4.3.1",
|
||||
"json-bigint": "^1.0.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
|
|
@ -29,14 +28,13 @@
|
|||
"minecraft-data": "^3.0.0",
|
||||
"minecraft-folder-path": "^1.2.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-nethernet": "github:LucienHH/node-nethernet#protocol",
|
||||
"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",
|
||||
"sdp-transform": "^2.14.2",
|
||||
"uuid-1345": "^1.0.2",
|
||||
"werift": "^0.19.9",
|
||||
"ws": "^8.18.0",
|
||||
"xbox-rta": "^2.1.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const assert = require('assert')
|
|||
|
||||
const { getRandomUint64 } = require('./datatypes/util')
|
||||
const { serverAuthenticate } = require('./client/auth')
|
||||
const { SignalType } = require('./nethernet/signalling')
|
||||
const { SignalType } = require('node-nethernet')
|
||||
|
||||
/** @param {{ port?: number, version?: number, networkId?: string, transport?: string, delayedInit?: boolean }} options */
|
||||
function createServer (options) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
const { waitFor } = require('./datatypes/util')
|
||||
const { Client } = require('./nethernet/client')
|
||||
const { Server } = require('./nethernet/server')
|
||||
const { Client, Server } = require('node-nethernet')
|
||||
|
||||
class NethernetClient {
|
||||
constructor (options = {}) {
|
||||
|
|
|
|||
|
|
@ -1,307 +0,0 @@
|
|||
const dgram = require('node:dgram')
|
||||
const { write } = require('sdp-transform')
|
||||
const { EventEmitter } = require('node:events')
|
||||
const { RTCIceCandidate, RTCPeerConnection } = require('werift')
|
||||
|
||||
const { Connection } = require('./connection')
|
||||
const { SignalType, SignalStructure } = require('./signalling')
|
||||
|
||||
const { getBroadcastAddress } = require('./net')
|
||||
const { PACKET_TYPE } = require('./discovery/packets/Packet')
|
||||
const { RequestPacket } = require('./discovery/packets/RequestPacket')
|
||||
const { MessagePacket } = require('./discovery/packets/MessagePacket')
|
||||
const { ResponsePacket } = require('./discovery/packets/ResponsePacket')
|
||||
const { decrypt, encrypt, calculateChecksum } = require('./discovery/crypto')
|
||||
|
||||
const { getRandomUint64 } = require('./util')
|
||||
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
|
||||
const PORT = 7551
|
||||
const BROADCAST_ADDRESS = getBroadcastAddress()
|
||||
|
||||
class Client extends EventEmitter {
|
||||
constructor (networkId) {
|
||||
super()
|
||||
|
||||
this.serverNetworkId = networkId
|
||||
|
||||
this.networkId = getRandomUint64()
|
||||
debug('C: Generated networkId:', this.networkId)
|
||||
|
||||
this.connectionId = getRandomUint64()
|
||||
debug('C: Generated connectionId:', this.connectionId)
|
||||
|
||||
this.socket = dgram.createSocket('udp4')
|
||||
|
||||
this.socket.on('message', (buffer, rinfo) => {
|
||||
debug('C: Received message from', rinfo.address, ':', rinfo.port)
|
||||
this.processPacket(buffer, rinfo)
|
||||
})
|
||||
|
||||
this.responses = new Map()
|
||||
this.addresses = new Map()
|
||||
|
||||
this.credentials = []
|
||||
|
||||
this.signalHandler = this.sendDiscoveryMessage
|
||||
|
||||
this.sendDiscoveryRequest()
|
||||
debug('C: Sent initial discovery request')
|
||||
|
||||
this.pingInterval = setInterval(() => {
|
||||
debug('C: Sending periodic discovery request')
|
||||
this.sendDiscoveryRequest()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
async handleCandidate (signal) {
|
||||
debug('C: Handling ICE candidate signal:', signal)
|
||||
await this.rtcConnection.addIceCandidate(new RTCIceCandidate({ candidate: signal.data }))
|
||||
}
|
||||
|
||||
async handleAnswer (signal) {
|
||||
debug('C: Handling answer signal:', signal)
|
||||
await this.rtcConnection.setRemoteDescription({ type: 'answer', sdp: signal.data })
|
||||
}
|
||||
|
||||
async createOffer () {
|
||||
debug('C: Creating RTC offer')
|
||||
this.rtcConnection = new RTCPeerConnection({
|
||||
iceServers: this.credentials
|
||||
})
|
||||
|
||||
this.connection = new Connection(this, this.connectionId, this.rtcConnection)
|
||||
|
||||
const candidates = []
|
||||
|
||||
this.rtcConnection.onicecandidate = (e) => {
|
||||
if (e.candidate) {
|
||||
debug('C: Collected ICE candidate:', e.candidate.candidate)
|
||||
candidates.push(e.candidate.candidate)
|
||||
}
|
||||
}
|
||||
|
||||
this.connection.setChannels(
|
||||
this.rtcConnection.createDataChannel('ReliableDataChannel'),
|
||||
this.rtcConnection.createDataChannel('UnreliableDataChannel')
|
||||
)
|
||||
|
||||
this.connection.reliable.onopen = () => { this.emit('connected', this.connection) }
|
||||
|
||||
this.rtcConnection.onconnectionstatechange = () => {
|
||||
const state = this.rtcConnection.connectionState
|
||||
debug('C: Connection state changed:', state)
|
||||
if (state === 'disconnected') this.emit('disconnect', this.connectionId, 'disconnected')
|
||||
}
|
||||
|
||||
await this.rtcConnection.createOffer()
|
||||
|
||||
const ice = this.rtcConnection.iceTransports[0]
|
||||
const dtls = this.rtcConnection.dtlsTransports[0]
|
||||
|
||||
if (!ice || !dtls) {
|
||||
debug('C: Failed to create ICE or DTLS transports')
|
||||
throw new Error('Failed to create transports')
|
||||
}
|
||||
|
||||
const iceParams = ice.iceGather.localParameters
|
||||
const dtlsParams = dtls.localParameters
|
||||
|
||||
if (dtlsParams.fingerprints.length === 0) {
|
||||
debug('C: No DTLS fingerprints available')
|
||||
throw new Error('local DTLS parameters has no fingerprints')
|
||||
}
|
||||
|
||||
const desc = write({
|
||||
version: 0,
|
||||
origin: {
|
||||
username: '-',
|
||||
sessionId: getRandomUint64().toString(),
|
||||
sessionVersion: 2,
|
||||
netType: 'IN',
|
||||
ipVer: 4,
|
||||
address: '127.0.0.1'
|
||||
},
|
||||
name: '-',
|
||||
timing: { start: 0, stop: 0 },
|
||||
groups: [{ type: 'BUNDLE', mids: '0' }],
|
||||
extmapAllowMixed: 'extmap-allow-mixed',
|
||||
msidSemantic: { semantic: '', token: 'WMS' },
|
||||
media: [
|
||||
{
|
||||
rtp: [],
|
||||
fmtp: [],
|
||||
type: 'application',
|
||||
port: 9,
|
||||
protocol: 'UDP/DTLS/SCTP',
|
||||
payloads: 'webrtc-datachannel',
|
||||
connection: { ip: '0.0.0.0', version: 4 },
|
||||
iceUfrag: iceParams.usernameFragment,
|
||||
icePwd: iceParams.password,
|
||||
iceOptions: 'trickle',
|
||||
fingerprint: { type: dtlsParams.fingerprints[0].algorithm, hash: dtlsParams.fingerprints[0].value },
|
||||
setup: 'active',
|
||||
mid: '0',
|
||||
sctpPort: 5000,
|
||||
maxMessageSize: 65536
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
await this.rtcConnection.setLocalDescription({ type: 'offer', sdp: desc })
|
||||
|
||||
debug('C: Local SDP set:', desc)
|
||||
|
||||
this.signalHandler(
|
||||
new SignalStructure(SignalType.ConnectRequest, this.connectionId, desc, this.serverNetworkId)
|
||||
)
|
||||
|
||||
for (const candidate of candidates) {
|
||||
debug('C: Sending ICE candidate signal:', candidate)
|
||||
this.signalHandler(
|
||||
new SignalStructure(SignalType.CandidateAdd, this.connectionId, candidate, this.serverNetworkId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
processPacket (buffer, rinfo) {
|
||||
debug('C: Processing packet from', rinfo.address, ':', rinfo.port)
|
||||
if (buffer.length < 32) {
|
||||
debug('C: Received packet is too short')
|
||||
throw new Error('Packet is too short')
|
||||
}
|
||||
|
||||
const decryptedData = decrypt(buffer.slice(32))
|
||||
const checksum = calculateChecksum(decryptedData)
|
||||
|
||||
if (Buffer.compare(buffer.slice(0, 32), checksum) !== 0) {
|
||||
debug('C: Checksum mismatch for packet from', rinfo.address)
|
||||
throw new Error('Checksum mismatch')
|
||||
}
|
||||
|
||||
const packetType = decryptedData.readUInt16LE(2)
|
||||
debug('C: Packet type:', packetType)
|
||||
|
||||
switch (packetType) {
|
||||
case PACKET_TYPE.DISCOVERY_REQUEST:
|
||||
debug('C: Received DISCOVERY_REQUEST packet')
|
||||
break
|
||||
case PACKET_TYPE.DISCOVERY_RESPONSE:
|
||||
debug('C: Received DISCOVERY_RESPONSE packet')
|
||||
this.handleResponse(new ResponsePacket(decryptedData).decode(), rinfo)
|
||||
break
|
||||
case PACKET_TYPE.DISCOVERY_MESSAGE:
|
||||
debug('C: Received DISCOVERY_MESSAGE packet')
|
||||
this.handleMessage(new MessagePacket(decryptedData).decode())
|
||||
break
|
||||
default:
|
||||
debug('C: Unknown packet type:', packetType)
|
||||
throw new Error('Unknown packet type')
|
||||
}
|
||||
}
|
||||
|
||||
handleResponse (packet, rinfo) {
|
||||
debug('C: Handling discovery response from', rinfo.address, 'with data:', packet)
|
||||
this.addresses.set(packet.senderId, rinfo)
|
||||
this.responses.set(packet.senderId, packet.data)
|
||||
this.emit('pong', packet)
|
||||
}
|
||||
|
||||
handleMessage (packet) {
|
||||
debug('C: Handling discovery message:', packet)
|
||||
if (packet.data === 'Ping') {
|
||||
debug('C: Ignoring ping message')
|
||||
return
|
||||
}
|
||||
|
||||
const signal = SignalStructure.fromString(packet.data)
|
||||
|
||||
signal.networkId = packet.senderId
|
||||
|
||||
debug('C: Processing signal:', signal)
|
||||
this.handleSignal(signal)
|
||||
}
|
||||
|
||||
handleSignal (signal) {
|
||||
debug('C: Handling signal of type:', signal.type)
|
||||
switch (signal.type) {
|
||||
case SignalType.ConnectResponse:
|
||||
debug('C: Handling ConnectResponse signal')
|
||||
this.handleAnswer(signal)
|
||||
break
|
||||
case SignalType.CandidateAdd:
|
||||
debug('C: Handling CandidateAdd signal')
|
||||
this.handleCandidate(signal)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
sendDiscoveryRequest () {
|
||||
debug('C: Sending discovery request')
|
||||
const requestPacket = new RequestPacket()
|
||||
|
||||
requestPacket.senderId = this.networkId
|
||||
|
||||
requestPacket.encode()
|
||||
|
||||
const buf = requestPacket.getBuffer()
|
||||
|
||||
const packetToSend = Buffer.concat([calculateChecksum(buf), encrypt(buf)])
|
||||
|
||||
this.socket.send(packetToSend, PORT, BROADCAST_ADDRESS)
|
||||
}
|
||||
|
||||
sendDiscoveryMessage (signal) {
|
||||
debug('C: Sending discovery message for signal:', signal)
|
||||
const rinfo = this.addresses.get(signal.networkId)
|
||||
|
||||
if (!rinfo) {
|
||||
debug('C: No address found for networkId:', signal.networkId)
|
||||
return
|
||||
}
|
||||
|
||||
const messagePacket = new MessagePacket()
|
||||
|
||||
messagePacket.senderId = this.networkId
|
||||
messagePacket.recipientId = BigInt(signal.networkId)
|
||||
messagePacket.data = signal.toString()
|
||||
messagePacket.encode()
|
||||
|
||||
const buf = messagePacket.getBuffer()
|
||||
|
||||
const packetToSend = Buffer.concat([calculateChecksum(buf), encrypt(buf)])
|
||||
|
||||
this.socket.send(packetToSend, rinfo.port, rinfo.address)
|
||||
}
|
||||
|
||||
async connect () {
|
||||
debug('C: Initiating connection')
|
||||
this.running = true
|
||||
|
||||
await this.createOffer()
|
||||
}
|
||||
|
||||
send (buffer) {
|
||||
this.connection.send(buffer)
|
||||
}
|
||||
|
||||
ping () {
|
||||
debug('C: Sending ping')
|
||||
|
||||
this.sendDiscoveryRequest()
|
||||
}
|
||||
|
||||
close (reason) {
|
||||
debug('C: Closing client with reason:', reason)
|
||||
if (!this.running) return
|
||||
clearInterval(this.pingInterval)
|
||||
this.connection?.close()
|
||||
setTimeout(() => this.socket.close(), 100)
|
||||
this.connection = null
|
||||
this.running = false
|
||||
this.removeAllListeners()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Client }
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
const debug = require('debug')('minecraft-protocol')
|
||||
|
||||
const MAX_MESSAGE_SIZE = 10_000
|
||||
|
||||
class Connection {
|
||||
constructor (nethernet, address, rtcConnection) {
|
||||
this.nethernet = nethernet
|
||||
|
||||
this.address = address
|
||||
|
||||
this.rtcConnection = rtcConnection
|
||||
|
||||
this.reliable = null
|
||||
|
||||
this.unreliable = null
|
||||
|
||||
this.promisedSegments = 0
|
||||
|
||||
this.buf = Buffer.alloc(0)
|
||||
}
|
||||
|
||||
setChannels (reliable, unreliable) {
|
||||
if (reliable) {
|
||||
this.reliable = reliable
|
||||
this.reliable.onmessage = (msg) => this.handleMessage(msg.data)
|
||||
}
|
||||
if (unreliable) {
|
||||
this.unreliable = unreliable
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage (data) {
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data)
|
||||
}
|
||||
|
||||
if (data.length < 2) {
|
||||
throw new Error('Unexpected EOF')
|
||||
}
|
||||
|
||||
const segments = data[0]
|
||||
|
||||
debug(`handleMessage segments: ${segments}`)
|
||||
|
||||
data = data.subarray(1)
|
||||
|
||||
if (this.promisedSegments > 0 && this.promisedSegments - 1 !== segments) {
|
||||
throw new Error(`Invalid promised segments: expected ${this.promisedSegments - 1}, got ${segments}`)
|
||||
}
|
||||
|
||||
this.promisedSegments = segments
|
||||
|
||||
this.buf = this.buf ? Buffer.concat([this.buf, data]) : data
|
||||
|
||||
if (this.promisedSegments > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this.nethernet.emit('encapsulated', this.buf, this.address)
|
||||
|
||||
this.buf = null
|
||||
}
|
||||
|
||||
send (data) {
|
||||
if (!this.reliable || this.reliable.readyState !== 'open') {
|
||||
throw new Error('Reliable data channel is not available')
|
||||
}
|
||||
|
||||
let n = 0
|
||||
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data)
|
||||
}
|
||||
|
||||
let segments = Math.ceil(data.length / MAX_MESSAGE_SIZE)
|
||||
|
||||
for (let i = 0; i < data.length; i += MAX_MESSAGE_SIZE) {
|
||||
segments--
|
||||
|
||||
let end = i + MAX_MESSAGE_SIZE
|
||||
if (end > data.length) end = data.length
|
||||
|
||||
const frag = data.subarray(i, end)
|
||||
const message = Buffer.concat([Buffer.from([segments]), frag])
|
||||
|
||||
debug('Sending fragment', segments, 'header', message[0])
|
||||
|
||||
this.reliable.send(message)
|
||||
|
||||
n += frag.length
|
||||
}
|
||||
|
||||
if (segments !== 0) {
|
||||
throw new Error('Segments count did not reach 0 after sending all fragments')
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
close () {
|
||||
if (this.reliable) {
|
||||
this.reliable.close()
|
||||
}
|
||||
if (this.unreliable) {
|
||||
this.unreliable.close()
|
||||
}
|
||||
if (this.rtcConnection) {
|
||||
this.rtcConnection.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Connection }
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
const BinaryStream = require('@jsprismarine/jsbinaryutils').default
|
||||
|
||||
class ServerData extends BinaryStream {
|
||||
encode () {
|
||||
this.writeByte(this.version)
|
||||
this.writeString(this.motd)
|
||||
this.writeString(this.levelName)
|
||||
this.writeIntLE(this.gamemodeId)
|
||||
this.writeIntLE(this.playerCount)
|
||||
this.writeIntLE(this.playersMax)
|
||||
this.writeBoolean(this.isEditorWorld)
|
||||
this.writeBoolean(this.hardcore)
|
||||
this.writeIntLE(this.transportLayer)
|
||||
}
|
||||
|
||||
decode () {
|
||||
this.version = this.readByte()
|
||||
this.motd = this.readString()
|
||||
this.levelName = this.readString()
|
||||
this.gamemodeId = this.readIntLE()
|
||||
this.playerCount = this.readIntLE()
|
||||
this.playersMax = this.readIntLE()
|
||||
this.isEditorWorld = this.readBoolean()
|
||||
this.hardcore = this.readBoolean()
|
||||
this.transportLayer = this.readIntLE()
|
||||
}
|
||||
|
||||
readString () {
|
||||
return this.read(this.readByte()).toString()
|
||||
}
|
||||
|
||||
writeString (v) {
|
||||
this.writeByte(Buffer.byteLength(v))
|
||||
this.write(Buffer.from(v, 'utf-8'))
|
||||
}
|
||||
|
||||
prependLength () {
|
||||
const buf = Buffer.alloc(2)
|
||||
buf.writeUInt16LE(this.binary.length, 0)
|
||||
this.binary = [...buf, ...this.binary]
|
||||
this.writeIndex += 2
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ServerData }
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
const crypto = require('node:crypto')
|
||||
|
||||
const appIdBuffer = Buffer.allocUnsafe(8)
|
||||
appIdBuffer.writeBigUInt64LE(BigInt(0xdeadbeef))
|
||||
|
||||
const AES_KEY = crypto.createHash('sha256')
|
||||
.update(appIdBuffer)
|
||||
.digest()
|
||||
|
||||
function encrypt (data) {
|
||||
const cipher = crypto.createCipheriv('aes-256-ecb', AES_KEY, null)
|
||||
return Buffer.concat([cipher.update(data), cipher.final()])
|
||||
}
|
||||
|
||||
function decrypt (data) {
|
||||
const decipher = crypto.createDecipheriv('aes-256-ecb', AES_KEY, null)
|
||||
return Buffer.concat([decipher.update(data), decipher.final()])
|
||||
}
|
||||
|
||||
function calculateChecksum (data) {
|
||||
const hmac = crypto.createHmac('sha256', AES_KEY)
|
||||
hmac.update(data)
|
||||
return hmac.digest()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
encrypt,
|
||||
decrypt,
|
||||
calculateChecksum
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
const { PACKET_TYPE, Packet } = require('./Packet')
|
||||
|
||||
class MessagePacket extends Packet {
|
||||
constructor (data) {
|
||||
super(PACKET_TYPE.DISCOVERY_MESSAGE, data)
|
||||
}
|
||||
|
||||
encode () {
|
||||
super.encode()
|
||||
this.writeUnsignedLongLE(this.recipientId)
|
||||
|
||||
this.writeUnsignedIntLE(this.data.length)
|
||||
this.write(Buffer.from(this.data, 'utf-8'))
|
||||
|
||||
this.prependLength()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
decode () {
|
||||
super.decode()
|
||||
this.recipientId = this.readUnsignedLongLE()
|
||||
|
||||
const length = this.readUnsignedIntLE()
|
||||
this.data = this.read(length).toString()
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { MessagePacket }
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
const PACKET_TYPE = {
|
||||
DISCOVERY_REQUEST: 0,
|
||||
DISCOVERY_RESPONSE: 1,
|
||||
DISCOVERY_MESSAGE: 2
|
||||
}
|
||||
|
||||
const BinaryStream = require('@jsprismarine/jsbinaryutils').default
|
||||
|
||||
class Packet extends BinaryStream {
|
||||
constructor (id, buffer) {
|
||||
super(buffer)
|
||||
|
||||
this.id = id
|
||||
}
|
||||
|
||||
encode () {
|
||||
this.writeUnsignedShortLE(this.id)
|
||||
this.writeUnsignedLongLE(this.senderId)
|
||||
this.write(Buffer.alloc(8))
|
||||
}
|
||||
|
||||
decode () {
|
||||
this.packetLength = this.readUnsignedShortLE()
|
||||
this.id = this.readUnsignedShortLE()
|
||||
this.senderId = this.readUnsignedLongLE()
|
||||
this.read(8)
|
||||
}
|
||||
|
||||
prependLength () {
|
||||
const buf = Buffer.alloc(2)
|
||||
buf.writeUInt16LE(this.binary.length, 0)
|
||||
this.binary = [...buf, ...this.binary]
|
||||
this.writeIndex += 2
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { PACKET_TYPE, Packet }
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
const { PACKET_TYPE, Packet } = require('./Packet')
|
||||
|
||||
class RequestPacket extends Packet {
|
||||
constructor (data) {
|
||||
super(PACKET_TYPE.DISCOVERY_REQUEST, data)
|
||||
}
|
||||
|
||||
encode () {
|
||||
super.encode()
|
||||
|
||||
this.prependLength()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
decode () {
|
||||
super.decode()
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { RequestPacket }
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
const { PACKET_TYPE, Packet } = require('./Packet')
|
||||
|
||||
class ResponsePacket extends Packet {
|
||||
constructor (data) {
|
||||
super(PACKET_TYPE.DISCOVERY_RESPONSE, data)
|
||||
}
|
||||
|
||||
encode () {
|
||||
super.encode()
|
||||
const hex = this.data.toString('hex')
|
||||
|
||||
this.writeUnsignedIntLE(hex.length)
|
||||
this.write(Buffer.from(hex, 'utf-8'))
|
||||
|
||||
this.prependLength()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
decode () {
|
||||
super.decode()
|
||||
const length = this.readUnsignedIntLE()
|
||||
this.data = Buffer.from(this.read(length).toString('utf-8'), 'hex')
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ResponsePacket }
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
const os = require('node:os')
|
||||
|
||||
function getBroadcastAddress () {
|
||||
const interfaces = os.networkInterfaces()
|
||||
|
||||
for (const interfaceName in interfaces) {
|
||||
for (const iface of interfaces[interfaceName]) {
|
||||
// Only consider IPv4, non-internal (non-loopback) addresses
|
||||
if (iface.family === 'IPv4' && !iface.internal) {
|
||||
const ip = iface.address.split('.').map(Number)
|
||||
const netmask = iface.netmask.split('.').map(Number)
|
||||
const broadcast = ip.map((octet, i) => (octet | (~netmask[i] & 255)))
|
||||
|
||||
return broadcast.join('.') // Return the broadcast address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getBroadcastAddress
|
||||
}
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
const dgram = require('node:dgram')
|
||||
const { EventEmitter } = require('node:events')
|
||||
const { RTCIceCandidate, RTCPeerConnection } = require('werift')
|
||||
|
||||
const { Connection } = require('./connection')
|
||||
const { SignalStructure, SignalType } = require('./signalling')
|
||||
|
||||
const { PACKET_TYPE } = require('./discovery/packets/Packet')
|
||||
const { MessagePacket } = require('./discovery/packets/MessagePacket')
|
||||
const { ResponsePacket } = require('./discovery/packets/ResponsePacket')
|
||||
const { decrypt, encrypt, calculateChecksum } = require('./discovery/crypto')
|
||||
|
||||
const { getRandomUint64 } = require('./util')
|
||||
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
|
||||
class Server extends EventEmitter {
|
||||
constructor (options = {}) {
|
||||
super()
|
||||
|
||||
this.options = options
|
||||
|
||||
this.networkId = options.networkId ?? getRandomUint64()
|
||||
|
||||
this.connections = new Map()
|
||||
|
||||
debug('S: Server initialised with networkId: %s', this.networkId)
|
||||
}
|
||||
|
||||
async handleCandidate (signal) {
|
||||
const conn = this.connections.get(signal.connectionId)
|
||||
|
||||
if (conn) {
|
||||
debug('S: Adding ICE candidate for connectionId: %s', signal.connectionId)
|
||||
await conn.rtcConnection.addIceCandidate(new RTCIceCandidate({ candidate: signal.data }))
|
||||
} else {
|
||||
debug('S: Received candidate for unknown connection', signal)
|
||||
}
|
||||
}
|
||||
|
||||
async handleOffer (signal, respond, credentials = []) {
|
||||
debug('S: Handling offer for connectionId: %s', signal.connectionId)
|
||||
const rtcConnection = new RTCPeerConnection({
|
||||
iceServers: credentials
|
||||
})
|
||||
|
||||
const connection = new Connection(this, signal.connectionId, rtcConnection)
|
||||
|
||||
this.connections.set(signal.connectionId, connection)
|
||||
|
||||
rtcConnection.onicecandidate = (e) => {
|
||||
if (e.candidate) {
|
||||
debug('S: ICE candidate generated for connectionId: %s', signal.connectionId)
|
||||
respond(
|
||||
new SignalStructure(SignalType.CandidateAdd, signal.connectionId, e.candidate.candidate, signal.networkId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
rtcConnection.ondatachannel = ({ channel }) => {
|
||||
debug('S: Data channel established with label: %s', channel.label)
|
||||
if (channel.label === 'ReliableDataChannel') connection.setChannels(channel)
|
||||
if (channel.label === 'UnreliableDataChannel') connection.setChannels(null, channel)
|
||||
}
|
||||
|
||||
rtcConnection.onconnectionstatechange = () => {
|
||||
const state = rtcConnection.connectionState
|
||||
debug('S: Connection state changed for connectionId: %s, state: %s', signal.connectionId, state)
|
||||
if (state === 'connected') this.emit('openConnection', connection)
|
||||
if (state === 'disconnected') this.emit('closeConnection', signal.connectionId, 'disconnected')
|
||||
}
|
||||
|
||||
await rtcConnection.setRemoteDescription({ type: 'offer', sdp: signal.data })
|
||||
debug('S: Remote description set for connectionId: %s', signal.connectionId)
|
||||
|
||||
const answer = await rtcConnection.createAnswer()
|
||||
await rtcConnection.setLocalDescription(answer)
|
||||
debug('S: Local description set (answer) for connectionId: %s', signal.connectionId)
|
||||
|
||||
respond(
|
||||
new SignalStructure(SignalType.ConnectResponse, signal.connectionId, answer.sdp, signal.networkId)
|
||||
)
|
||||
}
|
||||
|
||||
processPacket (buffer, rinfo) {
|
||||
debug('S: Processing packet from %s:%s', rinfo.address, rinfo.port)
|
||||
if (buffer.length < 32) {
|
||||
debug('S: Packet is too short')
|
||||
throw new Error('Packet is too short')
|
||||
}
|
||||
|
||||
const decryptedData = decrypt(buffer.slice(32))
|
||||
|
||||
const checksum = calculateChecksum(decryptedData)
|
||||
|
||||
if (Buffer.compare(buffer.slice(0, 32), checksum) !== 0) {
|
||||
debug('S: Checksum mismatch')
|
||||
throw new Error('Checksum mismatch')
|
||||
}
|
||||
|
||||
const packetType = decryptedData.readUInt16LE(2)
|
||||
|
||||
debug('S: Packet type: %s', packetType)
|
||||
switch (packetType) {
|
||||
case PACKET_TYPE.DISCOVERY_REQUEST:
|
||||
debug('S: Handling discovery request')
|
||||
this.handleRequest(rinfo)
|
||||
break
|
||||
case PACKET_TYPE.DISCOVERY_RESPONSE:
|
||||
debug('S: Discovery response received (ignored)')
|
||||
break
|
||||
case PACKET_TYPE.DISCOVERY_MESSAGE:
|
||||
debug('S: Handling discovery message')
|
||||
this.handleMessage(new MessagePacket(decryptedData).decode(), rinfo)
|
||||
break
|
||||
default:
|
||||
debug('S: Unknown packet type: %s', packetType)
|
||||
throw new Error('Unknown packet type')
|
||||
}
|
||||
}
|
||||
|
||||
setAdvertisement (buffer) {
|
||||
debug('S: Setting advertisement data')
|
||||
this.advertisement = buffer
|
||||
}
|
||||
|
||||
handleRequest (rinfo) {
|
||||
debug('S: Handling request from %s:%s', rinfo.address, rinfo.port)
|
||||
const data = this.advertisement
|
||||
|
||||
if (!data) {
|
||||
debug('S: Advertisement data not set')
|
||||
return new Error('Advertisement data not set yet')
|
||||
}
|
||||
|
||||
const responsePacket = new ResponsePacket()
|
||||
|
||||
responsePacket.senderId = this.networkId
|
||||
responsePacket.data = data
|
||||
|
||||
responsePacket.encode()
|
||||
|
||||
const buf = responsePacket.getBuffer()
|
||||
|
||||
const packetToSend = Buffer.concat([calculateChecksum(buf), encrypt(buf)])
|
||||
|
||||
this.socket.send(packetToSend, rinfo.port, rinfo.address)
|
||||
debug('S: Response sent to %s:%s', rinfo.address, rinfo.port)
|
||||
}
|
||||
|
||||
handleMessage (packet, rinfo) {
|
||||
debug('S: Handling message from %s:%s', rinfo.address, rinfo.port)
|
||||
if (packet.data === 'Ping') {
|
||||
debug('S: Ping message received')
|
||||
return
|
||||
}
|
||||
|
||||
const respond = (signal) => {
|
||||
debug('S: Responding with signal: %o', signal)
|
||||
const messagePacket = new MessagePacket()
|
||||
|
||||
messagePacket.senderId = this.networkId
|
||||
messagePacket.recipientId = signal.networkId
|
||||
messagePacket.data = signal.toString()
|
||||
messagePacket.encode()
|
||||
|
||||
const buf = messagePacket.getBuffer()
|
||||
|
||||
const packetToSend = Buffer.concat([calculateChecksum(buf), encrypt(buf)])
|
||||
|
||||
this.socket.send(packetToSend, rinfo.port, rinfo.address)
|
||||
debug('S: Signal response sent to %s:%s', rinfo.address, rinfo.port)
|
||||
}
|
||||
|
||||
const signal = SignalStructure.fromString(packet.data)
|
||||
|
||||
signal.networkId = packet.senderId
|
||||
|
||||
switch (signal.type) {
|
||||
case SignalType.ConnectRequest:
|
||||
debug('S: Handling ConnectRequest signal')
|
||||
this.handleOffer(signal, respond)
|
||||
break
|
||||
case SignalType.CandidateAdd:
|
||||
debug('S: Handling CandidateAdd signal')
|
||||
this.handleCandidate(signal)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
async listen () {
|
||||
debug('S: Starting server')
|
||||
this.socket = dgram.createSocket('udp4')
|
||||
|
||||
this.socket.on('message', (buffer, rinfo) => {
|
||||
debug('S: Message received from %s:%s', rinfo.address, rinfo.port)
|
||||
this.processPacket(buffer, rinfo)
|
||||
})
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const failFn = e => reject(e)
|
||||
this.socket.once('error', failFn)
|
||||
this.socket.bind(7551, () => {
|
||||
debug('S: Server is listening on port 7551')
|
||||
this.socket.removeListener('error', failFn)
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
send (buffer) {
|
||||
this.connection.send(buffer)
|
||||
}
|
||||
|
||||
close (reason) {
|
||||
debug('S: Closing server: %s', reason)
|
||||
for (const conn of this.connections.values()) {
|
||||
conn.close()
|
||||
}
|
||||
|
||||
this.socket.close(() => {
|
||||
debug('S: Server closed')
|
||||
this.emit('close', reason)
|
||||
this.removeAllListeners()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Server }
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
const SignalType = {
|
||||
ConnectRequest: 'CONNECTREQUEST',
|
||||
ConnectResponse: 'CONNECTRESPONSE',
|
||||
CandidateAdd: 'CANDIDATEADD',
|
||||
ConnectError: 'CONNECTERROR'
|
||||
}
|
||||
|
||||
class SignalStructure {
|
||||
constructor (type, connectionId, data, networkId) {
|
||||
this.type = type
|
||||
this.connectionId = connectionId
|
||||
this.data = data
|
||||
this.networkId = networkId
|
||||
}
|
||||
|
||||
toString () {
|
||||
return `${this.type} ${this.connectionId} ${this.data}`
|
||||
}
|
||||
|
||||
static fromString (message) {
|
||||
const [type, connectionId, ...data] = message.split(' ')
|
||||
|
||||
return new this(type, BigInt(connectionId), data.join(' '))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { SignalStructure, SignalType }
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
const getRandomUint64 = () => {
|
||||
const high = Math.floor(Math.random() * 0xFFFFFFFF)
|
||||
const low = Math.floor(Math.random() * 0xFFFFFFFF)
|
||||
|
||||
return (BigInt(high) << 32n) | BigInt(low)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRandomUint64
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const { Versions, CURRENT_VERSION } = require('../options')
|
||||
|
||||
const { ServerData } = require('../nethernet/discovery/ServerData')
|
||||
const { ServerData } = require('node-nethernet')
|
||||
|
||||
class NethernetServerAdvertisement {
|
||||
version = 3
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const { WebSocket } = require('ws')
|
||||
const { stringify } = require('json-bigint')
|
||||
const { once, EventEmitter } = require('node:events')
|
||||
const { SignalStructure } = require('../nethernet/signalling')
|
||||
const { SignalStructure } = require('node-nethernet')
|
||||
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue