nethernet 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@ jobs:
13
13
 
14
14
  strategy:
15
15
  matrix:
16
- node-version: [18.x]
16
+ node-version: [24.x]
17
17
 
18
18
  steps:
19
19
  - uses: actions/checkout@v2
@@ -3,6 +3,9 @@ on:
3
3
  push:
4
4
  branches:
5
5
  - master # Change this to your default branch
6
+ permissions:
7
+ id-token: write
8
+ contents: write
6
9
  jobs:
7
10
  npm-publish:
8
11
  name: npm-publish
@@ -13,13 +16,12 @@ jobs:
13
16
  - name: Set up Node.js
14
17
  uses: actions/setup-node@master
15
18
  with:
16
- node-version: 18.0.0
19
+ node-version: 24
20
+ registry-url: 'https://registry.npmjs.org'
17
21
  - id: publish
18
- uses: JS-DevTools/npm-publish@v1
19
- with:
20
- token: ${{ secrets.NPM_AUTH_TOKEN }}
22
+ uses: JS-DevTools/npm-publish@v4
21
23
  - name: Create Release
22
- if: steps.publish.outputs.type != 'none'
24
+ if: ${{ steps.publish.outputs.type }}
23
25
  id: create_release
24
26
  uses: actions/create-release@v1
25
27
  env:
package/HISTORY.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## History
2
2
 
