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.
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish.yml +7 -5
- package/HISTORY.md +9 -0
- package/README.md +25 -10
- package/example.js +17 -2
- package/index.d.ts +132 -0
- package/index.js +14 -6
- package/package.json +9 -4
- package/src/client.js +206 -0
- package/src/compilerTypes.js +32 -0
- package/src/connection.js +121 -0
- package/src/crypto.js +30 -0
- package/src/protocol.json +129 -0
- package/src/serializer.js +35 -0
- package/src/server.js +176 -0
- package/src/signalling.js +27 -0
- package/src/util.js +52 -0
- package/test/basic.test.js +101 -2
package/.github/workflows/ci.yml
CHANGED
|
@@ -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:
|
|
19
|
+
node-version: 24
|
|
20
|
+
registry-url: 'https://registry.npmjs.org'
|
|
17
21
|
- id: publish
|
|
18
|
-
uses: JS-DevTools/npm-publish@
|
|
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
|
|
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
|
[](https://discord.gg/GsEFRM8)
|
|
8
8
|
|
|
9
|
-
WIP
|
|
10
9
|
|
|
11
|
-
A
|
|
10
|
+
A Node.JS implementation of the NetherNet protocol.
|
|
12
11
|
|
|
13
|
-
##
|
|
12
|
+
## Install
|
|
14
13
|
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
template.helloWorld()
|
|
14
|
+
```sh
|
|
15
|
+
npm install nethernet
|
|
19
16
|
```
|
|
20
17
|
|
|
21
|
-
##
|
|
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
|
-
|
|
32
|
+
server.on('openConnection', (client) => {
|
|
33
|
+
client.send(Buffer.from('\xA0 Hello world'))
|
|
34
|
+
})
|
|
24
35
|
|
|
25
|
-
|
|
36
|
+
server.listen()
|
|
37
|
+
|
|
38
|
+
client.connect()
|
|
39
|
+
|
|
40
|
+
```
|
package/example.js
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { Client, Server } = require('nethernet')
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
8
|
-
|
|
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
|
|
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
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
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
|
+
}
|
package/test/basic.test.js
CHANGED
|
@@ -1,7 +1,106 @@
|
|
|
1
1
|
/* eslint-env mocha */
|
|
2
|
+
process.env.DEBUG = '*'
|
|
3
|
+
const { Server, Client } = require('nethernet')
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
})
|