raknet-typeo 1.0.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/package.json +36 -0
- package/src/client.js +209 -0
- package/src/compat/bedrock-protocol.js +147 -0
- package/src/compat/index.js +10 -0
- package/src/compat/register.js +62 -0
- package/src/connection.js +394 -0
- package/src/constants.js +87 -0
- package/src/index.js +52 -0
- package/src/protocol/ack.js +47 -0
- package/src/protocol/frame.js +100 -0
- package/src/protocol/offline.js +151 -0
- package/src/server.js +161 -0
- package/src/transport/udp.js +38 -0
- package/src/transport/websocket.js +138 -0
- package/src/utils/binary-stream.js +184 -0
- package/src/ws-client.js +12 -0
- package/src/ws-server.js +13 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { BinaryStream } from "../utils/binary-stream.js";
|
|
2
|
+
import { MessageId, OFFLINE_MESSAGE_DATA_ID, RAKNET_PROTOCOL_VERSION } from "../constants.js";
|
|
3
|
+
|
|
4
|
+
export function isOfflineMessage(buf) {
|
|
5
|
+
if (buf.length < 1) return false;
|
|
6
|
+
const id = buf[0];
|
|
7
|
+
const offlineIds = [
|
|
8
|
+
MessageId.UnconnectedPing,
|
|
9
|
+
MessageId.UnconnectedPingOpenConn,
|
|
10
|
+
MessageId.UnconnectedPong,
|
|
11
|
+
MessageId.OpenConnectionRequest1,
|
|
12
|
+
MessageId.OpenConnectionReply1,
|
|
13
|
+
MessageId.OpenConnectionRequest2,
|
|
14
|
+
MessageId.OpenConnectionReply2,
|
|
15
|
+
MessageId.IncompatibleProtocolVersion,
|
|
16
|
+
MessageId.AlreadyConnected,
|
|
17
|
+
MessageId.NoFreeIncomingConnections,
|
|
18
|
+
MessageId.ConnectionBanned,
|
|
19
|
+
MessageId.IpRecentlyConnected,
|
|
20
|
+
];
|
|
21
|
+
return offlineIds.includes(id);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function hasOfflineDataId(buf, offset) {
|
|
25
|
+
if (buf.length < offset + 16) return false;
|
|
26
|
+
return OFFLINE_MESSAGE_DATA_ID.equals(buf.slice(offset, offset + 16));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readUnconnectedPing(buf) {
|
|
30
|
+
const s = new BinaryStream(buf);
|
|
31
|
+
s.skip(1);
|
|
32
|
+
const sendTimestamp = s.readInt64BE();
|
|
33
|
+
if (!hasOfflineDataId(buf, s.offset)) return null;
|
|
34
|
+
s.skip(16);
|
|
35
|
+
const clientGuid = s.readInt64BE();
|
|
36
|
+
return { sendTimestamp, clientGuid };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function writeUnconnectedPong(sendTimestamp, serverGuid, motd) {
|
|
40
|
+
const s = new BinaryStream();
|
|
41
|
+
s.writeByte(MessageId.UnconnectedPong);
|
|
42
|
+
s.writeInt64BE(sendTimestamp);
|
|
43
|
+
s.writeInt64BE(serverGuid);
|
|
44
|
+
s.writeBytes(OFFLINE_MESSAGE_DATA_ID);
|
|
45
|
+
s.writeString(motd);
|
|
46
|
+
return s.flush();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function writeUnconnectedPing(sendTimestamp, clientGuid, openConnections = false) {
|
|
50
|
+
const s = new BinaryStream();
|
|
51
|
+
s.writeByte(openConnections ? MessageId.UnconnectedPingOpenConn : MessageId.UnconnectedPing);
|
|
52
|
+
s.writeInt64BE(sendTimestamp);
|
|
53
|
+
s.writeBytes(OFFLINE_MESSAGE_DATA_ID);
|
|
54
|
+
s.writeInt64BE(clientGuid);
|
|
55
|
+
return s.flush();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function readOpenConnectionRequest1(buf) {
|
|
59
|
+
const s = new BinaryStream(buf);
|
|
60
|
+
s.skip(1);
|
|
61
|
+
if (!hasOfflineDataId(buf, s.offset)) return null;
|
|
62
|
+
s.skip(16);
|
|
63
|
+
const protocol = s.readByte();
|
|
64
|
+
const mtu = buf.length + 28;
|
|
65
|
+
return { protocol, mtu };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function writeOpenConnectionReply1(serverGuid, mtu) {
|
|
69
|
+
const s = new BinaryStream();
|
|
70
|
+
s.writeByte(MessageId.OpenConnectionReply1);
|
|
71
|
+
s.writeBytes(OFFLINE_MESSAGE_DATA_ID);
|
|
72
|
+
s.writeInt64BE(serverGuid);
|
|
73
|
+
s.writeBool(false);
|
|
74
|
+
s.writeUInt16BE(mtu);
|
|
75
|
+
return s.flush();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function readOpenConnectionRequest2(buf) {
|
|
79
|
+
const s = new BinaryStream(buf);
|
|
80
|
+
s.skip(1);
|
|
81
|
+
if (!hasOfflineDataId(buf, s.offset)) return null;
|
|
82
|
+
s.skip(16);
|
|
83
|
+
const addr = s.readAddress();
|
|
84
|
+
const mtu = s.readUInt16BE();
|
|
85
|
+
const clientGuid = s.readInt64BE();
|
|
86
|
+
return { serverAddress: addr.address, serverPort: addr.port, mtu, clientGuid };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function writeOpenConnectionReply2(serverGuid, clientAddress, clientPort, mtu) {
|
|
90
|
+
const s = new BinaryStream();
|
|
91
|
+
s.writeByte(MessageId.OpenConnectionReply2);
|
|
92
|
+
s.writeBytes(OFFLINE_MESSAGE_DATA_ID);
|
|
93
|
+
s.writeInt64BE(serverGuid);
|
|
94
|
+
s.writeAddress(clientAddress, clientPort, 4);
|
|
95
|
+
s.writeUInt16BE(mtu);
|
|
96
|
+
s.writeBool(false);
|
|
97
|
+
return s.flush();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function writeIncompatibleProtocolVersion(serverGuid) {
|
|
101
|
+
const s = new BinaryStream();
|
|
102
|
+
s.writeByte(MessageId.IncompatibleProtocolVersion);
|
|
103
|
+
s.writeByte(RAKNET_PROTOCOL_VERSION);
|
|
104
|
+
s.writeBytes(OFFLINE_MESSAGE_DATA_ID);
|
|
105
|
+
s.writeInt64BE(serverGuid);
|
|
106
|
+
return s.flush();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function writeNoFreeIncomingConnections(serverGuid) {
|
|
110
|
+
const s = new BinaryStream();
|
|
111
|
+
s.writeByte(MessageId.NoFreeIncomingConnections);
|
|
112
|
+
s.writeBytes(OFFLINE_MESSAGE_DATA_ID);
|
|
113
|
+
s.writeInt64BE(serverGuid);
|
|
114
|
+
return s.flush();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function writeAlreadyConnected(serverGuid) {
|
|
118
|
+
const s = new BinaryStream();
|
|
119
|
+
s.writeByte(MessageId.AlreadyConnected);
|
|
120
|
+
s.writeBytes(OFFLINE_MESSAGE_DATA_ID);
|
|
121
|
+
s.writeInt64BE(serverGuid);
|
|
122
|
+
return s.flush();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function writeConnectionBanned(serverGuid) {
|
|
126
|
+
const s = new BinaryStream();
|
|
127
|
+
s.writeByte(MessageId.ConnectionBanned);
|
|
128
|
+
s.writeBytes(OFFLINE_MESSAGE_DATA_ID);
|
|
129
|
+
s.writeInt64BE(serverGuid);
|
|
130
|
+
return s.flush();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function writeOpenConnectionRequest1(mtu) {
|
|
134
|
+
const s = new BinaryStream();
|
|
135
|
+
s.writeByte(MessageId.OpenConnectionRequest1);
|
|
136
|
+
s.writeBytes(OFFLINE_MESSAGE_DATA_ID);
|
|
137
|
+
s.writeByte(RAKNET_PROTOCOL_VERSION);
|
|
138
|
+
const padding = mtu - 28 - 1 - 16 - 1;
|
|
139
|
+
if (padding > 0) s.writeBytes(Buffer.alloc(padding, 0));
|
|
140
|
+
return s.flush();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function writeOpenConnectionRequest2(serverAddress, serverPort, mtu, clientGuid) {
|
|
144
|
+
const s = new BinaryStream();
|
|
145
|
+
s.writeByte(MessageId.OpenConnectionRequest2);
|
|
146
|
+
s.writeBytes(OFFLINE_MESSAGE_DATA_ID);
|
|
147
|
+
s.writeAddress(serverAddress, serverPort, 4);
|
|
148
|
+
s.writeUInt16BE(mtu);
|
|
149
|
+
s.writeInt64BE(clientGuid);
|
|
150
|
+
return s.flush();
|
|
151
|
+
}
|
package/src/server.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import { Connection } from "./connection.js";
|
|
3
|
+
import {
|
|
4
|
+
isOfflineMessage,
|
|
5
|
+
readUnconnectedPing,
|
|
6
|
+
writeUnconnectedPong,
|
|
7
|
+
readOpenConnectionRequest1,
|
|
8
|
+
writeOpenConnectionReply1,
|
|
9
|
+
readOpenConnectionRequest2,
|
|
10
|
+
writeOpenConnectionReply2,
|
|
11
|
+
writeIncompatibleProtocolVersion,
|
|
12
|
+
writeNoFreeIncomingConnections,
|
|
13
|
+
writeAlreadyConnected,
|
|
14
|
+
} from "./protocol/offline.js";
|
|
15
|
+
import {
|
|
16
|
+
MessageId,
|
|
17
|
+
PacketReliability,
|
|
18
|
+
PacketPriority,
|
|
19
|
+
RAKNET_PROTOCOL_VERSION,
|
|
20
|
+
DATAGRAM_FLAG,
|
|
21
|
+
ACK_FLAG,
|
|
22
|
+
NACK_FLAG,
|
|
23
|
+
MAX_MTU_SIZE,
|
|
24
|
+
} from "./constants.js";
|
|
25
|
+
import { UdpTransport } from "./transport/udp.js";
|
|
26
|
+
|
|
27
|
+
export class RakServer extends EventEmitter {
|
|
28
|
+
constructor(address, port, guid, maxConnections = 20, motd = "MCPE;Server;390;1.14.0;0;20;0;world;Survival;1;", options = {}) {
|
|
29
|
+
super();
|
|
30
|
+
this.address = address;
|
|
31
|
+
this.port = port;
|
|
32
|
+
this._guid = guid ?? randomBigInt();
|
|
33
|
+
this._maxConnections = maxConnections;
|
|
34
|
+
this._motd = motd;
|
|
35
|
+
this._transport = options.transport ?? new UdpTransport();
|
|
36
|
+
this._connections = new Map();
|
|
37
|
+
this._bound = false;
|
|
38
|
+
this._setupTransport();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_setupTransport() {
|
|
42
|
+
this._transport.onError((err) => this.emit("error", err));
|
|
43
|
+
this._transport.onMessage((msg, rinfo) => this._handleMessage(msg, rinfo.address, rinfo.port));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
listen() {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
this._transport.bind(this.port, this.address)
|
|
49
|
+
.then(() => { this._bound = true; this.emit("listening"); resolve(); })
|
|
50
|
+
.catch(reject);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_key(address, port) { return `${address}:${port}`; }
|
|
55
|
+
|
|
56
|
+
_handleMessage(msg, address, port) {
|
|
57
|
+
if (msg.length === 0) return;
|
|
58
|
+
const flag = msg[0];
|
|
59
|
+
|
|
60
|
+
if (isOfflineMessage(msg)) {
|
|
61
|
+
this._handleOffline(msg, address, port, flag);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const conn = this._connections.get(this._key(address, port));
|
|
66
|
+
if (conn) {
|
|
67
|
+
if (
|
|
68
|
+
(flag & DATAGRAM_FLAG) === DATAGRAM_FLAG ||
|
|
69
|
+
(flag & ACK_FLAG) === ACK_FLAG ||
|
|
70
|
+
(flag & NACK_FLAG) === NACK_FLAG
|
|
71
|
+
) {
|
|
72
|
+
conn.handleDatagram(msg);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_handleOffline(msg, address, port, id) {
|
|
78
|
+
const sendFn = (buf, a, p) => this._transport.send(buf, a, p);
|
|
79
|
+
|
|
80
|
+
switch (id) {
|
|
81
|
+
case MessageId.UnconnectedPing:
|
|
82
|
+
case MessageId.UnconnectedPingOpenConn: {
|
|
83
|
+
const ping = readUnconnectedPing(msg);
|
|
84
|
+
if (!ping) return;
|
|
85
|
+
this._transport.send(writeUnconnectedPong(ping.sendTimestamp, this._guid, this._motd), address, port);
|
|
86
|
+
this.emit("ping", { address, port, guid: ping.clientGuid });
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case MessageId.OpenConnectionRequest1: {
|
|
91
|
+
const req = readOpenConnectionRequest1(msg);
|
|
92
|
+
if (!req) return;
|
|
93
|
+
const connKey = this._key(address, port);
|
|
94
|
+
if (this._connections.has(connKey)) { this._transport.send(writeAlreadyConnected(this._guid), address, port); return; }
|
|
95
|
+
if (req.protocol !== RAKNET_PROTOCOL_VERSION) { this._transport.send(writeIncompatibleProtocolVersion(this._guid), address, port); return; }
|
|
96
|
+
if (this._connections.size >= this._maxConnections) { this._transport.send(writeNoFreeIncomingConnections(this._guid), address, port); return; }
|
|
97
|
+
this._transport.send(writeOpenConnectionReply1(this._guid, Math.min(req.mtu, MAX_MTU_SIZE)), address, port);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case MessageId.OpenConnectionRequest2: {
|
|
102
|
+
const req = readOpenConnectionRequest2(msg);
|
|
103
|
+
if (!req) return;
|
|
104
|
+
const connKey = this._key(address, port);
|
|
105
|
+
if (this._connections.has(connKey)) { this._transport.send(writeAlreadyConnected(this._guid), address, port); return; }
|
|
106
|
+
if (this._connections.size >= this._maxConnections) { this._transport.send(writeNoFreeIncomingConnections(this._guid), address, port); return; }
|
|
107
|
+
|
|
108
|
+
this._transport.send(writeOpenConnectionReply2(this._guid, address, port, req.mtu), address, port);
|
|
109
|
+
const conn = new Connection(address, port, req.clientGuid, sendFn, req.mtu);
|
|
110
|
+
this._registerConnection(conn, connKey);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_registerConnection(conn, connKey) {
|
|
117
|
+
conn.on("connected", () => this.emit("connect", conn));
|
|
118
|
+
conn.on("disconnected", (reason) => { this._connections.delete(connKey); this.emit("disconnect", conn, reason); });
|
|
119
|
+
conn.on("timeout", () => { this._connections.delete(connKey); this.emit("disconnect", conn, "timeout"); });
|
|
120
|
+
conn.on("encapsulated", (packet) => {
|
|
121
|
+
this.emit("encapsulated", packet, { address: conn.address, port: conn.port });
|
|
122
|
+
this.emit("message", packet, conn);
|
|
123
|
+
});
|
|
124
|
+
conn.on("error", (err) => this.emit("error", err));
|
|
125
|
+
this._connections.set(connKey, conn);
|
|
126
|
+
conn.start();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
send(buf, address, port, priority, reliability, channel) {
|
|
130
|
+
this._connections.get(this._key(address, port))?.sendEncapsulated(buf, priority, reliability, channel);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
broadcast(buf, priority, reliability, channel) {
|
|
134
|
+
for (const conn of this._connections.values()) conn.sendEncapsulated(buf, priority, reliability, channel);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getConnection(address, port) { return this._connections.get(this._key(address, port)); }
|
|
138
|
+
getConnections() { return [...this._connections.values()]; }
|
|
139
|
+
|
|
140
|
+
closeConnection(address, port) {
|
|
141
|
+
const key = this._key(address, port);
|
|
142
|
+
const conn = this._connections.get(key);
|
|
143
|
+
if (conn) { conn.close(true); this._connections.delete(key); this.emit("disconnect", conn, "server_closed"); }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
setMotd(motd) { this._motd = motd; }
|
|
147
|
+
|
|
148
|
+
close() {
|
|
149
|
+
for (const conn of this._connections.values()) conn.close(true);
|
|
150
|
+
this._connections.clear();
|
|
151
|
+
if (!this._bound) return Promise.resolve();
|
|
152
|
+
this._bound = false;
|
|
153
|
+
return this._transport.close();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function randomBigInt() {
|
|
158
|
+
const hi = BigInt(Math.floor(Math.random() * 0x7fffffff));
|
|
159
|
+
const lo = BigInt(Math.floor(Math.random() * 0xffffffff));
|
|
160
|
+
return (hi << 32n) | lo;
|
|
161
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as dgram from "dgram";
|
|
2
|
+
|
|
3
|
+
export class UdpTransport {
|
|
4
|
+
constructor() {
|
|
5
|
+
this._socket = dgram.createSocket({ type: "udp4", reuseAddr: true });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
getRawSocket() { return this._socket; }
|
|
9
|
+
|
|
10
|
+
send(buf, address, port) {
|
|
11
|
+
this._socket.send(buf, 0, buf.length, port, address);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
bind(port, address) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
this._socket.once("error", reject);
|
|
17
|
+
this._socket.bind(port, address ?? "0.0.0.0", () => {
|
|
18
|
+
this._socket.off("error", reject);
|
|
19
|
+
resolve();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
onMessage(handler) {
|
|
25
|
+
this._socket.on("message", (msg, rinfo) => {
|
|
26
|
+
handler(msg, { address: rinfo.address, port: rinfo.port });
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onError(handler) { this._socket.on("error", handler); }
|
|
31
|
+
|
|
32
|
+
close() {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
try { this._socket.close(() => resolve()); }
|
|
35
|
+
catch { resolve(); }
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as http from "http";
|
|
2
|
+
import * as https from "https";
|
|
3
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
4
|
+
|
|
5
|
+
let virtualPortCounter = 10000;
|
|
6
|
+
function nextVirtualPort() { return virtualPortCounter++; }
|
|
7
|
+
|
|
8
|
+
export class WebSocketServerTransport {
|
|
9
|
+
constructor(tlsOptions) {
|
|
10
|
+
this._tlsOptions = tlsOptions;
|
|
11
|
+
this._messageHandler = undefined;
|
|
12
|
+
this._errorHandler = undefined;
|
|
13
|
+
this._peers = new Map();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
bind(port, address) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
this._httpServer = this._tlsOptions
|
|
19
|
+
? https.createServer(this._tlsOptions)
|
|
20
|
+
: http.createServer();
|
|
21
|
+
|
|
22
|
+
this._wss = new WebSocketServer({ server: this._httpServer });
|
|
23
|
+
|
|
24
|
+
this._wss.on("connection", (ws, req) => {
|
|
25
|
+
const remoteIp =
|
|
26
|
+
(req.headers["x-forwarded-for"])
|
|
27
|
+
?.split(",")[0]
|
|
28
|
+
?.trim() ?? req.socket.remoteAddress ?? "0.0.0.0";
|
|
29
|
+
const virtualPort = nextVirtualPort();
|
|
30
|
+
const key = `${remoteIp}:${virtualPort}`;
|
|
31
|
+
this._peers.set(key, ws);
|
|
32
|
+
|
|
33
|
+
ws.on("message", (data) => {
|
|
34
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
35
|
+
this._messageHandler?.(buf, { address: remoteIp, port: virtualPort });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
ws.on("close", () => {
|
|
39
|
+
this._peers.delete(key);
|
|
40
|
+
this._messageHandler?.(Buffer.from([0x15]), { address: remoteIp, port: virtualPort });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
ws.on("error", (err) => this._errorHandler?.(err));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
this._wss.on("error", (err) => { this._errorHandler?.(err); reject(err); });
|
|
47
|
+
this._httpServer.listen(port, address ?? "0.0.0.0", () => resolve());
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
send(buf, address, port) {
|
|
52
|
+
const ws = this._peers.get(`${address}:${port}`);
|
|
53
|
+
if (ws && ws.readyState === WebSocket.OPEN) ws.send(buf);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
onMessage(handler) { this._messageHandler = handler; }
|
|
57
|
+
onError(handler) { this._errorHandler = handler; }
|
|
58
|
+
|
|
59
|
+
close() {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
for (const ws of this._peers.values()) ws.terminate();
|
|
62
|
+
this._peers.clear();
|
|
63
|
+
this._wss.close(() => this._httpServer.close(() => resolve()));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getHttpServer() { return this._httpServer; }
|
|
68
|
+
|
|
69
|
+
mountOnExisting(server, path = "/raknet") {
|
|
70
|
+
this._httpServer = server;
|
|
71
|
+
this._wss = new WebSocketServer({ server, path });
|
|
72
|
+
|
|
73
|
+
this._wss.on("connection", (ws, req) => {
|
|
74
|
+
const remoteIp =
|
|
75
|
+
(req.headers["x-forwarded-for"])
|
|
76
|
+
?.split(",")[0]
|
|
77
|
+
?.trim() ?? req.socket.remoteAddress ?? "0.0.0.0";
|
|
78
|
+
const virtualPort = nextVirtualPort();
|
|
79
|
+
const key = `${remoteIp}:${virtualPort}`;
|
|
80
|
+
this._peers.set(key, ws);
|
|
81
|
+
|
|
82
|
+
ws.on("message", (data) => {
|
|
83
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
84
|
+
this._messageHandler?.(buf, { address: remoteIp, port: virtualPort });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
ws.on("close", () => {
|
|
88
|
+
this._peers.delete(key);
|
|
89
|
+
this._messageHandler?.(Buffer.from([0x15]), { address: remoteIp, port: virtualPort });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
ws.on("error", (err) => this._errorHandler?.(err));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class WebSocketClientTransport {
|
|
98
|
+
constructor(serverUrl) {
|
|
99
|
+
this._serverUrl = serverUrl;
|
|
100
|
+
this._serverAddress = serverUrl;
|
|
101
|
+
this._serverPort = 0;
|
|
102
|
+
this._messageHandler = undefined;
|
|
103
|
+
this._errorHandler = undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
bind(_port, _address) {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
this._ws = new WebSocket(this._serverAddress);
|
|
109
|
+
this._ws.binaryType = "nodebuffer";
|
|
110
|
+
|
|
111
|
+
this._ws.on("open", () => resolve());
|
|
112
|
+
this._ws.on("error", (err) => { this._errorHandler?.(err); reject(err); });
|
|
113
|
+
|
|
114
|
+
this._ws.on("message", (data) => {
|
|
115
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
116
|
+
this._messageHandler?.(buf, { address: this._serverAddress, port: this._serverPort });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
this._ws.on("close", () => {
|
|
120
|
+
this._messageHandler?.(Buffer.from([0x15]), { address: this._serverAddress, port: this._serverPort });
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
send(buf, _address, _port) {
|
|
126
|
+
if (this._ws && this._ws.readyState === WebSocket.OPEN) this._ws.send(buf);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onMessage(handler) { this._messageHandler = handler; }
|
|
130
|
+
onError(handler) { this._errorHandler = handler; }
|
|
131
|
+
|
|
132
|
+
close() {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
if (this._ws) { this._ws.once("close", () => resolve()); this._ws.close(); }
|
|
135
|
+
else resolve();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
export class BinaryStream {
|
|
2
|
+
constructor(buffer) {
|
|
3
|
+
this._buffer = buffer ? Buffer.from(buffer) : Buffer.alloc(0);
|
|
4
|
+
this._offset = 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
get buffer() { return this._buffer; }
|
|
8
|
+
get offset() { return this._offset; }
|
|
9
|
+
get length() { return this._buffer.length; }
|
|
10
|
+
get remaining() { return this._buffer.length - this._offset; }
|
|
11
|
+
|
|
12
|
+
seek(offset) { this._offset = offset; }
|
|
13
|
+
skip(n) { this._offset += n; }
|
|
14
|
+
|
|
15
|
+
_grow(n) {
|
|
16
|
+
const needed = this._offset + n;
|
|
17
|
+
if (needed > this._buffer.length) {
|
|
18
|
+
const extended = Buffer.alloc(needed);
|
|
19
|
+
this._buffer.copy(extended);
|
|
20
|
+
this._buffer = extended;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
readByte() { return this._buffer.readUInt8(this._offset++); }
|
|
25
|
+
writeByte(value) {
|
|
26
|
+
this._grow(1);
|
|
27
|
+
this._buffer.writeUInt8(value & 0xff, this._offset++);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
readBytes(n) {
|
|
31
|
+
const slice = this._buffer.slice(this._offset, this._offset + n);
|
|
32
|
+
this._offset += n;
|
|
33
|
+
return slice;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
writeBytes(buf) {
|
|
37
|
+
this._grow(buf.length);
|
|
38
|
+
buf.copy(this._buffer, this._offset);
|
|
39
|
+
this._offset += buf.length;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
readBool() { return this.readByte() !== 0; }
|
|
43
|
+
writeBool(value) { this.writeByte(value ? 1 : 0); }
|
|
44
|
+
|
|
45
|
+
readUInt16BE() {
|
|
46
|
+
const v = this._buffer.readUInt16BE(this._offset);
|
|
47
|
+
this._offset += 2;
|
|
48
|
+
return v;
|
|
49
|
+
}
|
|
50
|
+
writeUInt16BE(value) {
|
|
51
|
+
this._grow(2);
|
|
52
|
+
this._buffer.writeUInt16BE(value & 0xffff, this._offset);
|
|
53
|
+
this._offset += 2;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
readUInt16LE() {
|
|
57
|
+
const v = this._buffer.readUInt16LE(this._offset);
|
|
58
|
+
this._offset += 2;
|
|
59
|
+
return v;
|
|
60
|
+
}
|
|
61
|
+
writeUInt16LE(value) {
|
|
62
|
+
this._grow(2);
|
|
63
|
+
this._buffer.writeUInt16LE(value & 0xffff, this._offset);
|
|
64
|
+
this._offset += 2;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
readUInt24LE() {
|
|
68
|
+
const a = this._buffer.readUInt8(this._offset);
|
|
69
|
+
const b = this._buffer.readUInt8(this._offset + 1);
|
|
70
|
+
const c = this._buffer.readUInt8(this._offset + 2);
|
|
71
|
+
this._offset += 3;
|
|
72
|
+
return a | (b << 8) | (c << 16);
|
|
73
|
+
}
|
|
74
|
+
writeUInt24LE(value) {
|
|
75
|
+
this._grow(3);
|
|
76
|
+
this._buffer.writeUInt8(value & 0xff, this._offset);
|
|
77
|
+
this._buffer.writeUInt8((value >> 8) & 0xff, this._offset + 1);
|
|
78
|
+
this._buffer.writeUInt8((value >> 16) & 0xff, this._offset + 2);
|
|
79
|
+
this._offset += 3;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
readInt32BE() {
|
|
83
|
+
const v = this._buffer.readInt32BE(this._offset);
|
|
84
|
+
this._offset += 4;
|
|
85
|
+
return v;
|
|
86
|
+
}
|
|
87
|
+
writeInt32BE(value) {
|
|
88
|
+
this._grow(4);
|
|
89
|
+
this._buffer.writeInt32BE(value, this._offset);
|
|
90
|
+
this._offset += 4;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
readUInt32BE() {
|
|
94
|
+
const v = this._buffer.readUInt32BE(this._offset);
|
|
95
|
+
this._offset += 4;
|
|
96
|
+
return v;
|
|
97
|
+
}
|
|
98
|
+
writeUInt32BE(value) {
|
|
99
|
+
this._grow(4);
|
|
100
|
+
this._buffer.writeUInt32BE(value >>> 0, this._offset);
|
|
101
|
+
this._offset += 4;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
readUInt32LE() {
|
|
105
|
+
const v = this._buffer.readUInt32LE(this._offset);
|
|
106
|
+
this._offset += 4;
|
|
107
|
+
return v;
|
|
108
|
+
}
|
|
109
|
+
writeUInt32LE(value) {
|
|
110
|
+
this._grow(4);
|
|
111
|
+
this._buffer.writeUInt32LE(value >>> 0, this._offset);
|
|
112
|
+
this._offset += 4;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
readInt64BE() {
|
|
116
|
+
const v = this._buffer.readBigInt64BE(this._offset);
|
|
117
|
+
this._offset += 8;
|
|
118
|
+
return v;
|
|
119
|
+
}
|
|
120
|
+
writeInt64BE(value) {
|
|
121
|
+
this._grow(8);
|
|
122
|
+
this._buffer.writeBigInt64BE(value, this._offset);
|
|
123
|
+
this._offset += 8;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
readUInt64BE() {
|
|
127
|
+
const v = this._buffer.readBigUInt64BE(this._offset);
|
|
128
|
+
this._offset += 8;
|
|
129
|
+
return v;
|
|
130
|
+
}
|
|
131
|
+
writeUInt64BE(value) {
|
|
132
|
+
this._grow(8);
|
|
133
|
+
this._buffer.writeBigUInt64BE(value, this._offset);
|
|
134
|
+
this._offset += 8;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
readString() {
|
|
138
|
+
const len = this.readUInt16BE();
|
|
139
|
+
return this.readBytes(len).toString("utf8");
|
|
140
|
+
}
|
|
141
|
+
writeString(value) {
|
|
142
|
+
const buf = Buffer.from(value, "utf8");
|
|
143
|
+
this.writeUInt16BE(buf.length);
|
|
144
|
+
this.writeBytes(buf);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
readAddress() {
|
|
148
|
+
const version = this.readByte();
|
|
149
|
+
if (version === 4) {
|
|
150
|
+
const parts = [];
|
|
151
|
+
for (let i = 0; i < 4; i++) parts.push(this.readByte() ^ 0xff);
|
|
152
|
+
const port = this.readUInt16BE();
|
|
153
|
+
return { address: parts.join("."), port, version: 4 };
|
|
154
|
+
} else {
|
|
155
|
+
this.skip(2);
|
|
156
|
+
const port = this.readUInt16BE();
|
|
157
|
+
this.skip(4);
|
|
158
|
+
const ipBytes = this.readBytes(16);
|
|
159
|
+
this.skip(4);
|
|
160
|
+
const parts = [];
|
|
161
|
+
for (let i = 0; i < 8; i++) parts.push(ipBytes.readUInt16BE(i * 2).toString(16));
|
|
162
|
+
return { address: parts.join(":"), port, version: 6 };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
writeAddress(address, port, version = 4) {
|
|
167
|
+
this.writeByte(version);
|
|
168
|
+
if (version === 4) {
|
|
169
|
+
for (const part of address.split(".")) this.writeByte((parseInt(part) ^ 0xff) & 0xff);
|
|
170
|
+
this.writeUInt16BE(port);
|
|
171
|
+
} else {
|
|
172
|
+
this.writeUInt16BE(0x0a);
|
|
173
|
+
this.writeUInt16BE(port);
|
|
174
|
+
this.writeUInt32BE(0);
|
|
175
|
+
const parts = address.split(":").map((p) => parseInt(p, 16));
|
|
176
|
+
for (const part of parts) this.writeUInt16BE(part);
|
|
177
|
+
this.writeUInt32BE(0);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
flush() {
|
|
182
|
+
return this._buffer.slice(0, this._offset);
|
|
183
|
+
}
|
|
184
|
+
}
|
package/src/ws-client.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RakClient } from "./client.js";
|
|
2
|
+
import { WebSocketClientTransport } from "./transport/websocket.js";
|
|
3
|
+
|
|
4
|
+
export class RakWebSocketClient extends RakClient {
|
|
5
|
+
constructor(serverUrl, options = {}) {
|
|
6
|
+
const url = new URL(serverUrl);
|
|
7
|
+
const address = url.hostname;
|
|
8
|
+
const port = parseInt(url.port) || (url.protocol === "wss:" ? 443 : 80);
|
|
9
|
+
const transport = new WebSocketClientTransport(serverUrl);
|
|
10
|
+
super(address, port, options.guid, { transport });
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/ws-server.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RakServer } from "./server.js";
|
|
2
|
+
import { WebSocketServerTransport } from "./transport/websocket.js";
|
|
3
|
+
|
|
4
|
+
export class RakWebSocketServer extends RakServer {
|
|
5
|
+
constructor(address, port, guid, options = {}) {
|
|
6
|
+
const wsTransport = new WebSocketServerTransport(options.tls);
|
|
7
|
+
super(address, port, guid, options.maxConnections, options.motd, { transport: wsTransport });
|
|
8
|
+
this._wsTransport = wsTransport;
|
|
9
|
+
if (options.server) this._wsTransport.mountOnExisting(options.server, options.path ?? "/raknet");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getHttpServer() { return this._wsTransport.getHttpServer(); }
|
|
13
|
+
}
|