3
+ ### 0.1.0
4
+ * [Update CI to Node 24 (#10)](https://github.com/PrismarineJS/node-nethernet/commit/bbd59e2895a48454682df7eee460ff080f4e7df7) (thanks @rom1504)
5
+ * [Fix publish workflow for trusted publishing (#9)](https://github.com/PrismarineJS/node-nethernet/commit/63351452f0a4bffdf6c011f155dced9140369ceb) (thanks @rom1504)
6
+ * [Fix publish condition for npm-publish v4 (#8)](https://github.com/PrismarineJS/node-nethernet/commit/f945da96e87f93068b1605f8c242054d08e05515) (thanks @rom1504)
7
+ * [Switch to trusted publishing via OIDC (#7)](https://github.com/PrismarineJS/node-nethernet/commit/78777cdb6fab03c31d288ec7dba48bcf318ee8e2) (thanks @rom1504)
8
+ * [Initial implementation (#1)](https://github.com/PrismarineJS/node-nethernet/commit/622ad0e2dc144f5f5de5a33d4f77bc9bef388a40) (thanks @LucienHH)
9
+ * [init](https://github.com/PrismarineJS/node-nethernet/commit/d33f363e5e5e5a9a326cc9acb5e4d4cb0e22d27d) (thanks @extremeheat)
10
+ * [Initial commit](https://github.com/PrismarineJS/node-nethernet/commit/a2d42ca53c6751134e6e96625518b396d4b2b6c2) (thanks @extremeheat)
11
+
3
12
  ### 1.0.0
4
13
 
5
14
  * initial implementation
package/README.md CHANGED
@@ -6,20 +6,35 @@
6
6
 
7
7
  [![Official Discord](https://img.shields.io/static/v1.svg?label=OFFICIAL&message=DISCORD&color=blue&logo=discord&style=for-the-badge)](https://discord.gg/GsEFRM8)
8
8
 
9
- WIP
10
9
 
11
- A template repository to make it easy to create new prismarine repo
10
+ A Node.JS implementation of the NetherNet protocol.
12
11
 
13
- ## Usage
12
+ ## Install
14
13
 
15
- ```js
16
- const template = require('nethernet')
17
-
18
- template.helloWorld()
14
+ ```sh
15
+ npm install nethernet
19
16
  ```
20
17
 
21
- ## API
18
+ ## Example
19
+
20
+ ```ts
21
+ const { Client, Server } = require('node-nethernet')
22
+
23
+ const server = new Server()
24
+ // Client sends request to the broadcast address and server responds with a message
25
+ server.setAdvertisement(Buffer.from([0]))
26
+ const client = new Client(server.networkId)
27
+
28
+ client.on('encapsulated', (buffer) => {
29
+ console.assert(buffer.toString() === '\xA0 Hello world')
30
+ })
22
31
 
23
- ### helloWorld()
32
+ server.on('openConnection', (client) => {
33
+ client.send(Buffer.from('\xA0 Hello world'))
34
+ })
24
35
 
25
- Prints hello world
36
+ server.listen()
37
+
38
+ client.connect()
39
+
40
+ ```
package/example.js CHANGED
@@ -1,3 +1,18 @@
1
- const template = require('node-nethernet')
1
+ const { Client, Server } = require('nethernet')
2
2
 
3
- template.helloWorld()
3
+ const server = new Server()
4
+ // Client sends request to broadcast address and server responds with a message
5
+ server.setAdvertisement(Buffer.from([0]))
6
+ const client = new Client(server.networkId)
7
+
8
+ client.on('encapsulated', (buffer) => {
9
+ console.assert(buffer.toString() === '\xA0 Hello world')
10
+ })
11
+
12
+ server.on('openConnection', (client) => {
13
+ client.send(Buffer.from('\xA0 Hello world'))
14
+ })
15
+
16
+ server.listen()
17
+
18
+ client.connect()
package/index.d.ts ADDED
@@ -0,0 +1,132 @@
1
+ import EventEmitter from 'node:events'
2
+ import { RemoteInfo, Socket } from 'node:dgram'
3
+ import { PeerConnection, DataChannel, IceServer } from 'node-datachannel'
4
+
5
+ declare module 'nethernet' {
6
+
7
+ export interface ResponsePacket {
8
+ binary: number[]
9
+ buffer: Buffer
10
+ writeIndex: number
11
+ readIndex: number
12
+ id: number
13
+ packetLength: number
14
+ senderId: bigint
15
+ data: Buffer
16
+ }
17
+
18
+ export class Connection {
19
+ nethernet: Client | Server;
20
+ address: bigint;
21
+ rtcConnection: PeerConnection;
22
+ reliable: DataChannel | null;
23
+ unreliable: DataChannel | null;
24
+ promisedSegments: number;
25
+ buf: Buffer | null;
26
+ sendQueue: Buffer[];
27
+
28
+ constructor(nethernet: Client | Server, address: bigint, rtcConnection: PeerConnection);
29
+ setChannels(reliable?: DataChannel | null, unreliable?: DataChannel | null): void;
30
+ handleMessage(data: Buffer | string | ArrayBuffer): void;
31
+ send(data: Buffer | string): number;
32
+ sendNow(data: Buffer): number;
33
+ flushQueue(): void;
34
+ close(): void;
35
+ }
36
+
37
+ export interface ServerOptions {
38
+ networkId?: bigint;
39
+ }
40
+
41
+ export interface ServerEvents {
42
+ openConnection: (connection: Connection) => void;
43
+ closeConnection: (connectionId: bigint, reason: string) => void;
44
+ encapsulated: (data: Buffer, connectionId: bigint) => void;
45
+ close: (reason?: string) => void;
46
+ }
47
+
48
+ export class Server extends EventEmitter {
49
+ options: ServerOptions;
50
+ networkId: bigint;
51
+ connections: Map<bigint, Connection>;
52
+ advertisement?: Buffer;
53
+ socket: Socket;
54
+ serializer: any;
55
+ deserializer: any;
56
+
57
+ constructor(options?: ServerOptions);
58
+ handleCandidate(signal: SignalStructure): Promise<void>;
59
+ handleOffer(signal: SignalStructure, respond: (signal: SignalStructure) => void, credentials?: (string | IceServer)[]): Promise<void>;
60
+ processPacket(buffer: Buffer, rinfo: RemoteInfo): void;
61
+ setAdvertisement(buffer: Buffer): void;
62
+ handleRequest(rinfo: RemoteInfo): void;
63
+ handleMessage(packet: any, rinfo: RemoteInfo): void;
64
+ listen(): Promise<void>;
65
+ close(reason?: string): void;
66
+
67
+ on<K extends keyof ServerEvents>(event: K, listener: ServerEvents[K]): this;
68
+ emit<K extends keyof ServerEvents>(event: K, ...args: Parameters<ServerEvents[K]>): boolean;
69
+ }
70
+
71
+ export interface ClientEvents {
72
+ connected: (connection: Connection) => void;
73
+ disconnect: (connectionId: bigint, reason: string) => void;
74
+ encapsulated: (data: Buffer, connectionId: bigint) => void;
75
+ pong: (packet: any) => void;
76
+ }
77
+
78
+ export class Client extends EventEmitter {
79
+ serverNetworkId: bigint;
80
+ broadcastAddress: string;
81
+ networkId: bigint;
82
+ connectionId: bigint;
83
+ socket: Socket;
84
+ serializer: any;
85
+ deserializer: any;
86
+ responses: Map<bigint, any>;
87
+ addresses: Map<bigint, RemoteInfo>;
88
+ credentials: (string | IceServer)[];
89
+ signalHandler: (signal: SignalStructure) => void;
90
+ connection?: Connection;
91
+ rtcConnection?: PeerConnection;
92
+ pingInterval?: NodeJS.Timeout;
93
+ running: boolean;
94
+
95
+ constructor(networkId: bigint, broadcastAddress?: string);
96
+ handleCandidate(signal: SignalStructure): Promise<void>;
97
+ handleAnswer(signal: SignalStructure): Promise<void>;
98
+ createOffer(): Promise<void>;
99
+ processPacket(buffer: Buffer, rinfo: RemoteInfo): void;
100
+ handleResponse(packet: any, rinfo: RemoteInfo): void;
101
+ handleMessage(packet: any): void;
102
+ handleSignal(signal: SignalStructure): void;
103
+ sendDiscoveryRequest(): void;
104
+ sendDiscoveryMessage(signal: SignalStructure): void;
105
+ connect(): Promise<void>;
106
+ send(buffer: Buffer): void;
107
+ ping(): void;
108
+ close(reason?: string): void;
109
+
110
+ on<K extends keyof ClientEvents>(event: K, listener: ClientEvents[K]): this;
111
+ emit<K extends keyof ClientEvents>(event: K, ...args: Parameters<ClientEvents[K]>): boolean;
112
+ }
113
+
114
+ export enum SignalType {
115
+ ConnectRequest = 'CONNECTREQUEST',
116
+ ConnectResponse = 'CONNECTRESPONSE',
117
+ CandidateAdd = 'CANDIDATEADD',
118
+ ConnectError = 'CONNECTERROR'
119
+ }
120
+
121
+ export class SignalStructure {
122
+ type: SignalType;
123
+ connectionId: bigint;
124
+ data: string;
125
+ networkId?: bigint;
126
+
127
+ constructor(type: SignalType, connectionId: bigint, data: string, networkId?: bigint);
128
+ toString(): string;
129
+ static fromString(message: string): SignalStructure;
130
+ }
131
+
132
+ }
package/index.js CHANGED
@@ -1,9 +1,17 @@
1
- if (typeof process !== 'undefined' && parseInt(process.versions.node.split('.')[0]) < 18) {
2
- console.error('Your node version is currently', process.versions.node)
3
- console.error('Please update it to a version >= 18.x.x from https://nodejs.org/')
4
- process.exit(1)
1
+ const { Client } = require('./src/client')
2
+ const { Server } = require('./src/server')
3
+ const { SignalStructure } = require('./src/signalling')
4
+
5
+ const SignalType = {
6
+ ConnectRequest: 'CONNECTREQUEST',
7
+ ConnectResponse: 'CONNECTRESPONSE',
8
+ CandidateAdd: 'CANDIDATEADD',
9
+ ConnectError: 'CONNECTERROR'
5
10
  }
6
11
 
7
- module.exports.helloWorld = function () {
8
- console.log('Hello world !')
12
+ module.exports = {
13
+ Client,
14
+ Server,
15
+ SignalType,
16
+ SignalStructure
9
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nethernet",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "(WIP) Minecraft Bedrock nethernet protocol",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -24,8 +24,13 @@
24
24
  },
25
25
  "homepage": "https://github.com/PrismarineJS/node-nethernet#readme",
26
26
  "devDependencies": {
27
- "node-nethernet": "file:.",
28
- "standard": "^17.0.0",
29
- "mocha": "^10.0.0"
27
+ "debug": "^4.4.0",
28
+ "mocha": "^10.0.0",
29
+ "nethernet": "file:.",
30
+ "standard": "^17.0.0"
31
+ },
32
+ "dependencies": {
33
+ "node-datachannel": "^0.26.0",
34
+ "protodef": "^1.19.0"
30
35
  }
31
36
  }
package/src/client.js ADDED
@@ -0,0 +1,206 @@
1
+ const dgram = require('node:dgram')
2
+ const { EventEmitter } = require('node:events')
3
+
4
+ const { Connection } = require('./connection')
5
+ const { SignalType, SignalStructure } = require('./signalling')
6
+
7
+ const { getRandomUint64, createPacketData, prepareSecurePacket, processSecurePacket } = require('./util')
8
+ const { PeerConnection } = require('node-datachannel')
9
+ const { PACKET_TYPE, createSerializer, createDeserializer } = require('./serializer')
10
+
11
+ const debug = require('debug')('nethernet')
12
+
13
+ const PORT = 7551
14
+ const BROADCAST_ADDRESS = '255.255.255.255'
15
+ const SOCKET_CLOSE_TIMEOUT_MS = 100
16
+
17
+ class Client extends EventEmitter {
18
+ constructor (networkId, broadcastAddress = BROADCAST_ADDRESS) {
19
+ super()
20
+
21
+ this.serverNetworkId = networkId
22
+
23
+ this.broadcastAddress = broadcastAddress
24
+
25
+ this.networkId = getRandomUint64()
26
+
27
+ this.connectionId = getRandomUint64()
28
+
29
+ this.socket = dgram.createSocket('udp4')
30
+
31
+ this.socket.on('message', (buffer, rinfo) => {
32
+ this.processPacket(buffer, rinfo)
33
+ })
34
+
35
+ this.socket.bind(() => {
36
+ this.socket.setBroadcast(true)
37
+ })
38
+
39
+ this.serializer = createSerializer()
40
+ this.deserializer = createDeserializer()
41
+
42
+ this.responses = new Map()
43
+ this.addresses = new Map()
44
+
45
+ this.credentials = []
46
+
47
+ this.signalHandler = this.sendDiscoveryMessage
48
+
49
+ this.sendDiscoveryRequest()
50
+
51
+ this.pingInterval = setInterval(() => {
52
+ this.sendDiscoveryRequest()
53
+ }, 2000)
54
+ }
55
+
56
+ async handleCandidate (signal) {
57
+ this.rtcConnection.addRemoteCandidate(signal.data, '0')
58
+ }
59
+
60
+ async handleAnswer (signal) {
61
+ this.rtcConnection.setRemoteDescription(signal.data, 'answer')
62
+ }
63
+
64
+ async createOffer () {
65
+ this.rtcConnection = new PeerConnection('client', { iceServers: this.credentials })
66
+
67
+ this.connection = new Connection(this, this.connectionId, this.rtcConnection)
68
+
69
+ this.rtcConnection.onLocalCandidate(candidate => {
70
+ this.signalHandler(
71
+ new SignalStructure(SignalType.CandidateAdd, this.connectionId, candidate, this.serverNetworkId)
72
+ )
73
+ })
74
+
75
+ this.rtcConnection.onLocalDescription(desc => {
76
+ const pattern = /o=rtc \d+ 0 IN IP4 127\.0\.0\.1/
77
+
78
+ const newOLine = `o=- ${this.networkId} 2 IN IP4 127.0.0.1`
79
+
80
+ desc = desc.replace(pattern, newOLine)
81
+
82
+ debug('client ICE local description changed', desc)
83
+ this.signalHandler(
84
+ new SignalStructure(SignalType.ConnectRequest, this.connectionId, desc, this.serverNetworkId)
85
+ )
86
+ })
87
+
88
+ this.rtcConnection.onStateChange(state => {
89
+ debug('Client state changed', state)
90
+ if (state === 'connected') this.emit('connected', this.connection)
91
+ if (state === 'closed' || state === 'disconnected' || state === 'failed') this.emit('disconnect', this.connectionId, 'disconnected')
92
+ })
93
+
94
+ setTimeout(() => {
95
+ this.connection.setChannels(
96
+ this.rtcConnection.createDataChannel('ReliableDataChannel'),
97
+ this.rtcConnection.createDataChannel('UnreliableDataChannel')
98
+ )
99
+ }, 500)
100
+ }
101
+
102
+ processPacket (buffer, rinfo) {
103
+ const parsedPacket = processSecurePacket(buffer, this.deserializer)
104
+ debug('Received packet', parsedPacket)
105
+
106
+ switch (parsedPacket.name) {
107
+ case 'discovery_request':
108
+ break
109
+ case 'discovery_response':
110
+ this.handleResponse(parsedPacket, rinfo)
111
+ break
112
+ case 'discovery_message':
113
+ this.handleMessage(parsedPacket)
114
+ break
115
+ default:
116
+ throw new Error('Unknown packet type')
117
+ }
118
+ }
119
+
120
+ handleResponse (packet, rinfo) {
121
+ const senderId = BigInt(packet.params.sender_id)
122
+ this.addresses.set(senderId, rinfo)
123
+ this.responses.set(senderId, packet.params)
124
+ this.emit('pong', packet.params)
125
+ }
126
+
127
+ handleMessage (packet) {
128
+ const data = packet.params.data
129
+
130
+ if (data === 'Ping') {
131
+ return
132
+ }
133
+
134
+ const signal = SignalStructure.fromString(data)
135
+
136
+ signal.networkId = packet.params.sender_id
137
+
138
+ this.handleSignal(signal)
139
+ }
140
+
141
+ handleSignal (signal) {
142
+ switch (signal.type) {
143
+ case SignalType.ConnectResponse:
144
+ this.handleAnswer(signal)
145
+ break
146
+ case SignalType.CandidateAdd:
147
+ this.handleCandidate(signal)
148
+ break
149
+ }
150
+ }
151
+
152
+ sendDiscoveryRequest () {
153
+ const packetData = createPacketData('discovery_request', PACKET_TYPE.DISCOVERY_REQUEST, this.networkId)
154
+
155
+ const packetToSend = prepareSecurePacket(this.serializer, packetData)
156
+
157
+ this.socket.send(packetToSend, PORT, this.broadcastAddress)
158
+ }
159
+
160
+ sendDiscoveryMessage (signal) {
161
+ const rinfo = this.addresses.get(BigInt(signal.networkId))
162
+
163
+ if (!rinfo) {
164
+ return
165
+ }
166
+
167
+ const packetData = createPacketData('discovery_message', PACKET_TYPE.DISCOVERY_MESSAGE, this.networkId,
168
+ {
169
+ recipient_id: BigInt(signal.networkId),
170
+ data: signal.toString()
171
+ }
172
+ )
173
+
174
+ const packetToSend = prepareSecurePacket(this.serializer, packetData)
175
+ this.socket.send(packetToSend, rinfo.port, rinfo.address)
176
+ }
177
+
178
+ async connect () {
179
+ this.running = true
180
+
181
+ await this.createOffer()
182
+ }
183
+
184
+ send (buffer) {
185
+ this.connection.send(buffer)
186
+ }
187
+
188
+ ping () {
189
+ this.running = true
190
+
191
+ this.sendDiscoveryRequest()
192
+ }
193
+
194
+ close (reason) {
195
+ debug('Closing client', reason)
196
+ if (!this.running) return
197
+ clearInterval(this.pingInterval)
198
+ this.connection?.close()
199
+ setTimeout(() => this.socket.close(), SOCKET_CLOSE_TIMEOUT_MS)
200
+ this.connection = null
201
+ this.running = false
202
+ this.removeAllListeners()
203
+ }
204
+ }
205
+
206
+ module.exports = { Client }
@@ -0,0 +1,32 @@
1
+ /* eslint-disable */
2
+ const [Read, Write, SizeOf] = [{}, {}, {}]
3
+
4
+ /**
5
+ * Encapsulated data with length prefix
6
+ */
7
+ Read.encapsulated = ['parametrizable', (compiler, { lengthType, type }) => {
8
+ return compiler.wrapCode(`
9
+ const payloadSize = ${compiler.callType(lengthType, 'offset')}
10
+ const { value, size } = ctx.${type}(buffer, offset + payloadSize.size)
11
+ return { value, size: size + payloadSize.size }
12
+ `.trim())
13
+ }]
14
+ Write.encapsulated = ['parametrizable', (compiler, { lengthType, type }) => {
15
+ return compiler.wrapCode(`
16
+ const buf = Buffer.allocUnsafe(buffer.length - offset)
17
+ const payloadSize = (ctx.${type})(value, buf, 0)
18
+ let size = (ctx.${lengthType})(payloadSize, buffer, offset)
19
+ size += buf.copy(buffer, size, 0, payloadSize)
20
+
21
+ return size
22
+ `.trim())
23
+ }]
24
+ SizeOf.encapsulated = ['parametrizable', (compiler, { lengthType, type }) => {
25
+ return compiler.wrapCode(`
26
+ const payloadSize = (ctx.${type})(value)
27
+ return (ctx.${lengthType})(payloadSize) + payloadSize
28
+ `.trim())
29
+ }]
30
+
31
+
32
+ module.exports = { Read, Write, SizeOf }
@@ -0,0 +1,121 @@
1
+ const debug = require('debug')('nethernet')
2
+
3
+ const MAX_MESSAGE_SIZE = 10_000
4
+
5
+ class Connection {
6
+ constructor (nethernet, address, rtcConnection) {
7
+ this.nethernet = nethernet
8
+ this.address = address
9
+ this.rtcConnection = rtcConnection
10
+ this.reliable = null
11
+ this.unreliable = null
12
+ this.promisedSegments = 0
13
+ this.buf = Buffer.alloc(0)
14
+ this.sendQueue = []
15
+ }
16
+
17
+ setChannels (reliable, unreliable) {
18
+ if (reliable) {
19
+ this.reliable = reliable
20
+ this.reliable.onMessage((msg) => {
21
+ this.handleMessage(msg)
22
+ })
23
+ this.reliable.onOpen(() => {
24
+ this.flushQueue()
25
+ })
26
+ }
27
+ if (unreliable) {
28
+ this.unreliable = unreliable
29
+ }
30
+ }
31
+
32
+ handleMessage (data) {
33
+ if (typeof data === 'string' || data instanceof ArrayBuffer) {
34
+ data = Buffer.from(data)
35
+ }
36
+
37
+ if (data.length < 2) {
38
+ throw new Error('Unexpected EOF')
39
+ }
40
+
41
+ const segments = data[0]
42
+ debug(`handleMessage segments: ${segments}`)
43
+ data = data.subarray(1)
44
+
45
+ if (this.promisedSegments > 0 && this.promisedSegments - 1 !== segments) {
46
+ throw new Error(`Invalid promised segments: expected ${this.promisedSegments - 1}, got ${segments}`)
47
+ }
48
+
49
+ this.promisedSegments = segments
50
+ this.buf = this.buf ? Buffer.concat([this.buf, data]) : data
51
+
52
+ if (this.promisedSegments > 0) {
53
+ return
54
+ }
55
+
56
+ this.nethernet.emit('encapsulated', this.buf, this.address)
57
+ this.buf = null
58
+ }
59
+
60
+ send (data) {
61
+ if (typeof data === 'string') {
62
+ data = Buffer.from(data)
63
+ }
64
+
65
+ if (!this.reliable || this.reliable.readyState === 'connecting') {
66
+ debug('Reliable channel not open, queuing message')
67
+ this.sendQueue.push(data)
68
+ return 0
69
+ }
70
+
71
+ if (this.reliable.readyState === 'closed' || this.reliable.readyState === 'closing') {
72
+ debug('Reliable channel is not open', this.reliable?.readyState)
73
+ throw new Error('Reliable channel is not open')
74
+ }
75
+
76
+ return this.sendNow(data)
77
+ }
78
+
79
+ sendNow (data) {
80
+ let n = 0
81
+ let segments = Math.ceil(data.length / MAX_MESSAGE_SIZE)
82
+
83
+ for (let i = 0; i < data.length; i += MAX_MESSAGE_SIZE) {
84
+ segments--
85
+ const end = Math.min(i + MAX_MESSAGE_SIZE, data.length)
86
+ const frag = data.subarray(i, end)
87
+ const message = Buffer.concat([Buffer.from([segments]), frag])
88
+ debug('Sending fragment', segments)
89
+ this.reliable.sendMessageBinary(message)
90
+ n += frag.length
91
+ }
92
+
93
+ if (segments !== 0) {
94
+ throw new Error('Segments count did not reach 0 after sending all fragments')
95
+ }
96
+
97
+ return n
98
+ }
99
+
100
+ flushQueue () {
101
+ debug('Flushing send queue')
102
+ while (this.sendQueue.length > 0) {
103
+ const data = this.sendQueue.shift()
104
+ this.sendNow(data)
105
+ }
106
+ }
107
+
108
+ close () {
109
+ if (this.reliable) {
110
+ this.reliable.close()
111
+ }
112
+ if (this.unreliable) {
113
+ this.unreliable.close()
114
+ }
115
+ if (this.rtcConnection) {
116
+ this.rtcConnection.close()
117
+ }
118
+ }
119
+ }
120
+
121
+ module.exports = { Connection }
package/src/crypto.js ADDED
@@ -0,0 +1,30 @@
1
+ const crypto = require('node:crypto')
2
+
3
+ const appIdBuffer = Buffer.alloc(8)
4
+ appIdBuffer.writeBigUInt64LE(BigInt(0xdeadbeef))
5
+
6
+ const AES_KEY = crypto.createHash('sha256')
7
+ .update(appIdBuffer)
8
+ .digest()
9
+
10
+ function encrypt (data) {
11
+ const cipher = crypto.createCipheriv('aes-256-ecb', AES_KEY, null)
12
+ return Buffer.concat([cipher.update(data), cipher.final()])
13
+ }
14
+
15
+ function decrypt (data) {
16
+ const decipher = crypto.createDecipheriv('aes-256-ecb', AES_KEY, null)
17
+ return Buffer.concat([decipher.update(data), decipher.final()])
18
+ }
19
+
20
+ function calculateChecksum (data) {
21
+ const hmac = crypto.createHmac('sha256', AES_KEY)
22
+ hmac.update(data)
23
+ return hmac.digest()
24
+ }
25
+
26
+ module.exports = {
27
+ encrypt,
28
+ decrypt,
29
+ calculateChecksum
30
+ }
@@ -0,0 +1,129 @@
1
+ {
2
+ "types": {
3
+ "string": [
4
+ "pstring",
5
+ {
6
+ "countType": "varint"
7
+ }
8
+ ],
9
+ "encapsulated": "native",
10
+ "nethernet_packet": [
11
+ "encapsulated",
12
+ {
13
+ "lengthType": "lu16",
14
+ "type": "nethernet_packet_inner"
15
+ }
16
+ ],
17
+ "nethernet_packet_inner": [
18
+ "container",
19
+ [
20
+ {
21
+ "name": "name",
22
+ "type": [
23
+ "mapper",
24
+ {
25
+ "type": "lu16",
26
+ "mappings": {
27
+ "0": "discovery_request",
28
+ "1": "discovery_response",
29
+ "2": "discovery_message"
30
+ }
31
+ }
32
+ ]
33
+ },
34
+ {
35
+ "name": "params",
36
+ "type": [
37
+ "switch",
38
+ {
39
+ "compareTo": "name",
40
+ "fields": {
41
+ "discovery_request": "DiscoveryRequest",
42
+ "discovery_response": "DiscoveryResponse",
43
+ "discovery_message": "DiscoveryMessage"
44
+ }
45
+ }
46
+ ]
47
+ }
48
+ ]
49
+ ],
50
+ "DiscoveryRequest": [
51
+ "container",
52
+ [
53
+ {
54
+ "name": "sender_id",
55
+ "type": "lu64"
56
+ },
57
+ {
58
+ "name": "reserved",
59
+ "type": [
60
+ "buffer",
61
+ {
62
+ "count": 8
63
+ }
64
+ ]
65
+ }
66
+ ]
67
+ ],
68
+ "DiscoveryResponse": [
69
+ "container",
70
+ [
71
+ {
72
+ "name": "sender_id",
73
+ "type": "lu64"
74
+ },
75
+ {
76
+ "name": "reserved",
77
+ "type": [
78
+ "buffer",
79
+ {
80
+ "count": 8
81
+ }
82
+ ]
83
+ },
84
+ {
85
+ "name": "data",
86
+ "type": [
87
+ "pstring",
88
+ {
89
+ "countType": "lu32",
90
+ "encoding": "utf-8"
91
+ }
92
+ ]
93
+ }
94
+ ]
95
+ ],
96
+ "DiscoveryMessage": [
97
+ "container",
98
+ [
99
+ {
100
+ "name": "sender_id",
101
+ "type": "lu64"
102
+ },
103
+ {
104
+ "name": "reserved",
105
+ "type": [
106
+ "buffer",
107
+ {
108
+ "count": 8
109
+ }
110
+ ]
111
+ },
112
+ {
113
+ "name": "recipient_id",
114
+ "type": "lu64"
115
+ },
116
+ {
117
+ "name": "data",
118
+ "type": [
119
+ "pstring",
120
+ {
121
+ "countType": "lu32",
122
+ "encoding": "utf-8"
123
+ }
124
+ ]
125
+ }
126
+ ]
127
+ ]
128
+ }
129
+ }
@@ -0,0 +1,35 @@
1
+ const { ProtoDefCompiler } = require('protodef').Compiler
2
+ const { FullPacketParser, Serializer } = require('protodef')
3
+ const protocol = require('./protocol.json')
4
+
5
+ const PACKET_TYPE = {
6
+ DISCOVERY_REQUEST: 0,
7
+ DISCOVERY_RESPONSE: 1,
8
+ DISCOVERY_MESSAGE: 2
9
+ }
10
+
11
+ function createProtocol () {
12
+ const compiler = new ProtoDefCompiler()
13
+ compiler.addTypesToCompile(protocol.types)
14
+ compiler.addTypes(require('./compilerTypes'))
15
+
16
+ const compiledProto = compiler.compileProtoDefSync()
17
+ return compiledProto
18
+ }
19
+
20
+ function createSerializer () {
21
+ const proto = createProtocol()
22
+ return new Serializer(proto, 'nethernet_packet')
23
+ }
24
+
25
+ function createDeserializer () {
26
+ const proto = createProtocol()
27
+ return new FullPacketParser(proto, 'nethernet_packet')
28
+ }
29
+
30
+ module.exports = {
31
+ PACKET_TYPE,
32
+ createDeserializer,
33
+ createSerializer,
34
+ createProtocol
35
+ }
package/src/server.js ADDED
@@ -0,0 +1,176 @@
1
+ const dgram = require('node:dgram')
2
+ const { EventEmitter } = require('node:events')
3
+ const { PeerConnection } = require('node-datachannel')
4
+
5
+ const { Connection } = require('./connection')
6
+ const { SignalStructure, SignalType } = require('./signalling')
7
+
8
+ const { PACKET_TYPE, createSerializer, createDeserializer } = require('./serializer')
9
+
10
+ const { getRandomUint64, createPacketData, prepareSecurePacket, processSecurePacket } = require('./util')
11
+
12
+ const debug = require('debug')('nethernet')
13
+
14
+ class Server extends EventEmitter {
15
+ constructor (options = {}) {
16
+ super()
17
+
18
+ this.options = options
19
+
20
+ this.networkId = options.networkId ?? getRandomUint64()
21
+
22
+ this.connections = new Map()
23
+
24
+ this.serializer = createSerializer()
25
+ this.deserializer = createDeserializer()
26
+ }
27
+
28
+ async handleCandidate (signal) {
29
+ const conn = this.connections.get(signal.connectionId)
30
+
31
+ if (conn) {
32
+ conn.rtcConnection.addRemoteCandidate(signal.data, '0')
33
+ } else {
34
+ debug('Connection not found', signal.connectionId)
35
+ }
36
+ }
37
+
38
+ async handleOffer (signal, respond, credentials = []) {
39
+ const rtcConnection = new PeerConnection('server', { iceServers: credentials })
40
+
41
+ const connection = new Connection(this, signal.connectionId, rtcConnection)
42
+
43
+ this.connections.set(signal.connectionId, connection)
44
+
45
+ debug('Received offer', signal.connectionId)
46
+
47
+ rtcConnection.onLocalDescription(description => {
48
+ debug('Local description', description)
49
+ respond(
50
+ new SignalStructure(SignalType.ConnectResponse, signal.connectionId, description, signal.networkId)
51
+ )
52
+ })
53
+
54
+ rtcConnection.onLocalCandidate(candidate => {
55
+ respond(
56
+ new SignalStructure(SignalType.CandidateAdd, signal.connectionId, candidate, signal.networkId)
57
+ )
58
+ })
59
+
60
+ rtcConnection.onDataChannel(channel => {
61
+ debug('Received data channel', channel.getLabel())
62
+ if (channel.getLabel() === 'ReliableDataChannel') connection.setChannels(channel)
63
+ if (channel.getLabel() === 'UnreliableDataChannel') connection.setChannels(null, channel)
64
+ })
65
+
66
+ rtcConnection.onStateChange(state => {
67
+ debug('Server RTC state changed', state)
68
+ if (state === 'connected') this.emit('openConnection', connection)
69
+ if (state === 'closed' || state === 'disconnected' || state === 'failed') this.emit('closeConnection', signal.connectionId, 'disconnected')
70
+ })
71
+
72
+ rtcConnection.setRemoteDescription(signal.data, 'offer')
73
+ }
74
+
75
+ processPacket (buffer, rinfo) {
76
+ const parsedPacket = processSecurePacket(buffer, this.deserializer)
77
+ debug('Received packet', parsedPacket)
78
+
79
+ switch (parsedPacket.name) {
80
+ case 'discovery_request':
81
+ this.handleRequest(rinfo)
82
+ break
83
+ case 'discovery_response':
84
+ break
85
+ case 'discovery_message':
86
+ this.handleMessage(parsedPacket, rinfo)
87
+ break
88
+ default:
89
+ throw new Error('Unknown packet type')
90
+ }
91
+ }
92
+
93
+ setAdvertisement (buffer) {
94
+ this.advertisement = buffer
95
+ }
96
+
97
+ handleRequest (rinfo) {
98
+ const data = this.advertisement
99
+
100
+ if (!data) {
101
+ throw new Error('Advertisement data not set yet')
102
+ }
103
+
104
+ const packetData = createPacketData('discovery_response', PACKET_TYPE.DISCOVERY_RESPONSE, this.networkId,
105
+ {
106
+ data: data.toString('hex')
107
+ }
108
+ )
109
+
110
+ const packetToSend = prepareSecurePacket(this.serializer, packetData)
111
+ this.socket.send(packetToSend, rinfo.port, rinfo.address)
112
+ }
113
+
114
+ handleMessage (packet, rinfo) {
115
+ const data = packet.params.data
116
+ if (data === 'Ping') {
117
+ return
118
+ }
119
+
120
+ const respond = (signal) => {
121
+ const packetData = createPacketData('discovery_message', PACKET_TYPE.DISCOVERY_MESSAGE, this.networkId,
122
+ {
123
+ recipient_id: BigInt(signal.networkId),
124
+ data: signal.toString()
125
+ }
126
+ )
127
+
128
+ const packetToSend = prepareSecurePacket(this.serializer, packetData)
129
+ this.socket.send(packetToSend, rinfo.port, rinfo.address)
130
+ }
131
+
132
+ const signal = SignalStructure.fromString(data)
133
+
134
+ signal.networkId = packet.params.sender_id
135
+
136
+ switch (signal.type) {
137
+ case SignalType.ConnectRequest:
138
+ this.handleOffer(signal, respond)
139
+ break
140
+ case SignalType.CandidateAdd:
141
+ this.handleCandidate(signal)
142
+ break
143
+ }
144
+ }
145
+
146
+ async listen () {
147
+ this.socket = dgram.createSocket('udp4')
148
+
149
+ this.socket.on('message', (buffer, rinfo) => {
150
+ this.processPacket(buffer, rinfo)
151
+ })
152
+
153
+ await new Promise((resolve, reject) => {
154
+ const failFn = e => reject(e)
155
+ this.socket.once('error', failFn)
156
+ this.socket.bind(7551, () => {
157
+ this.socket.removeListener('error', failFn)
158
+ resolve(true)
159
+ })
160
+ })
161
+ }
162
+
163
+ close (reason) {
164
+ debug('Closing server', reason)
165
+ for (const conn of this.connections.values()) {
166
+ conn.close()
167
+ }
168
+
169
+ this.socket.close(() => {
170
+ this.emit('close', reason)
171
+ this.removeAllListeners()
172
+ })
173
+ }
174
+ }
175
+
176
+ module.exports = { Server }
@@ -0,0 +1,27 @@
1
+ const SignalType = {
2
+ ConnectRequest: 'CONNECTREQUEST',
3
+ ConnectResponse: 'CONNECTRESPONSE',
4
+ CandidateAdd: 'CANDIDATEADD',
5
+ ConnectError: 'CONNECTERROR'
6
+ }
7
+
8
+ class SignalStructure {
9
+ constructor (type, connectionId, data, networkId) {
10
+ this.type = type
11
+ this.connectionId = connectionId
12
+ this.data = data
13
+ this.networkId = networkId
14
+ }
15
+
16
+ toString () {
17
+ return `${this.type} ${this.connectionId} ${this.data}`
18
+ }
19
+
20
+ static fromString (message) {
21
+ const [type, connectionId, ...data] = message.split(' ')
22
+
23
+ return new this(type, BigInt(connectionId), data.join(' '))
24
+ }
25
+ }
26
+
27
+ module.exports = { SignalStructure, SignalType }
package/src/util.js ADDED
@@ -0,0 +1,52 @@
1
+ const { encrypt, calculateChecksum, decrypt } = require('./crypto')
2
+
3
+ const getRandomUint64 = () => {
4
+ const high = Math.floor(Math.random() * 0xFFFFFFFF)
5
+ const low = Math.floor(Math.random() * 0xFFFFFFFF)
6
+
7
+ return (BigInt(high) << 32n) | BigInt(low)
8
+ }
9
+
10
+ const createPacketData = (packetName, packetId, senderId, additionalParams = {}) => {
11
+ return {
12
+ name: packetName,
13
+ params: {
14
+ sender_id: senderId,
15
+ reserved: Buffer.alloc(8),
16
+ ...additionalParams
17
+ }
18
+ }
19
+ }
20
+
21
+ const prepareSecurePacket = (serializer, packetData) => {
22
+ const buf = serializer.createPacketBuffer(packetData)
23
+
24
+ const checksum = calculateChecksum(buf)
25
+ const encryptedData = encrypt(buf)
26
+
27
+ return Buffer.concat([checksum, encryptedData])
28
+ }
29
+
30
+ const processSecurePacket = (buffer, deserializer) => {
31
+ if (buffer.length < 32) {
32
+ throw new Error('Packet is too short')
33
+ }
34
+
35
+ const decryptedData = decrypt(buffer.slice(32))
36
+
37
+ const checksum = calculateChecksum(decryptedData)
38
+ if (Buffer.compare(buffer.slice(0, 32), checksum) !== 0) {
39
+ throw new Error('Checksum mismatch')
40
+ }
41
+
42
+ const packet = deserializer.parsePacketBuffer(decryptedData)
43
+
44
+ return { name: packet.data.name, params: packet.data.params }
45
+ }
46
+
47
+ module.exports = {
48
+ getRandomUint64,
49
+ createPacketData,
50
+ prepareSecurePacket,
51
+ processSecurePacket
52
+ }
@@ -1,7 +1,106 @@
1
1
  /* eslint-env mocha */
2
+ process.env.DEBUG = '*'
3
+ const { Server, Client } = require('nethernet')
2
4
 
3
- describe('basic', () => {
4
- it('test', () => {
5
+ async function pingTest () {
6
+ return new Promise((resolve, reject) => {
7
+ const message = 'FMCPE;JSRakNet - JS powered RakNet;408;1.16.20;0;5;0;JSRakNet;Creative;'
8
+ const server = new Server()
9
+ server.setAdvertisement(Buffer.from(message))
10
+ const client = new Client(server.networkId, '127.0.0.1')
11
+ client.once('pong', (packet) => {
12
+ console.log('PONG data', packet)
13
+ const msg = Buffer.from(packet.data, 'hex').toString()
14
+ if (!msg || msg !== message) throw Error(`PONG mismatch ${msg} != ${message}`)
15
+ console.log('OK')
16
+ client.close()
17
+ server.close()
18
+ setTimeout(() => {
19
+ resolve() // allow for server + client to close
20
+ }, 500)
21
+ })
5
22
 
23
+ server.listen()
24
+ client.ping()
6
25
  })
26
+ }
27
+
28
+ async function connectTest () {
29
+ return new Promise((resolve, reject) => {
30
+ const message = 'FMCPE;JSRakNet - JS powered RakNet;408;1.16.20;0;5;0;JSRakNet;Creative;'
31
+ const server = new Server()
32
+ server.setAdvertisement(Buffer.from(message))
33
+ const client = new Client(server.networkId, '127.0.0.1')
34
+
35
+ server.listen()
36
+ let lastC = 0
37
+ client.on('connected', () => {
38
+ console.log('connected!')
39
+ client.on('encapsulated', (encap) => {
40
+ console.assert(encap[0] === 0xf0)
41
+ const ix = encap[1]
42
+ if (lastC++ !== ix) {
43
+ throw Error(`Packet mismatch: ${lastC - 1} != ${ix}`)
44
+ }
45
+ client.send(encap)
46
+ })
47
+ })
48
+ let lastS = 0
49
+ server.on('encapsulated', (encap) => {
50
+ console.assert(encap[0] === 0xf0)
51
+ const ix = encap[1]
52
+ if (lastS++ !== ix) {
53
+ throw Error(`Packet mismatch: ${lastS - 1} != ${ix}`)
54
+ }
55
+ if (lastS === 50) {
56
+ client.close()
57
+ server.close()
58
+ resolve(true)
59
+ }
60
+ })
61
+ server.on('openConnection', (client) => {
62
+ console.debug('Client opened connection')
63
+ for (let i = 0; i < 50; i++) {
64
+ const buf = Buffer.alloc(1000)
65
+ for (let j = 0; j < 64; j += 4) buf[j] = j + i
66
+ buf[0] = 0xf0
67
+ buf[1] = i
68
+ client.send(buf)
69
+ }
70
+ })
71
+ client.connect()
72
+ })
73
+ }
74
+
75
+ async function kickTest () {
76
+ return new Promise((resolve, reject) => {
77
+ const server = new Server()
78
+ server.setAdvertisement(Buffer.from([0]))
79
+ const client = new Client(server.networkId, '127.0.0.1')
80
+ server.on('openConnection', (con) => {
81
+ console.log('new connection')
82
+ con.close()
83
+ })
84
+ server.listen()
85
+ client.on('disconnect', packet => {
86
+ console.log('Client got disconnect', packet)
87
+ try {
88
+ client.send(Buffer.from('\xf0 yello'))
89
+ } catch (e) {
90
+ console.log('** Expected error 😀 **', e)
91
+ server.close()
92
+ client.close()
93
+ resolve()
94
+ }
95
+ })
96
+
97
+ client.connect()
98
+ })
99
+ }
100
+
101
+ describe('server tests', function () {
102
+ this.timeout(30000)
103
+ it('ping test', pingTest)
104
+ it('connection test', connectTest)
105
+ it('kick test', kickTest)
7
106
  })