├── .gitignore ├── README.md ├── build.js ├── examples ├── client.js ├── ipaddr.js ├── nodeUdpConn.js └── server.js ├── libs └── chacha20poly1305.js ├── package.json ├── src ├── ByteBuffer.ts ├── Defines.ts ├── Errors.ts ├── NetcodeConnection.ts ├── Packet.ts ├── ReplayProtection.ts ├── Token.ts ├── Utils.ts ├── chacha20poly1305.d.ts ├── client │ └── Client.ts └── server │ ├── ClientManager.ts │ └── Server.ts ├── tests ├── bytebuffer_test.js ├── client_manager_test.js ├── packet_test.js ├── replayprotection_test.js └── token_test.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | dist/browser 2 | dist/node 3 | *.tsbuildinfo 4 | .vscode 5 | node_modules 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netcode.io-typescript 2 | 3 | TypeScript/JavaScript implementation of [netcode.io](https://github.com/networkprotocol/netcode). 4 | 5 | ### Why TypeScript/JavaScript version of netcode? 6 | 7 | JavsScript is mainly used for webs, and I know we cannot send UDP packets from the browser ([Why can't I send UDP packets from a browser?](http://gafferongames.com/post/why_cant_i_send_udp_packets_from_a_browser/)). These days, instant games (e.g. Facebook Instant Games) are quite popular, and fast-paced multiplayer games are inevitably emerging on these platforms. These platforms are using typical web technologies, but mostly they also expose UDP APIs from the native side. So this is the reason that I want a JavaScript version of netcode mainly for the client-side. 8 | 9 | ### netcode version 10 | 11 | 1.0.1 12 | 13 | ### Build project 14 | 15 | npm run-script build 16 | 17 | ### Test 18 | 19 | mocha tests 20 | 21 | ### How to use 22 | 23 | Implement Netcode.IUDPConn interface for netcode to use to send/receive UDP packets. 24 | 25 | A sample class is as below. 26 | 27 | ``` 28 | class InstantGameUDPConn implements Netcode.IUDPConn { 29 | public static create() { 30 | return new InstantGameUDPConn(); 31 | } 32 | 33 | public constructor() { 34 | this._socket = createUDPSocket(); // Replace with your platform UDP API 35 | } 36 | public connect(addr: Netcode.IUDPAddr) { 37 | const ip = Netcode.Utils.IPV4AddressToString(addr); 38 | this._connectedAddr = ip; 39 | this._connectedPort = addr.port; 40 | } 41 | public bind(addr: Netcode.IUDPAddr) { 42 | this._socket.bind(addr.port); 43 | } 44 | public send(b: Uint8Array) { 45 | if (this._connectedPort) { 46 | this._socket.send({ 47 | address: this._connectedAddr, 48 | port: this._connectedPort, 49 | message: b.buffer, 50 | }); 51 | } 52 | } 53 | public sendTo(b: Uint8Array, addr: Netcode.IUDPAddr) { 54 | const ip = Netcode.Utils.IPV4AddressToString(addr); 55 | this._socket.send({ 56 | address: ip, 57 | port: addr.port, 58 | message: b.buffer, 59 | }); 60 | } 61 | public close() { 62 | this._socket.close(); 63 | } 64 | public setReadBuffer(size: number) {} 65 | public setWriteBuffer(size: number) {} 66 | 67 | public onMessage(callback: Netcode.onMessageHandler) { 68 | this._socket.onMessage(res => { 69 | const { message, remoteInfo } = res; 70 | if (!this._addr) { 71 | const fullAddr = remoteInfo.address + ':' + remoteInfo.port; 72 | this._addr = Netcode.Utils.stringToIPV4Address(fullAddr); 73 | } 74 | callback(new Uint8Array(message), this._addr); 75 | }); 76 | } 77 | 78 | private _connectedAddr: string; 79 | private _connectedPort: number; 80 | private _socket: UDPSocket; 81 | private _addr: Netcode.IUDPAddr; 82 | } 83 | ``` 84 | 85 | Pass your Netcode.IUDPConn class creator when connecting your client. 86 | 87 | ``` 88 | const client = new Netcode.Client(YOUR_CONNECT_TOKEN); 89 | client.id = YOUR_CLINET_ID; 90 | client.connect(InstantGameUDPConn.create); 91 | ``` 92 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | fs.mkdirSync(path.resolve(__dirname, 'dist/browser'), { recursive: true }); 5 | fs.mkdirSync(path.resolve(__dirname, 'dist/node'), { recursive: true }); 6 | 7 | fs.copyFileSync( 8 | path.resolve(__dirname, 'libs/chacha20poly1305.js'), 9 | path.resolve(__dirname, 'dist/browser/chacha20poly1305.js') 10 | ); 11 | 12 | fs.copyFileSync( 13 | path.resolve(__dirname, 'libs/chacha20poly1305.js'), 14 | path.resolve(__dirname, 'dist/node/chacha20poly1305.js') 15 | ); 16 | 17 | var script = fs.readFileSync( 18 | path.resolve(__dirname, 'dist/browser/netcode.js') 19 | ); 20 | script += ` 21 | var {aead_encrypt,aead_decrypt,getRandomBytes}=require('./chacha20poly1305'); 22 | `; 23 | var entryFile = path.resolve(__dirname, 'dist/node/netcode.js'); 24 | fs.writeFileSync(entryFile, script); 25 | console.log('successfully export', entryFile); 26 | -------------------------------------------------------------------------------- /examples/client.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var { Netcode } = require('../dist/node/netcode'); 3 | var { createUdpConn } = require('./nodeUdpConn'); 4 | 5 | function getConnectToken() { 6 | return new Promise((resolve, reject) => { 7 | var options = { 8 | host: 'localhost', 9 | path: '/token', 10 | port: '8880', 11 | method: 'GET', 12 | }; 13 | 14 | var webtoken; 15 | callback = function (response) { 16 | response.on('data', function (chunk) { 17 | webtoken = JSON.parse(chunk); 18 | // console.log(webtoken); 19 | let buff = Buffer.alloc(2048, webtoken.connect_token, 'base64'); 20 | const token = new Netcode.ConnectToken(); 21 | var err = token.read(buff); 22 | if (err === Netcode.Errors.none) { 23 | resolve({ token, clientID: webtoken.client_id }); 24 | } else { 25 | console.error('read token faile', Netcode.Errors[err]); 26 | reject(); 27 | } 28 | }); 29 | }; 30 | 31 | var req = http.request(options, callback); 32 | req.end(); 33 | }); 34 | } 35 | 36 | var time = 0; 37 | pingPayloadBytes = new Uint8Array(2); 38 | 39 | function startClientLoop(clientID, token) { 40 | var client = new Netcode.Client(token); 41 | client.id = clientID; 42 | client.debug = true; 43 | console.log(client._connectToken.sharedTokenData.serverAddrs); 44 | var err = client.connect(createUdpConn); 45 | if (err !== Netcode.Errors.none) { 46 | console.error('error connecting', err); 47 | } 48 | console.log('client start connecting to server'); 49 | setInterval(fakeGameLoop, 17, client); 50 | } 51 | 52 | var printed = false; 53 | var lastSendPingTime = 0; 54 | function fakeGameLoop(client) { 55 | // if (time > 5) { 56 | // clearInterval(fakeGameLoop); 57 | // } 58 | 59 | if (client.state == Netcode.ClientState.connected) { 60 | if (!printed) { 61 | console.log('client connected to server'); 62 | printed = true; 63 | } 64 | const now = Date.now(); 65 | if (now - lastSendPingTime > 1000) { 66 | client.sendPayload(pingPayloadBytes); 67 | lastSendPingTime = now; 68 | } 69 | } 70 | 71 | client.tick(time); 72 | 73 | while (true) { 74 | var r = client.recvPayload(); 75 | if (r && r.data) { 76 | console.log('recv pong payload', r.data); 77 | const rtt = Date.now() - lastSendPingTime; 78 | console.log('rtt', rtt); 79 | } else { 80 | break; 81 | } 82 | } 83 | 84 | time += 1 / 60; 85 | } 86 | 87 | getConnectToken().then(ret => { 88 | startClientLoop(ret.clientID, ret.token); 89 | }); 90 | -------------------------------------------------------------------------------- /examples/ipaddr.js: -------------------------------------------------------------------------------- 1 | (function (root) { 2 | 'use strict'; 3 | // A list of regular expressions that match arbitrary IPv4 addresses, 4 | // for which a number of weird notations exist. 5 | // Note that an address like 0010.0xa5.1.1 is considered legal. 6 | const ipv4Part = '(0?\\d+|0x[a-f0-9]+)'; 7 | const ipv4Regexes = { 8 | fourOctet: new RegExp(`^${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}$`, 'i'), 9 | threeOctet: new RegExp(`^${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}$`, 'i'), 10 | twoOctet: new RegExp(`^${ipv4Part}\\.${ipv4Part}$`, 'i'), 11 | longValue: new RegExp(`^${ipv4Part}$`, 'i') 12 | }; 13 | 14 | // Regular Expression for checking Octal numbers 15 | const octalRegex = new RegExp(`^0[0-7]+$`, 'i'); 16 | const hexRegex = new RegExp(`^0x[a-f0-9]+$`, 'i'); 17 | 18 | const zoneIndex = '%[0-9a-z]{1,}'; 19 | 20 | // IPv6-matching regular expressions. 21 | // For IPv6, the task is simpler: it is enough to match the colon-delimited 22 | // hexadecimal IPv6 and a transitional variant with dotted-decimal IPv4 at 23 | // the end. 24 | const ipv6Part = '(?:[0-9a-f]+::?)+'; 25 | const ipv6Regexes = { 26 | zoneIndex: new RegExp(zoneIndex, 'i'), 27 | 'native': new RegExp(`^(::)?(${ipv6Part})?([0-9a-f]+)?(::)?(${zoneIndex})?$`, 'i'), 28 | deprecatedTransitional: new RegExp(`^(?:::)(${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}(${zoneIndex})?)$`, 'i'), 29 | transitional: new RegExp(`^((?:${ipv6Part})|(?:::)(?:${ipv6Part})?)${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}(${zoneIndex})?$`, 'i') 30 | }; 31 | 32 | // Expand :: in an IPv6 address or address part consisting of `parts` groups. 33 | function expandIPv6 (string, parts) { 34 | // More than one '::' means invalid adddress 35 | if (string.indexOf('::') !== string.lastIndexOf('::')) { 36 | return null; 37 | } 38 | 39 | let colonCount = 0; 40 | let lastColon = -1; 41 | let zoneId = (string.match(ipv6Regexes.zoneIndex) || [])[0]; 42 | let replacement, replacementCount; 43 | 44 | // Remove zone index and save it for later 45 | if (zoneId) { 46 | zoneId = zoneId.substring(1); 47 | string = string.replace(/%.+$/, ''); 48 | } 49 | 50 | // How many parts do we already have? 51 | while ((lastColon = string.indexOf(':', lastColon + 1)) >= 0) { 52 | colonCount++; 53 | } 54 | 55 | // 0::0 is two parts more than :: 56 | if (string.substr(0, 2) === '::') { 57 | colonCount--; 58 | } 59 | 60 | if (string.substr(-2, 2) === '::') { 61 | colonCount--; 62 | } 63 | 64 | // The following loop would hang if colonCount > parts 65 | if (colonCount > parts) { 66 | return null; 67 | } 68 | 69 | // replacement = ':' + '0:' * (parts - colonCount) 70 | replacementCount = parts - colonCount; 71 | replacement = ':'; 72 | while (replacementCount--) { 73 | replacement += '0:'; 74 | } 75 | 76 | // Insert the missing zeroes 77 | string = string.replace('::', replacement); 78 | 79 | // Trim any garbage which may be hanging around if :: was at the edge in 80 | // the source strin 81 | if (string[0] === ':') { 82 | string = string.slice(1); 83 | } 84 | 85 | if (string[string.length - 1] === ':') { 86 | string = string.slice(0, -1); 87 | } 88 | 89 | parts = (function () { 90 | const ref = string.split(':'); 91 | const results = []; 92 | 93 | for (let i = 0; i < ref.length; i++) { 94 | results.push(parseInt(ref[i], 16)); 95 | } 96 | 97 | return results; 98 | })(); 99 | 100 | return { 101 | parts: parts, 102 | zoneId: zoneId 103 | }; 104 | } 105 | 106 | // A generic CIDR (Classless Inter-Domain Routing) RFC1518 range matcher. 107 | function matchCIDR (first, second, partSize, cidrBits) { 108 | if (first.length !== second.length) { 109 | throw new Error('ipaddr: cannot match CIDR for objects with different lengths'); 110 | } 111 | 112 | let part = 0; 113 | let shift; 114 | 115 | while (cidrBits > 0) { 116 | shift = partSize - cidrBits; 117 | if (shift < 0) { 118 | shift = 0; 119 | } 120 | 121 | if (first[part] >> shift !== second[part] >> shift) { 122 | return false; 123 | } 124 | 125 | cidrBits -= partSize; 126 | part += 1; 127 | } 128 | 129 | return true; 130 | } 131 | 132 | function parseIntAuto (string) { 133 | // Hexadedimal base 16 (0x#) 134 | if (hexRegex.test(string)) { 135 | return parseInt(string, 16); 136 | } 137 | // While octal representation is discouraged by ECMAScript 3 138 | // and forbidden by ECMAScript 5, we silently allow it to 139 | // work only if the rest of the string has numbers less than 8. 140 | if (string[0] === '0' && !isNaN(parseInt(string[1], 10))) { 141 | if (octalRegex.test(string)) { 142 | return parseInt(string, 8); 143 | } 144 | throw new Error(`ipaddr: cannot parse ${string} as octal`); 145 | } 146 | // Always include the base 10 radix! 147 | return parseInt(string, 10); 148 | } 149 | 150 | function padPart (part, length) { 151 | while (part.length < length) { 152 | part = `0${part}`; 153 | } 154 | 155 | return part; 156 | } 157 | 158 | const ipaddr = {}; 159 | 160 | // An IPv4 address (RFC791). 161 | ipaddr.IPv4 = (function () { 162 | // Constructs a new IPv4 address from an array of four octets 163 | // in network order (MSB first) 164 | // Verifies the input. 165 | function IPv4 (octets) { 166 | if (octets.length !== 4) { 167 | throw new Error('ipaddr: ipv4 octet count should be 4'); 168 | } 169 | 170 | let i, octet; 171 | 172 | for (i = 0; i < octets.length; i++) { 173 | octet = octets[i]; 174 | if (!((0 <= octet && octet <= 255))) { 175 | throw new Error('ipaddr: ipv4 octet should fit in 8 bits'); 176 | } 177 | } 178 | 179 | this.octets = octets; 180 | } 181 | 182 | // Special IPv4 address ranges. 183 | // See also https://en.wikipedia.org/wiki/Reserved_IP_addresses 184 | IPv4.prototype.SpecialRanges = { 185 | unspecified: [[new IPv4([0, 0, 0, 0]), 8]], 186 | broadcast: [[new IPv4([255, 255, 255, 255]), 32]], 187 | // RFC3171 188 | multicast: [[new IPv4([224, 0, 0, 0]), 4]], 189 | // RFC3927 190 | linkLocal: [[new IPv4([169, 254, 0, 0]), 16]], 191 | // RFC5735 192 | loopback: [[new IPv4([127, 0, 0, 0]), 8]], 193 | // RFC6598 194 | carrierGradeNat: [[new IPv4([100, 64, 0, 0]), 10]], 195 | // RFC1918 196 | 'private': [ 197 | [new IPv4([10, 0, 0, 0]), 8], 198 | [new IPv4([172, 16, 0, 0]), 12], 199 | [new IPv4([192, 168, 0, 0]), 16] 200 | ], 201 | // Reserved and testing-only ranges; RFCs 5735, 5737, 2544, 1700 202 | reserved: [ 203 | [new IPv4([192, 0, 0, 0]), 24], 204 | [new IPv4([192, 0, 2, 0]), 24], 205 | [new IPv4([192, 88, 99, 0]), 24], 206 | [new IPv4([198, 51, 100, 0]), 24], 207 | [new IPv4([203, 0, 113, 0]), 24], 208 | [new IPv4([240, 0, 0, 0]), 4] 209 | ] 210 | }; 211 | 212 | // The 'kind' method exists on both IPv4 and IPv6 classes. 213 | IPv4.prototype.kind = function () { 214 | return 'ipv4'; 215 | }; 216 | 217 | // Checks if this address matches other one within given CIDR range. 218 | IPv4.prototype.match = function (other, cidrRange) { 219 | let ref; 220 | if (cidrRange === undefined) { 221 | ref = other; 222 | other = ref[0]; 223 | cidrRange = ref[1]; 224 | } 225 | 226 | if (other.kind() !== 'ipv4') { 227 | throw new Error('ipaddr: cannot match ipv4 address with non-ipv4 one'); 228 | } 229 | 230 | return matchCIDR(this.octets, other.octets, 8, cidrRange); 231 | }; 232 | 233 | // returns a number of leading ones in IPv4 address, making sure that 234 | // the rest is a solid sequence of 0's (valid netmask) 235 | // returns either the CIDR length or null if mask is not valid 236 | IPv4.prototype.prefixLengthFromSubnetMask = function () { 237 | let cidr = 0; 238 | // non-zero encountered stop scanning for zeroes 239 | let stop = false; 240 | // number of zeroes in octet 241 | const zerotable = { 242 | 0: 8, 243 | 128: 7, 244 | 192: 6, 245 | 224: 5, 246 | 240: 4, 247 | 248: 3, 248 | 252: 2, 249 | 254: 1, 250 | 255: 0 251 | }; 252 | let i, octet, zeros; 253 | 254 | for (i = 3; i >= 0; i -= 1) { 255 | octet = this.octets[i]; 256 | if (octet in zerotable) { 257 | zeros = zerotable[octet]; 258 | if (stop && zeros !== 0) { 259 | return null; 260 | } 261 | 262 | if (zeros !== 8) { 263 | stop = true; 264 | } 265 | 266 | cidr += zeros; 267 | } else { 268 | return null; 269 | } 270 | } 271 | 272 | return 32 - cidr; 273 | }; 274 | 275 | // Checks if the address corresponds to one of the special ranges. 276 | IPv4.prototype.range = function () { 277 | return ipaddr.subnetMatch(this, this.SpecialRanges); 278 | }; 279 | 280 | // Returns an array of byte-sized values in network order (MSB first) 281 | IPv4.prototype.toByteArray = function () { 282 | return this.octets.slice(0); 283 | }; 284 | 285 | // Converts this IPv4 address to an IPv4-mapped IPv6 address. 286 | IPv4.prototype.toIPv4MappedAddress = function () { 287 | return ipaddr.IPv6.parse(`::ffff:${this.toString()}`); 288 | }; 289 | 290 | // Symmetrical method strictly for aligning with the IPv6 methods. 291 | IPv4.prototype.toNormalizedString = function () { 292 | return this.toString(); 293 | }; 294 | 295 | // Returns the address in convenient, decimal-dotted format. 296 | IPv4.prototype.toString = function () { 297 | return this.octets.join('.'); 298 | }; 299 | 300 | return IPv4; 301 | })(); 302 | 303 | // A utility function to return broadcast address given the IPv4 interface and prefix length in CIDR notation 304 | ipaddr.IPv4.broadcastAddressFromCIDR = function (string) { 305 | 306 | try { 307 | const cidr = this.parseCIDR(string); 308 | const ipInterfaceOctets = cidr[0].toByteArray(); 309 | const subnetMaskOctets = this.subnetMaskFromPrefixLength(cidr[1]).toByteArray(); 310 | const octets = []; 311 | let i = 0; 312 | while (i < 4) { 313 | // Broadcast address is bitwise OR between ip interface and inverted mask 314 | octets.push(parseInt(ipInterfaceOctets[i], 10) | parseInt(subnetMaskOctets[i], 10) ^ 255); 315 | i++; 316 | } 317 | 318 | return new this(octets); 319 | } catch (e) { 320 | throw new Error('ipaddr: the address does not have IPv4 CIDR format'); 321 | } 322 | }; 323 | 324 | // Checks if a given string is formatted like IPv4 address. 325 | ipaddr.IPv4.isIPv4 = function (string) { 326 | return this.parser(string) !== null; 327 | }; 328 | 329 | // Checks if a given string is a valid IPv4 address. 330 | ipaddr.IPv4.isValid = function (string) { 331 | try { 332 | new this(this.parser(string)); 333 | return true; 334 | } catch (e) { 335 | return false; 336 | } 337 | }; 338 | 339 | // Checks if a given string is a full four-part IPv4 Address. 340 | ipaddr.IPv4.isValidFourPartDecimal = function (string) { 341 | if (ipaddr.IPv4.isValid(string) && string.match(/^(0|[1-9]\d*)(\.(0|[1-9]\d*)){3}$/)) { 342 | return true; 343 | } else { 344 | return false; 345 | } 346 | }; 347 | 348 | // A utility function to return network address given the IPv4 interface and prefix length in CIDR notation 349 | ipaddr.IPv4.networkAddressFromCIDR = function (string) { 350 | let cidr, i, ipInterfaceOctets, octets, subnetMaskOctets; 351 | 352 | try { 353 | cidr = this.parseCIDR(string); 354 | ipInterfaceOctets = cidr[0].toByteArray(); 355 | subnetMaskOctets = this.subnetMaskFromPrefixLength(cidr[1]).toByteArray(); 356 | octets = []; 357 | i = 0; 358 | while (i < 4) { 359 | // Network address is bitwise AND between ip interface and mask 360 | octets.push(parseInt(ipInterfaceOctets[i], 10) & parseInt(subnetMaskOctets[i], 10)); 361 | i++; 362 | } 363 | 364 | return new this(octets); 365 | } catch (e) { 366 | throw new Error('ipaddr: the address does not have IPv4 CIDR format'); 367 | } 368 | }; 369 | 370 | // Tries to parse and validate a string with IPv4 address. 371 | // Throws an error if it fails. 372 | ipaddr.IPv4.parse = function (string) { 373 | const parts = this.parser(string); 374 | 375 | if (parts === null) { 376 | throw new Error('ipaddr: string is not formatted like an IPv4 Address'); 377 | } 378 | 379 | return new this(parts); 380 | }; 381 | 382 | // Parses the string as an IPv4 Address with CIDR Notation. 383 | ipaddr.IPv4.parseCIDR = function (string) { 384 | let match; 385 | 386 | if ((match = string.match(/^(.+)\/(\d+)$/))) { 387 | const maskLength = parseInt(match[2]); 388 | if (maskLength >= 0 && maskLength <= 32) { 389 | const parsed = [this.parse(match[1]), maskLength]; 390 | Object.defineProperty(parsed, 'toString', { 391 | value: function () { 392 | return this.join('/'); 393 | } 394 | }); 395 | return parsed; 396 | } 397 | } 398 | 399 | throw new Error('ipaddr: string is not formatted like an IPv4 CIDR range'); 400 | }; 401 | 402 | // Classful variants (like a.b, where a is an octet, and b is a 24-bit 403 | // value representing last three octets; this corresponds to a class C 404 | // address) are omitted due to classless nature of modern Internet. 405 | ipaddr.IPv4.parser = function (string) { 406 | let match, part, value; 407 | 408 | // parseInt recognizes all that octal & hexadecimal weirdness for us 409 | if ((match = string.match(ipv4Regexes.fourOctet))) { 410 | return (function () { 411 | const ref = match.slice(1, 6); 412 | const results = []; 413 | 414 | for (let i = 0; i < ref.length; i++) { 415 | part = ref[i]; 416 | results.push(parseIntAuto(part)); 417 | } 418 | 419 | return results; 420 | })(); 421 | } else if ((match = string.match(ipv4Regexes.longValue))) { 422 | value = parseIntAuto(match[1]); 423 | if (value > 0xffffffff || value < 0) { 424 | throw new Error('ipaddr: address outside defined range'); 425 | } 426 | 427 | return ((function () { 428 | const results = []; 429 | let shift; 430 | 431 | for (shift = 0; shift <= 24; shift += 8) { 432 | results.push((value >> shift) & 0xff); 433 | } 434 | 435 | return results; 436 | })()).reverse(); 437 | } else if ((match = string.match(ipv4Regexes.twoOctet))) { 438 | return (function () { 439 | const ref = match.slice(1, 4); 440 | const results = []; 441 | 442 | value = parseIntAuto(ref[1]); 443 | if (value > 0xffffff || value < 0) { 444 | throw new Error('ipaddr: address outside defined range'); 445 | } 446 | 447 | results.push(parseIntAuto(ref[0])); 448 | results.push((value >> 16) & 0xff); 449 | results.push((value >> 8) & 0xff); 450 | results.push( value & 0xff); 451 | 452 | return results; 453 | })(); 454 | } else if ((match = string.match(ipv4Regexes.threeOctet))) { 455 | return (function () { 456 | const ref = match.slice(1, 5); 457 | const results = []; 458 | 459 | value = parseIntAuto(ref[2]); 460 | if (value > 0xffff || value < 0) { 461 | throw new Error('ipaddr: address outside defined range'); 462 | } 463 | 464 | results.push(parseIntAuto(ref[0])); 465 | results.push(parseIntAuto(ref[1])); 466 | results.push((value >> 8) & 0xff); 467 | results.push( value & 0xff); 468 | 469 | return results; 470 | })(); 471 | } else { 472 | return null; 473 | } 474 | }; 475 | 476 | // A utility function to return subnet mask in IPv4 format given the prefix length 477 | ipaddr.IPv4.subnetMaskFromPrefixLength = function (prefix) { 478 | prefix = parseInt(prefix); 479 | if (prefix < 0 || prefix > 32) { 480 | throw new Error('ipaddr: invalid IPv4 prefix length'); 481 | } 482 | 483 | const octets = [0, 0, 0, 0]; 484 | let j = 0; 485 | const filledOctetCount = Math.floor(prefix / 8); 486 | 487 | while (j < filledOctetCount) { 488 | octets[j] = 255; 489 | j++; 490 | } 491 | 492 | if (filledOctetCount < 4) { 493 | octets[filledOctetCount] = Math.pow(2, prefix % 8) - 1 << 8 - (prefix % 8); 494 | } 495 | 496 | return new this(octets); 497 | }; 498 | 499 | // An IPv6 address (RFC2460) 500 | ipaddr.IPv6 = (function () { 501 | // Constructs an IPv6 address from an array of eight 16 - bit parts 502 | // or sixteen 8 - bit parts in network order(MSB first). 503 | // Throws an error if the input is invalid. 504 | function IPv6 (parts, zoneId) { 505 | let i, part; 506 | 507 | if (parts.length === 16) { 508 | this.parts = []; 509 | for (i = 0; i <= 14; i += 2) { 510 | this.parts.push((parts[i] << 8) | parts[i + 1]); 511 | } 512 | } else if (parts.length === 8) { 513 | this.parts = parts; 514 | } else { 515 | throw new Error('ipaddr: ipv6 part count should be 8 or 16'); 516 | } 517 | 518 | for (i = 0; i < this.parts.length; i++) { 519 | part = this.parts[i]; 520 | if (!((0 <= part && part <= 0xffff))) { 521 | throw new Error('ipaddr: ipv6 part should fit in 16 bits'); 522 | } 523 | } 524 | 525 | if (zoneId) { 526 | this.zoneId = zoneId; 527 | } 528 | } 529 | 530 | // Special IPv6 ranges 531 | IPv6.prototype.SpecialRanges = { 532 | // RFC4291, here and after 533 | unspecified: [new IPv6([0, 0, 0, 0, 0, 0, 0, 0]), 128], 534 | linkLocal: [new IPv6([0xfe80, 0, 0, 0, 0, 0, 0, 0]), 10], 535 | multicast: [new IPv6([0xff00, 0, 0, 0, 0, 0, 0, 0]), 8], 536 | loopback: [new IPv6([0, 0, 0, 0, 0, 0, 0, 1]), 128], 537 | uniqueLocal: [new IPv6([0xfc00, 0, 0, 0, 0, 0, 0, 0]), 7], 538 | ipv4Mapped: [new IPv6([0, 0, 0, 0, 0, 0xffff, 0, 0]), 96], 539 | // RFC6145 540 | rfc6145: [new IPv6([0, 0, 0, 0, 0xffff, 0, 0, 0]), 96], 541 | // RFC6052 542 | rfc6052: [new IPv6([0x64, 0xff9b, 0, 0, 0, 0, 0, 0]), 96], 543 | // RFC3056 544 | '6to4': [new IPv6([0x2002, 0, 0, 0, 0, 0, 0, 0]), 16], 545 | // RFC6052, RFC6146 546 | teredo: [new IPv6([0x2001, 0, 0, 0, 0, 0, 0, 0]), 32], 547 | // RFC4291 548 | reserved: [[new IPv6([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0]), 32]] 549 | }; 550 | 551 | // Checks if this address is an IPv4-mapped IPv6 address. 552 | IPv6.prototype.isIPv4MappedAddress = function () { 553 | return this.range() === 'ipv4Mapped'; 554 | }; 555 | 556 | // The 'kind' method exists on both IPv4 and IPv6 classes. 557 | IPv6.prototype.kind = function () { 558 | return 'ipv6'; 559 | }; 560 | 561 | // Checks if this address matches other one within given CIDR range. 562 | IPv6.prototype.match = function (other, cidrRange) { 563 | let ref; 564 | 565 | if (cidrRange === undefined) { 566 | ref = other; 567 | other = ref[0]; 568 | cidrRange = ref[1]; 569 | } 570 | 571 | if (other.kind() !== 'ipv6') { 572 | throw new Error('ipaddr: cannot match ipv6 address with non-ipv6 one'); 573 | } 574 | 575 | return matchCIDR(this.parts, other.parts, 16, cidrRange); 576 | }; 577 | 578 | // returns a number of leading ones in IPv6 address, making sure that 579 | // the rest is a solid sequence of 0's (valid netmask) 580 | // returns either the CIDR length or null if mask is not valid 581 | IPv6.prototype.prefixLengthFromSubnetMask = function () { 582 | let cidr = 0; 583 | // non-zero encountered stop scanning for zeroes 584 | let stop = false; 585 | // number of zeroes in octet 586 | const zerotable = { 587 | 0: 16, 588 | 32768: 15, 589 | 49152: 14, 590 | 57344: 13, 591 | 61440: 12, 592 | 63488: 11, 593 | 64512: 10, 594 | 65024: 9, 595 | 65280: 8, 596 | 65408: 7, 597 | 65472: 6, 598 | 65504: 5, 599 | 65520: 4, 600 | 65528: 3, 601 | 65532: 2, 602 | 65534: 1, 603 | 65535: 0 604 | }; 605 | let part, zeros; 606 | 607 | for (let i = 7; i >= 0; i -= 1) { 608 | part = this.parts[i]; 609 | if (part in zerotable) { 610 | zeros = zerotable[part]; 611 | if (stop && zeros !== 0) { 612 | return null; 613 | } 614 | 615 | if (zeros !== 16) { 616 | stop = true; 617 | } 618 | 619 | cidr += zeros; 620 | } else { 621 | return null; 622 | } 623 | } 624 | 625 | return 128 - cidr; 626 | }; 627 | 628 | 629 | // Checks if the address corresponds to one of the special ranges. 630 | IPv6.prototype.range = function () { 631 | return ipaddr.subnetMatch(this, this.SpecialRanges); 632 | }; 633 | 634 | // Returns an array of byte-sized values in network order (MSB first) 635 | IPv6.prototype.toByteArray = function () { 636 | let part; 637 | const bytes = []; 638 | const ref = this.parts; 639 | for (let i = 0; i < ref.length; i++) { 640 | part = ref[i]; 641 | bytes.push(part >> 8); 642 | bytes.push(part & 0xff); 643 | } 644 | 645 | return bytes; 646 | }; 647 | 648 | // Returns the address in expanded format with all zeroes included, like 649 | // 2001:0db8:0008:0066:0000:0000:0000:0001 650 | IPv6.prototype.toFixedLengthString = function () { 651 | const addr = ((function () { 652 | const results = []; 653 | for (let i = 0; i < this.parts.length; i++) { 654 | results.push(padPart(this.parts[i].toString(16), 4)); 655 | } 656 | 657 | return results; 658 | }).call(this)).join(':'); 659 | 660 | let suffix = ''; 661 | 662 | if (this.zoneId) { 663 | suffix = `%${this.zoneId}`; 664 | } 665 | 666 | return addr + suffix; 667 | }; 668 | 669 | // Converts this address to IPv4 address if it is an IPv4-mapped IPv6 address. 670 | // Throws an error otherwise. 671 | IPv6.prototype.toIPv4Address = function () { 672 | if (!this.isIPv4MappedAddress()) { 673 | throw new Error('ipaddr: trying to convert a generic ipv6 address to ipv4'); 674 | } 675 | 676 | const ref = this.parts.slice(-2); 677 | const high = ref[0]; 678 | const low = ref[1]; 679 | 680 | return new ipaddr.IPv4([high >> 8, high & 0xff, low >> 8, low & 0xff]); 681 | }; 682 | 683 | // Returns the address in expanded format with all zeroes included, like 684 | // 2001:db8:8:66:0:0:0:1 685 | // 686 | // Deprecated: use toFixedLengthString() instead. 687 | IPv6.prototype.toNormalizedString = function () { 688 | const addr = ((function () { 689 | const results = []; 690 | 691 | for (let i = 0; i < this.parts.length; i++) { 692 | results.push(this.parts[i].toString(16)); 693 | } 694 | 695 | return results; 696 | }).call(this)).join(':'); 697 | 698 | let suffix = ''; 699 | 700 | if (this.zoneId) { 701 | suffix = `%${this.zoneId}`; 702 | } 703 | 704 | return addr + suffix; 705 | }; 706 | 707 | // Returns the address in compact, human-readable format like 708 | // 2001:db8:8:66::1 709 | // in line with RFC 5952 (see https://tools.ietf.org/html/rfc5952#section-4) 710 | IPv6.prototype.toRFC5952String = function () { 711 | const regex = /((^|:)(0(:|$)){2,})/g; 712 | const string = this.toNormalizedString(); 713 | let bestMatchIndex = 0; 714 | let bestMatchLength = -1; 715 | let match; 716 | 717 | while ((match = regex.exec(string))) { 718 | if (match[0].length > bestMatchLength) { 719 | bestMatchIndex = match.index; 720 | bestMatchLength = match[0].length; 721 | } 722 | } 723 | 724 | if (bestMatchLength < 0) { 725 | return string; 726 | } 727 | 728 | return `${string.substring(0, bestMatchIndex)}::${string.substring(bestMatchIndex + bestMatchLength)}`; 729 | }; 730 | 731 | // Returns the address in compact, human-readable format like 732 | // 2001:db8:8:66::1 733 | // 734 | // Deprecated: use toRFC5952String() instead. 735 | IPv6.prototype.toString = function () { 736 | // Replace the first sequence of 1 or more '0' parts with '::' 737 | return this.toNormalizedString().replace(/((^|:)(0(:|$))+)/, '::'); 738 | }; 739 | 740 | return IPv6; 741 | 742 | })(); 743 | 744 | // Checks if a given string is formatted like IPv6 address. 745 | ipaddr.IPv6.isIPv6 = function (string) { 746 | return this.parser(string) !== null; 747 | }; 748 | 749 | // Checks to see if string is a valid IPv6 Address 750 | ipaddr.IPv6.isValid = function (string) { 751 | 752 | // Since IPv6.isValid is always called first, this shortcut 753 | // provides a substantial performance gain. 754 | if (typeof string === 'string' && string.indexOf(':') === -1) { 755 | return false; 756 | } 757 | 758 | try { 759 | const addr = this.parser(string); 760 | new this(addr.parts, addr.zoneId); 761 | return true; 762 | } catch (e) { 763 | return false; 764 | } 765 | }; 766 | 767 | // Tries to parse and validate a string with IPv6 address. 768 | // Throws an error if it fails. 769 | ipaddr.IPv6.parse = function (string) { 770 | const addr = this.parser(string); 771 | 772 | if (addr.parts === null) { 773 | throw new Error('ipaddr: string is not formatted like an IPv6 Address'); 774 | } 775 | 776 | return new this(addr.parts, addr.zoneId); 777 | }; 778 | 779 | ipaddr.IPv6.parseCIDR = function (string) { 780 | let maskLength, match, parsed; 781 | 782 | if ((match = string.match(/^(.+)\/(\d+)$/))) { 783 | maskLength = parseInt(match[2]); 784 | if (maskLength >= 0 && maskLength <= 128) { 785 | parsed = [this.parse(match[1]), maskLength]; 786 | Object.defineProperty(parsed, 'toString', { 787 | value: function () { 788 | return this.join('/'); 789 | } 790 | }); 791 | return parsed; 792 | } 793 | } 794 | 795 | throw new Error('ipaddr: string is not formatted like an IPv6 CIDR range'); 796 | }; 797 | 798 | // Parse an IPv6 address. 799 | ipaddr.IPv6.parser = function (string) { 800 | let addr, i, match, octet, octets, zoneId; 801 | 802 | if ((match = string.match(ipv6Regexes.deprecatedTransitional))) { 803 | return this.parser(`::ffff:${match[1]}`); 804 | } 805 | if (ipv6Regexes.native.test(string)) { 806 | return expandIPv6(string, 8); 807 | } 808 | if ((match = string.match(ipv6Regexes.transitional))) { 809 | zoneId = match[6] || ''; 810 | addr = expandIPv6(match[1].slice(0, -1) + zoneId, 6); 811 | if (addr.parts) { 812 | octets = [ 813 | parseInt(match[2]), 814 | parseInt(match[3]), 815 | parseInt(match[4]), 816 | parseInt(match[5]) 817 | ]; 818 | for (i = 0; i < octets.length; i++) { 819 | octet = octets[i]; 820 | if (!((0 <= octet && octet <= 255))) { 821 | return null; 822 | } 823 | } 824 | 825 | addr.parts.push(octets[0] << 8 | octets[1]); 826 | addr.parts.push(octets[2] << 8 | octets[3]); 827 | return { 828 | parts: addr.parts, 829 | zoneId: addr.zoneId 830 | }; 831 | } 832 | } 833 | 834 | return null; 835 | }; 836 | 837 | // Try to parse an array in network order (MSB first) for IPv4 and IPv6 838 | ipaddr.fromByteArray = function (bytes) { 839 | const length = bytes.length; 840 | 841 | if (length === 4) { 842 | return new ipaddr.IPv4(bytes); 843 | } else if (length === 16) { 844 | return new ipaddr.IPv6(bytes); 845 | } else { 846 | throw new Error('ipaddr: the binary input is neither an IPv6 nor IPv4 address'); 847 | } 848 | }; 849 | 850 | // Checks if the address is valid IP address 851 | ipaddr.isValid = function (string) { 852 | return ipaddr.IPv6.isValid(string) || ipaddr.IPv4.isValid(string); 853 | }; 854 | 855 | // Attempts to parse an IP Address, first through IPv6 then IPv4. 856 | // Throws an error if it could not be parsed. 857 | ipaddr.parse = function (string) { 858 | if (ipaddr.IPv6.isValid(string)) { 859 | return ipaddr.IPv6.parse(string); 860 | } else if (ipaddr.IPv4.isValid(string)) { 861 | return ipaddr.IPv4.parse(string); 862 | } else { 863 | throw new Error('ipaddr: the address has neither IPv6 nor IPv4 format'); 864 | } 865 | }; 866 | 867 | // Attempt to parse CIDR notation, first through IPv6 then IPv4. 868 | // Throws an error if it could not be parsed. 869 | ipaddr.parseCIDR = function (string) { 870 | try { 871 | return ipaddr.IPv6.parseCIDR(string); 872 | } catch (e) { 873 | try { 874 | return ipaddr.IPv4.parseCIDR(string); 875 | } catch (e2) { 876 | throw new Error('ipaddr: the address has neither IPv6 nor IPv4 CIDR format'); 877 | } 878 | } 879 | }; 880 | 881 | // Parse an address and return plain IPv4 address if it is an IPv4-mapped address 882 | ipaddr.process = function (string) { 883 | const addr = this.parse(string); 884 | 885 | if (addr.kind() === 'ipv6' && addr.isIPv4MappedAddress()) { 886 | return addr.toIPv4Address(); 887 | } else { 888 | return addr; 889 | } 890 | }; 891 | 892 | // An utility function to ease named range matching. See examples below. 893 | // rangeList can contain both IPv4 and IPv6 subnet entries and will not throw errors 894 | // on matching IPv4 addresses to IPv6 ranges or vice versa. 895 | ipaddr.subnetMatch = function (address, rangeList, defaultName) { 896 | let i, rangeName, rangeSubnets, subnet; 897 | 898 | if (defaultName === undefined || defaultName === null) { 899 | defaultName = 'unicast'; 900 | } 901 | 902 | for (rangeName in rangeList) { 903 | if (Object.prototype.hasOwnProperty.call(rangeList, rangeName)) { 904 | rangeSubnets = rangeList[rangeName]; 905 | // ECMA5 Array.isArray isn't available everywhere 906 | if (rangeSubnets[0] && !(rangeSubnets[0] instanceof Array)) { 907 | rangeSubnets = [rangeSubnets]; 908 | } 909 | 910 | for (i = 0; i < rangeSubnets.length; i++) { 911 | subnet = rangeSubnets[i]; 912 | if (address.kind() === subnet[0].kind() && address.match.apply(address, subnet)) { 913 | return rangeName; 914 | } 915 | } 916 | } 917 | } 918 | 919 | return defaultName; 920 | }; 921 | 922 | // Export for both the CommonJS and browser-like environment 923 | if (typeof module !== 'undefined' && module.exports) { 924 | module.exports = ipaddr; 925 | 926 | } else { 927 | root.ipaddr = ipaddr; 928 | } 929 | 930 | }(this)); 931 | -------------------------------------------------------------------------------- /examples/nodeUdpConn.js: -------------------------------------------------------------------------------- 1 | var udp = require('dgram'); 2 | var ipaddr = require('./ipaddr'); 3 | 4 | class NodeUDPConn { 5 | constructor() { 6 | this._socket = udp.createSocket('udp4'); 7 | } 8 | connect(addr) { 9 | var ipAddr = ipaddr.fromByteArray(addr.ip); 10 | var ip = ipAddr.toString(); 11 | // var ip = Utils.IPV4AddressToString(addr); 12 | console.log('ip address', ip); 13 | this._socket.connect(addr.port, ip); 14 | } 15 | bind(addr) { 16 | var ipAddr = ipaddr.fromByteArray(addr.ip); 17 | var ip = ipAddr.toString(); 18 | console.log('bind to', ip, addr.port); 19 | this._socket.bind(addr.port, ip); 20 | this._binded = true; 21 | } 22 | send(b) { 23 | this._socket.send(b); 24 | } 25 | sendTo(b, to) { 26 | var ipAddr = ipaddr.fromByteArray(addr.ip); 27 | var ip = ipAddr.toString(); 28 | this._socket.send(b, 0, b.length, to.port, ip); 29 | } 30 | close() { 31 | this._socket.close; 32 | } 33 | setReadBuffer(size) { 34 | if (this._binded && this._socket) { 35 | // this._socket.setRecvBufferSize(size); 36 | } 37 | } 38 | setWriteBuffer(size) { 39 | if (this._binded && this._socket) { 40 | // this._socket.setWriteBufferSize(size); 41 | } 42 | } 43 | onMessage(callback) { 44 | if (!this._strAddressToBytes) { 45 | this._strAddressToBytes = {}; 46 | } 47 | this._socket.on('message', (msg, remote) => { 48 | const fullAddr = remote.address + ':' + remote.port; 49 | let addr = this._strAddressToBytes[fullAddr]; 50 | if (!addr) { 51 | const ipAddr = ipaddr.parse(remote.address); 52 | addr = this._strAddressToBytes[fullAddr] = new Uint8Array( 53 | ipAddr.toByteArray() 54 | ); 55 | // addr = this._strAddressToBytes[fullAddr] = Utils.stringToIPV4Address(fullAddr); 56 | } 57 | callback(msg, addr); 58 | }); 59 | } 60 | } 61 | 62 | this.createUdpConn = function (addr) { 63 | return new NodeUDPConn(); 64 | }; 65 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | var { Netcode } = require('../dist/node/netcode'); 2 | var { createUdpConn } = require('./nodeUdpConn'); 3 | 4 | var PROTOCOL_ID = 0x1122334455667788; 5 | 6 | // obviously you'd generate this outside of both web server and game server and store it in something 7 | // like hashicorp vault to securely retrieve 8 | var serverKey = new Uint8Array([ 9 | 0x60, 10 | 0x6a, 11 | 0xbe, 12 | 0x6e, 13 | 0xc9, 14 | 0x19, 15 | 0x10, 16 | 0xea, 17 | 0x9a, 18 | 0x65, 19 | 0x62, 20 | 0xf6, 21 | 0x6f, 22 | 0x2b, 23 | 0x30, 24 | 0xe4, 25 | 0x43, 26 | 0x71, 27 | 0xd6, 28 | 0x2c, 29 | 0xd1, 30 | 0x99, 31 | 0x27, 32 | 0x26, 33 | 0x6b, 34 | 0x3c, 35 | 0x60, 36 | 0xf4, 37 | 0xb7, 38 | 0x15, 39 | 0xab, 40 | 0xa1, 41 | ]); 42 | 43 | function main() { 44 | const server = new Netcode.Server( 45 | { ip: Netcode.Utils.stringToIPV4Address('127.0.0.1').ip, port: 40000 }, 46 | serverKey, 47 | PROTOCOL_ID, 48 | 255 49 | ); 50 | server.listen(createUdpConn); 51 | 52 | var serverTime = 0; 53 | var delta = 10; 54 | setInterval(() => { 55 | server.update(serverTime); 56 | for (var i = 0; i < server.getMaxClients(); i += 1) { 57 | while (true) { 58 | var responsePayload = server.recvPayload(i); 59 | if (!responsePayload) { 60 | break; 61 | } 62 | console.log( 63 | 'server got payload', 64 | responsePayload.data, 65 | 'with sequence', 66 | responsePayload.sequence 67 | ); 68 | } 69 | } 70 | // do simulation/process payload packets 71 | // send payloads to clients 72 | server.sendPayloads(new Uint8Array([0, 0])); 73 | serverTime += delta / 1000; 74 | }, delta); 75 | } 76 | 77 | main(); 78 | -------------------------------------------------------------------------------- /libs/chacha20poly1305.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // code from https://gist.github.com/rumkin/e852eb12fe11281a5738c0a8abaf5e1a 3 | 4 | /* chacha20 - 256 bits */ 5 | 6 | // Written in 2014 by Devi Mandiri. Public domain. 7 | // 8 | // Implementation derived from chacha-ref.c version 20080118 9 | // See for details: http://cr.yp.to/chacha/chacha-20080128.pdf 10 | 11 | function U8TO32_LE(x, i) { 12 | return x[i] | (x[i + 1] << 8) | (x[i + 2] << 16) | (x[i + 3] << 24); 13 | } 14 | 15 | function U32TO8_LE(x, i, u) { 16 | x[i] = u; 17 | u >>>= 8; 18 | x[i + 1] = u; 19 | u >>>= 8; 20 | x[i + 2] = u; 21 | u >>>= 8; 22 | x[i + 3] = u; 23 | } 24 | 25 | function ROTATE(v, c) { 26 | return (v << c) | (v >>> (32 - c)); 27 | } 28 | 29 | function Chacha20(key, nonce, counter) { 30 | this.input = new Uint32Array(16); 31 | 32 | // https://tools.ietf.org/html/draft-irtf-cfrg-chacha20-poly1305-01#section-2.3 33 | this.input[0] = 1634760805; 34 | this.input[1] = 857760878; 35 | this.input[2] = 2036477234; 36 | this.input[3] = 1797285236; 37 | this.input[4] = U8TO32_LE(key, 0); 38 | this.input[5] = U8TO32_LE(key, 4); 39 | this.input[6] = U8TO32_LE(key, 8); 40 | this.input[7] = U8TO32_LE(key, 12); 41 | this.input[8] = U8TO32_LE(key, 16); 42 | this.input[9] = U8TO32_LE(key, 20); 43 | this.input[10] = U8TO32_LE(key, 24); 44 | this.input[11] = U8TO32_LE(key, 28); 45 | this.input[12] = counter; 46 | this.input[13] = U8TO32_LE(nonce, 0); 47 | this.input[14] = U8TO32_LE(nonce, 4); 48 | this.input[15] = U8TO32_LE(nonce, 8); 49 | } 50 | this.Chacha20 = Chacha20; 51 | 52 | Chacha20.prototype.quarterRound = function (x, a, b, c, d) { 53 | x[a] += x[b]; 54 | x[d] = ROTATE(x[d] ^ x[a], 16); 55 | x[c] += x[d]; 56 | x[b] = ROTATE(x[b] ^ x[c], 12); 57 | x[a] += x[b]; 58 | x[d] = ROTATE(x[d] ^ x[a], 8); 59 | x[c] += x[d]; 60 | x[b] = ROTATE(x[b] ^ x[c], 7); 61 | }; 62 | 63 | Chacha20.prototype.encrypt = function (dst, src, len) { 64 | var x = new Uint32Array(16); 65 | var output = new Uint8Array(64); 66 | var i, 67 | dpos = 0, 68 | spos = 0; 69 | 70 | while (len > 0) { 71 | for (i = 16; i--; ) x[i] = this.input[i]; 72 | for (i = 20; i > 0; i -= 2) { 73 | this.quarterRound(x, 0, 4, 8, 12); 74 | this.quarterRound(x, 1, 5, 9, 13); 75 | this.quarterRound(x, 2, 6, 10, 14); 76 | this.quarterRound(x, 3, 7, 11, 15); 77 | this.quarterRound(x, 0, 5, 10, 15); 78 | this.quarterRound(x, 1, 6, 11, 12); 79 | this.quarterRound(x, 2, 7, 8, 13); 80 | this.quarterRound(x, 3, 4, 9, 14); 81 | } 82 | for (i = 16; i--; ) x[i] += this.input[i]; 83 | for (i = 16; i--; ) U32TO8_LE(output, 4 * i, x[i]); 84 | 85 | this.input[12] += 1; 86 | if (!this.input[12]) { 87 | this.input[13] += 1; 88 | } 89 | if (len <= 64) { 90 | for (i = len; i--; ) { 91 | dst[i + dpos] = src[i + spos] ^ output[i]; 92 | } 93 | return; 94 | } 95 | for (i = 64; i--; ) { 96 | dst[i + dpos] = src[i + spos] ^ output[i]; 97 | } 98 | len -= 64; 99 | spos += 64; 100 | dpos += 64; 101 | } 102 | }; 103 | 104 | Chacha20.prototype.keystream = function (dst, len) { 105 | for (var i = 0; i < len; ++i) dst[i] = 0; 106 | this.encrypt(dst, dst, len); 107 | }; 108 | 109 | /* poly1305 */ 110 | 111 | // Written in 2014 by Devi Mandiri. Public domain. 112 | // 113 | // Implementation derived from poly1305-donna-16.h 114 | // See for details: https://github.com/floodyberry/poly1305-donna 115 | 116 | const Poly1305KeySize = 32; 117 | const Poly1305TagSize = 16; 118 | 119 | function Poly1305(key) { 120 | this.buffer = new Uint8Array(16); 121 | this.leftover = 0; 122 | this.r = new Uint16Array(10); 123 | this.h = new Uint16Array(10); 124 | this.pad = new Uint16Array(8); 125 | this.finished = 0; 126 | 127 | var t = new Uint16Array(8), 128 | i; 129 | 130 | for (i = 8; i--; ) t[i] = U8TO16_LE(key, i * 2); 131 | 132 | this.r[0] = t[0] & 0x1fff; 133 | this.r[1] = ((t[0] >>> 13) | (t[1] << 3)) & 0x1fff; 134 | this.r[2] = ((t[1] >>> 10) | (t[2] << 6)) & 0x1f03; 135 | this.r[3] = ((t[2] >>> 7) | (t[3] << 9)) & 0x1fff; 136 | this.r[4] = ((t[3] >>> 4) | (t[4] << 12)) & 0x00ff; 137 | this.r[5] = (t[4] >>> 1) & 0x1ffe; 138 | this.r[6] = ((t[4] >>> 14) | (t[5] << 2)) & 0x1fff; 139 | this.r[7] = ((t[5] >>> 11) | (t[6] << 5)) & 0x1f81; 140 | this.r[8] = ((t[6] >>> 8) | (t[7] << 8)) & 0x1fff; 141 | this.r[9] = (t[7] >>> 5) & 0x007f; 142 | 143 | for (i = 8; i--; ) { 144 | this.h[i] = 0; 145 | this.pad[i] = U8TO16_LE(key, 16 + 2 * i); 146 | } 147 | this.h[8] = 0; 148 | this.h[9] = 0; 149 | this.leftover = 0; 150 | this.finished = 0; 151 | } 152 | 153 | function U8TO16_LE(p, pos) { 154 | return (p[pos] & 0xff) | ((p[pos + 1] & 0xff) << 8); 155 | } 156 | 157 | function U16TO8_LE(p, pos, v) { 158 | p[pos] = v; 159 | p[pos + 1] = v >>> 8; 160 | } 161 | 162 | Poly1305.prototype.blocks = function (m, mpos, bytes) { 163 | var hibit = this.finished ? 0 : 1 << 11; 164 | var t = new Uint16Array(8), 165 | d = new Uint32Array(10), 166 | c = 0, 167 | i = 0, 168 | j = 0; 169 | 170 | while (bytes >= 16) { 171 | for (i = 8; i--; ) t[i] = U8TO16_LE(m, i * 2 + mpos); 172 | 173 | this.h[0] += t[0] & 0x1fff; 174 | this.h[1] += ((t[0] >>> 13) | (t[1] << 3)) & 0x1fff; 175 | this.h[2] += ((t[1] >>> 10) | (t[2] << 6)) & 0x1fff; 176 | this.h[3] += ((t[2] >>> 7) | (t[3] << 9)) & 0x1fff; 177 | this.h[4] += ((t[3] >>> 4) | (t[4] << 12)) & 0x1fff; 178 | this.h[5] += (t[4] >>> 1) & 0x1fff; 179 | this.h[6] += ((t[4] >>> 14) | (t[5] << 2)) & 0x1fff; 180 | this.h[7] += ((t[5] >>> 11) | (t[6] << 5)) & 0x1fff; 181 | this.h[8] += ((t[6] >>> 8) | (t[7] << 8)) & 0x1fff; 182 | this.h[9] += (t[7] >>> 5) | hibit; 183 | 184 | for (i = 0, c = 0; i < 10; i++) { 185 | d[i] = c; 186 | for (j = 0; j < 10; j++) { 187 | d[i] += 188 | (this.h[j] & 0xffffffff) * 189 | (j <= i ? this.r[i - j] : 5 * this.r[i + 10 - j]); 190 | if (j === 4) { 191 | c = d[i] >>> 13; 192 | d[i] &= 0x1fff; 193 | } 194 | } 195 | c += d[i] >>> 13; 196 | d[i] &= 0x1fff; 197 | } 198 | c = (c << 2) + c; 199 | c += d[0]; 200 | d[0] = c & 0xffff & 0x1fff; 201 | c = c >>> 13; 202 | d[1] += c; 203 | 204 | for (i = 10; i--; ) this.h[i] = d[i]; 205 | 206 | mpos += 16; 207 | bytes -= 16; 208 | } 209 | }; 210 | 211 | Poly1305.prototype.update = function (m, bytes) { 212 | var want = 0, 213 | i = 0, 214 | mpos = 0; 215 | 216 | if (this.leftover) { 217 | want = 16 - this.leftover; 218 | if (want > bytes) want = bytes; 219 | for (i = want; i--; ) { 220 | this.buffer[this.leftover + i] = m[i + mpos]; 221 | } 222 | bytes -= want; 223 | mpos += want; 224 | this.leftover += want; 225 | if (this.leftover < 16) return; 226 | this.blocks(this.buffer, 0, 16); 227 | this.leftover = 0; 228 | } 229 | 230 | if (bytes >= 16) { 231 | want = bytes & ~(16 - 1); 232 | this.blocks(m, mpos, want); 233 | mpos += want; 234 | bytes -= want; 235 | } 236 | 237 | if (bytes) { 238 | for (i = bytes; i--; ) { 239 | this.buffer[this.leftover + i] = m[i + mpos]; 240 | } 241 | this.leftover += bytes; 242 | } 243 | }; 244 | 245 | Poly1305.prototype.finish = function () { 246 | var mac = new Uint8Array(16), 247 | g = new Uint16Array(10), 248 | c = 0, 249 | mask = 0, 250 | f = 0, 251 | i = 0; 252 | 253 | if (this.leftover) { 254 | i = this.leftover; 255 | this.buffer[i++] = 1; 256 | for (; i < 16; i++) { 257 | this.buffer[i] = 0; 258 | } 259 | this.finished = 1; 260 | this.blocks(this.buffer, 0, 16); 261 | } 262 | 263 | c = this.h[1] >>> 13; 264 | this.h[1] &= 0x1fff; 265 | for (i = 2; i < 10; i++) { 266 | this.h[i] += c; 267 | c = this.h[i] >>> 13; 268 | this.h[i] &= 0x1fff; 269 | } 270 | this.h[0] += c * 5; 271 | c = this.h[0] >>> 13; 272 | this.h[0] &= 0x1fff; 273 | this.h[1] += c; 274 | c = this.h[1] >>> 13; 275 | this.h[1] &= 0x1fff; 276 | this.h[2] += c; 277 | 278 | g[0] = this.h[0] + 5; 279 | c = g[0] >>> 13; 280 | g[0] &= 0x1fff; 281 | for (i = 1; i < 10; i++) { 282 | g[i] = this.h[i] + c; 283 | c = g[i] >>> 13; 284 | g[i] &= 0x1fff; 285 | } 286 | g[9] -= 1 << 13; 287 | 288 | mask = (g[9] >>> 15) - 1; 289 | for (i = 10; i--; ) g[i] &= mask; 290 | mask = ~mask; 291 | for (i = 10; i--; ) { 292 | this.h[i] = (this.h[i] & mask) | g[i]; 293 | } 294 | 295 | this.h[0] = this.h[0] | (this.h[1] << 13); 296 | this.h[1] = (this.h[1] >> 3) | (this.h[2] << 10); 297 | this.h[2] = (this.h[2] >> 6) | (this.h[3] << 7); 298 | this.h[3] = (this.h[3] >> 9) | (this.h[4] << 4); 299 | this.h[4] = (this.h[4] >> 12) | (this.h[5] << 1) | (this.h[6] << 14); 300 | this.h[5] = (this.h[6] >> 2) | (this.h[7] << 11); 301 | this.h[6] = (this.h[7] >> 5) | (this.h[8] << 8); 302 | this.h[7] = (this.h[8] >> 8) | (this.h[9] << 5); 303 | 304 | f = (this.h[0] & 0xffffffff) + this.pad[0]; 305 | this.h[0] = f; 306 | for (i = 1; i < 8; i++) { 307 | f = (this.h[i] & 0xffffffff) + this.pad[i] + (f >>> 16); 308 | this.h[i] = f; 309 | } 310 | 311 | for (i = 8; i--; ) { 312 | U16TO8_LE(mac, i * 2, this.h[i]); 313 | this.pad[i] = 0; 314 | } 315 | for (i = 10; i--; ) { 316 | this.h[i] = 0; 317 | this.r[i] = 0; 318 | } 319 | 320 | return mac; 321 | }; 322 | 323 | function poly1305_auth(m, bytes, key) { 324 | var ctx = new Poly1305(key); 325 | ctx.update(m, bytes); 326 | return ctx.finish(); 327 | } 328 | 329 | function poly1305_verify(mac1, mac2) { 330 | var dif = 0; 331 | for (var i = 0; i < 16; i++) { 332 | dif |= mac1[i] ^ mac2[i]; 333 | } 334 | dif = (dif - 1) >>> 31; 335 | return dif & 1; 336 | } 337 | 338 | /* chacha20poly1305 AEAD */ 339 | 340 | // Written in 2014 by Devi Mandiri. Public domain. 341 | 342 | function store64(dst, num) { 343 | var hi = 0, 344 | lo = num >>> 0; 345 | if (+Math.abs(num) >= 1) { 346 | if (num > 0) { 347 | hi = (Math.min(+Math.floor(num / 4294967296), 4294967295) | 0) >>> 0; 348 | } else { 349 | hi = ~~+Math.ceil((num - +(~~num >>> 0)) / 4294967296) >>> 0; 350 | } 351 | } 352 | dst.push(lo & 0xff); 353 | lo >>>= 8; 354 | dst.push(lo & 0xff); 355 | lo >>>= 8; 356 | dst.push(lo & 0xff); 357 | lo >>>= 8; 358 | dst.push(lo & 0xff); 359 | dst.push(hi & 0xff); 360 | hi >>>= 8; 361 | dst.push(hi & 0xff); 362 | hi >>>= 8; 363 | dst.push(hi & 0xff); 364 | hi >>>= 8; 365 | dst.push(hi & 0xff); 366 | } 367 | 368 | function aead_mac(polykey, data, ciphertext) { 369 | var dlen = data.length, 370 | clen = ciphertext.length, 371 | dpad = dlen % 16, 372 | cpad = clen % 16, 373 | m = [], 374 | i; 375 | 376 | for (i = 0; i < dlen; i++) m.push(data[i]); 377 | 378 | if (dpad !== 0) { 379 | for (i = 16 - dpad; i--; ) m.push(0); 380 | } 381 | 382 | for (i = 0; i < clen; i++) m.push(ciphertext[i]); 383 | 384 | if (cpad !== 0) { 385 | for (i = 16 - cpad; i--; ) m.push(0); 386 | } 387 | 388 | store64(m, dlen); 389 | store64(m, clen); 390 | 391 | return poly1305_auth(m, m.length, polykey); 392 | } 393 | 394 | var g_tempBuf; 395 | var g_textBuf; 396 | var g_polyKeyBuf; 397 | this.aead_encrypt = function (key, nonce, plaintext, data) { 398 | if (!g_tempBuf) { 399 | g_tempBuf = new Uint8Array(2048); 400 | } 401 | if (!g_textBuf) { 402 | g_textBuf = new Uint8Array(2048); 403 | } 404 | if (!g_polyKeyBuf) { 405 | g_polyKeyBuf = new Uint8Array(64); 406 | } 407 | var plen = plaintext.length; 408 | var buf = g_tempBuf.subarray(0, plen); 409 | var ciphertext = g_textBuf.subarray(0, plen); 410 | var polykey = g_polyKeyBuf; 411 | var ctx = new Chacha20(key, nonce, 0); 412 | 413 | ctx.keystream(polykey, 64); 414 | 415 | ctx.keystream(buf, plen); 416 | 417 | for (var i = 0; i < plen; i++) { 418 | ciphertext[i] = buf[i] ^ plaintext[i]; 419 | } 420 | 421 | return [ciphertext, aead_mac(polykey, data, ciphertext)]; 422 | }; 423 | 424 | this.aead_decrypt = function (key, nonce, ciphertext, data, mac) { 425 | if (!g_tempBuf) { 426 | g_tempBuf = new Uint8Array(2048); 427 | } 428 | if (!g_textBuf) { 429 | g_textBuf = new Uint8Array(2048); 430 | } 431 | if (!g_polyKeyBuf) { 432 | g_polyKeyBuf = new Uint8Array(64); 433 | } 434 | var plen = ciphertext.length; 435 | var buf = g_tempBuf.subarray(0, plen); 436 | var plaintext = g_textBuf.subarray(0, plen); 437 | var polykey = g_polyKeyBuf; 438 | var ctx = new Chacha20(key, nonce, 0); 439 | 440 | ctx.keystream(polykey, 64); 441 | 442 | var tag = aead_mac(polykey, data, ciphertext); 443 | 444 | if (poly1305_verify(tag, mac) !== 1) return false; 445 | 446 | ctx.keystream(buf, plen); 447 | 448 | for (var i = 0; i < plen; i++) { 449 | plaintext[i] = buf[i] ^ ciphertext[i]; 450 | } 451 | return plaintext; 452 | }; 453 | 454 | this.getRandomBytes = ( 455 | typeof self !== 'undefined' && (self.crypto || self.msCrypto) 456 | ? function () { 457 | // Browsers 458 | var crypto = self.crypto || self.msCrypto, 459 | QUOTA = 65536; 460 | return function (n) { 461 | var a = new Uint8Array(n); 462 | for (var i = 0; i < n; i += QUOTA) { 463 | crypto.getRandomValues(a.subarray(i, i + Math.min(n - i, QUOTA))); 464 | } 465 | return a; 466 | }; 467 | } 468 | : function () { 469 | // Node 470 | return require('crypto').randomBytes; 471 | } 472 | )(); 473 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netcodeio", 3 | "version": "1.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "bin": { 7 | "netcodeio": "netcodeio.js" 8 | }, 9 | "directories": { 10 | "test": "tests" 11 | }, 12 | "scripts": { 13 | "test": "mocha ./tests/*_test.js", 14 | "build": "tsc & node build.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/bennychen/netcode.io-typescript.git" 19 | }, 20 | "author": "Benny Chen", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/bennychen/netcode.io-typescript/issues" 24 | }, 25 | "homepage": "https://github.com/bennychen/netcode.io-typescript#readme", 26 | "devDependencies": {}, 27 | "dependencies": {} 28 | } 29 | -------------------------------------------------------------------------------- /src/ByteBuffer.ts: -------------------------------------------------------------------------------- 1 | namespace Netcode { 2 | export class Long { 3 | public static readonly ZERO = new Long(0, 0); 4 | 5 | public static fromNumber(value: number): Long { 6 | if (value === 0) { 7 | return new Long(0, 0); 8 | } 9 | var sign = value < 0; 10 | if (sign) value = -value; 11 | var lo = value >>> 0, 12 | hi = ((value - lo) / 4294967296) >>> 0; 13 | if (sign) { 14 | hi = ~hi >>> 0; 15 | lo = ~lo >>> 0; 16 | if (++lo > 4294967295) { 17 | lo = 0; 18 | if (++hi > 4294967295) hi = 0; 19 | } 20 | } 21 | return new Long(lo, hi); 22 | } 23 | 24 | public get low(): number { 25 | return this._low; 26 | } 27 | 28 | public get high(): number { 29 | return this._high; 30 | } 31 | 32 | public set low(value: number) { 33 | this._low = value; 34 | } 35 | 36 | public set high(value: number) { 37 | this._high = value; 38 | } 39 | 40 | public constructor(low: number, high: number) { 41 | this._low = low | 0; 42 | this._high = high | 0; 43 | } 44 | 45 | public toNumber(): number { 46 | return (this._low >>> 0) + this._high * 0x100000000; 47 | } 48 | 49 | public equals(other: Long) { 50 | return this._low == other._low && this._high == other._high; 51 | } 52 | 53 | public clone(): Long { 54 | return new Long(this.low, this.high); 55 | } 56 | 57 | public plusOne() { 58 | if (this._low === 0xffffffff) { 59 | this._low = 0; 60 | this._high++; 61 | } else { 62 | this._low++; 63 | } 64 | } 65 | 66 | public leftShiftSelf(s: number) { 67 | const { low, high } = this.leftShift(s); 68 | this._low = low; 69 | this._high = high; 70 | } 71 | 72 | public leftShift(s: number): { low: number; high: number } { 73 | if (s === 0 || s >= 64) { 74 | return { high: this._high, low: this._low }; 75 | } else if (s < 32) { 76 | return { 77 | high: (this._low >>> (32 - s)) | (this._high << s), 78 | low: (this._low << s) | 0, 79 | }; 80 | } else if (s < 64) { 81 | return { 82 | high: (this._low << (s - 32)) | 0, 83 | low: 0, 84 | }; 85 | } 86 | } 87 | 88 | public rightShiftSelf(s: number) { 89 | const { low, high } = this.rightShift(s); 90 | this._low = low; 91 | this._high = high; 92 | } 93 | 94 | public rightShift(s: number): { low: number; high: number } { 95 | if (s === 0 || s >= 64) { 96 | return { high: this._high, low: this._low }; 97 | } else if (s < 32) { 98 | return { 99 | high: (this._high >>> s) | 0, 100 | low: (this._high << (32 - s)) | (this._low >>> s), 101 | }; 102 | } else if (s < 64) { 103 | return { 104 | high: 0, 105 | low: (this._high >>> (s - 32)) | 0, 106 | }; 107 | } 108 | } 109 | 110 | public setZero() { 111 | this._high = 0; 112 | this._low = 0; 113 | } 114 | 115 | private _low: number; 116 | private _high: number; 117 | } 118 | 119 | export class ByteBuffer { 120 | public static allocate(byte_size: number): ByteBuffer { 121 | return new ByteBuffer(new Uint8Array(byte_size)); 122 | } 123 | 124 | public constructor(bytes: Uint8Array) { 125 | if (!(bytes instanceof Uint8Array)) { 126 | throw new Error('bytes must be a Uint8Array'); 127 | } 128 | this._bytes = bytes; 129 | this._position = 0; 130 | } 131 | 132 | public get bytes(): Uint8Array { 133 | return this._bytes; 134 | } 135 | 136 | public get position(): number { 137 | return this._position; 138 | } 139 | 140 | public get capacity(): number { 141 | return this._bytes.length; 142 | } 143 | 144 | public skipPosition(value: number) { 145 | this._position += value; 146 | } 147 | 148 | public clearPosition() { 149 | this._position = 0; 150 | } 151 | 152 | public readInt8(): number | undefined { 153 | const n = this.readUint8(); 154 | if (n !== undefined) { 155 | return (n << 24) >> 24; 156 | } 157 | } 158 | 159 | public readUint8(): number | undefined { 160 | if (this.assertPosition(1)) { 161 | return this._bytes[this._position++]; 162 | } 163 | } 164 | 165 | public readInt16(): number | undefined { 166 | const n = this.readUint16(); 167 | if (n !== undefined) { 168 | return (n << 16) >> 16; 169 | } 170 | } 171 | 172 | public readUint16(): number | undefined { 173 | if (this.assertPosition(2)) { 174 | const n = 175 | this._bytes[this._position] | (this._bytes[this._position + 1] << 8); 176 | this._position += 2; 177 | return n; 178 | } 179 | } 180 | 181 | public readInt32(): number | undefined { 182 | if (this.assertPosition(4)) { 183 | const n = 184 | this._bytes[this._position] | 185 | (this._bytes[this._position + 1] << 8) | 186 | (this._bytes[this._position + 2] << 16) | 187 | (this._bytes[this._position + 3] << 24); 188 | this._position += 4; 189 | return n; 190 | } 191 | } 192 | 193 | public readUint32(): number | undefined { 194 | const n = this.readInt32(); 195 | if (n !== undefined) { 196 | return n >>> 0; 197 | } 198 | } 199 | 200 | public readInt64(): Long | undefined { 201 | const low = this.readInt32(); 202 | const high = this.readInt32(); 203 | if (low !== undefined && high !== undefined) { 204 | return new Long(low, high); 205 | } 206 | } 207 | 208 | public readUint64(): Long | undefined { 209 | const low = this.readUint32(); 210 | const high = this.readUint32(); 211 | if (low !== undefined && high !== undefined) { 212 | return new Long(low, high); 213 | } 214 | } 215 | 216 | public readBytes(length: number): Uint8Array | undefined { 217 | if (this.assertPosition(length)) { 218 | const ret = this._bytes.slice(this._position, this._position + length); 219 | this._position += length; 220 | return ret; 221 | } 222 | } 223 | 224 | public writeInt8(value: number) { 225 | this._bytes[this._position] = value; 226 | this._position += 1; 227 | } 228 | 229 | public writeUint8(value: number) { 230 | this._bytes[this._position] = value; 231 | this._position += 1; 232 | } 233 | 234 | public writeInt16(value: number) { 235 | this._bytes[this._position] = value; 236 | this._bytes[this._position + 1] = value >> 8; 237 | this._position += 2; 238 | } 239 | 240 | public writeUint16(value: number) { 241 | this._bytes[this._position] = value; 242 | this._bytes[this._position + 1] = value >> 8; 243 | this._position += 2; 244 | } 245 | 246 | public writeInt32(value: number) { 247 | this._bytes[this._position] = value; 248 | this._bytes[this._position + 1] = value >> 8; 249 | this._bytes[this._position + 2] = value >> 16; 250 | this._bytes[this._position + 3] = value >> 24; 251 | this._position += 4; 252 | } 253 | 254 | public writeUint32(value: number) { 255 | this._bytes[this._position] = value; 256 | this._bytes[this._position + 1] = value >> 8; 257 | this._bytes[this._position + 2] = value >> 16; 258 | this._bytes[this._position + 3] = value >> 24; 259 | this._position += 4; 260 | } 261 | 262 | public writeInt64(value: Long) { 263 | this.writeInt32(value.low); 264 | this.writeInt32(value.high); 265 | } 266 | 267 | public writeUint64(value: Long) { 268 | this.writeUint32(value.low); 269 | this.writeUint32(value.high); 270 | } 271 | 272 | public writeBytes(bytes: Uint8Array, length?: number) { 273 | if (length === undefined) { 274 | length = bytes.length; 275 | } 276 | for (let i = 0; i < length; i++) { 277 | this.writeUint8(bytes[i]); 278 | } 279 | } 280 | 281 | private assertPosition(length: number): boolean { 282 | const bufferLength = this._bytes.length; 283 | const bufferWindow = this._position + length; 284 | if (bufferWindow > bufferLength) { 285 | console.error('buffer out of bounds', bufferLength, bufferWindow); 286 | return false; 287 | } 288 | return true; 289 | } 290 | 291 | private _bytes: Uint8Array; 292 | private _position: number = 0; 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/Defines.ts: -------------------------------------------------------------------------------- 1 | namespace Netcode { 2 | export const CONNECT_TOKEN_PRIVATE_BYTES = 1024; 3 | export const CHALLENGE_TOKEN_BYTES = 300; 4 | export const VERSION_INFO_BYTES = 13; 5 | export const USER_DATA_BYTES = 256; 6 | export const MAX_PACKET_BYTES = 1220; 7 | export const MAX_PAYLOAD_BYTES = 1200; 8 | export const MAX_ADDRESS_STRING_LENGTH = 256; 9 | 10 | export const KEY_BYTES = 32; 11 | export const MAC_BYTES = 16; 12 | export const NONCE_BYTES = 8; 13 | export const MAX_SERVERS_PER_CONNECT = 32; 14 | 15 | export const VERSION_INFO = 'NETCODE 1.01\x00'; 16 | export const VERSION_INFO_BYTES_ARRAY = new Uint8Array([ 17 | 78, 18 | 69, 19 | 84, 20 | 67, 21 | 79, 22 | 68, 23 | 69, 24 | 32, 25 | 49, 26 | 46, 27 | 48, 28 | 49, 29 | 0, 30 | ]); 31 | 32 | export const CONNECT_TOKEN_BYTES = 2048; 33 | 34 | export const PACKET_QUEUE_SIZE = 256; 35 | 36 | export enum AddressType { 37 | none, 38 | ipv4, 39 | ipv6, 40 | } 41 | 42 | export interface IUDPAddr { 43 | ip: Uint8Array; 44 | port: number; 45 | isIPV6?: boolean; 46 | } 47 | 48 | export type onMessageHandler = ( 49 | message: Uint8Array, 50 | remote: IUDPAddr 51 | ) => void; 52 | 53 | export interface IUDPConn { 54 | connect(addr: IUDPAddr); 55 | bind(addr: IUDPAddr); 56 | send(b: Uint8Array); 57 | sendTo(b: Uint8Array, addr: IUDPAddr); 58 | close(); 59 | setReadBuffer(size: number); 60 | setWriteBuffer(size: number); 61 | onMessage(callback: onMessageHandler); 62 | } 63 | } 64 | 65 | this.Netcode = Netcode; 66 | -------------------------------------------------------------------------------- /src/Errors.ts: -------------------------------------------------------------------------------- 1 | namespace Netcode { 2 | export enum Errors { 3 | none, 4 | EOF, 5 | 6 | invalidPacket, 7 | packetTypeNotAllowed, 8 | badPacketLength, 9 | packetAlreadyReceived, 10 | 11 | emptyServer, 12 | tooManyServers, 13 | unknownIPAddressType, 14 | invalidPort, 15 | 16 | noPrivateKey, 17 | emptyPacketKey, 18 | badVersionInfo, 19 | badProtocolID, 20 | invalidClientID, 21 | connectTokenExpired, 22 | badCreateTimestamp, 23 | badExpireTimestamp, 24 | packetInvalidLength, 25 | decryptPrivateTokenData, 26 | readPrivateTokenData, 27 | badSequence, 28 | badPrivateData, 29 | badUserData, 30 | 31 | invalidHandler, 32 | readUDPError, 33 | socketZeroRecv, 34 | overMaxReadSize, 35 | exceededServerNumber, 36 | dialServer, 37 | errDecryptData, 38 | 39 | invalidDenyPacketDataSize, 40 | invalidChallengePacketDataSize, 41 | invalidChallengeTokenSequence, 42 | invalidChallengeTokenData, 43 | invalidResponseTokenData, 44 | invalidResponseTokenSequence, 45 | invalidResponsePacketDataSize, 46 | invalidDisconnectPacketDataSize, 47 | invalidKeepAliveClientIndex, 48 | invalidKeepAliveMaxClients, 49 | payloadPacketTooSmall, 50 | payloadPacketTooLarge, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/NetcodeConnection.ts: -------------------------------------------------------------------------------- 1 | namespace Netcode { 2 | export const SOCKET_RCVBUF_SIZE = 2048 * 1024; 3 | export const SOCKET_SNDBUF_SIZE = 2048 * 1024; 4 | 5 | export type NetcodeRecvHandler = (data: INetcodeData) => void; 6 | export type UDPConnCreator = () => IUDPConn; 7 | 8 | export interface INetcodeData { 9 | data: Uint8Array; 10 | from?: IUDPAddr; 11 | } 12 | 13 | export class NetcodeConn { 14 | public constructor() { 15 | this._isClosed = true; 16 | this._maxBytes = MAX_PACKET_BYTES; 17 | this._recvSize = SOCKET_RCVBUF_SIZE; 18 | this._sendSize = SOCKET_SNDBUF_SIZE; 19 | } 20 | 21 | public setRecvHandler(recvHandlerFn: NetcodeRecvHandler) { 22 | this._recvHandlerFn = recvHandlerFn; 23 | } 24 | 25 | public write(b: Uint8Array): boolean { 26 | if (this._isClosed) { 27 | return false; 28 | } 29 | this._conn.send(b); 30 | return true; 31 | } 32 | 33 | public writeTo(b: Uint8Array, addr: IUDPAddr): boolean { 34 | if (this._isClosed) { 35 | return false; 36 | } 37 | this._conn.sendTo(b, addr); 38 | return true; 39 | } 40 | 41 | public close() { 42 | if (!this._isClosed) { 43 | this._isClosed = true; 44 | if (this._conn) { 45 | this._conn.close(); 46 | } 47 | } 48 | } 49 | 50 | public setReadBuffer(bytes: number) { 51 | if (bytes) { 52 | this._recvSize = bytes; 53 | if (this._conn) { 54 | this._conn.setReadBuffer(this._recvSize); 55 | } 56 | } 57 | } 58 | 59 | public setWriteBuffer(bytes: number) { 60 | if (bytes) { 61 | this._sendSize = bytes; 62 | if (this._conn) { 63 | this._conn.setWriteBuffer(this._recvSize); 64 | } 65 | } 66 | } 67 | 68 | public dial(createUdpConn: UDPConnCreator, addr: IUDPAddr): boolean { 69 | if (!this._recvHandlerFn) { 70 | return false; 71 | } 72 | 73 | this._conn = createUdpConn(); 74 | if (this._conn !== undefined) { 75 | this.init(); 76 | this._conn.connect(addr); 77 | return true; 78 | } else { 79 | return false; 80 | } 81 | } 82 | 83 | public listen(createUdpConn: UDPConnCreator, addr: IUDPAddr): boolean { 84 | if (!this._recvHandlerFn) { 85 | return false; 86 | } 87 | 88 | this._conn = createUdpConn(); 89 | if (this._conn !== undefined) { 90 | this._conn.bind(addr); 91 | this.init(); 92 | return true; 93 | } else { 94 | return false; 95 | } 96 | } 97 | 98 | private init() { 99 | this._isClosed = false; 100 | 101 | if (this._recvSize) { 102 | this._conn.setReadBuffer(this._recvSize); 103 | } 104 | if (this._sendSize) { 105 | this._conn.setWriteBuffer(this._sendSize); 106 | } 107 | this._conn.onMessage(this.onMessage.bind(this)); 108 | } 109 | 110 | private onMessage(message: Uint8Array, remote: IUDPAddr) { 111 | if (!this._recvHandlerFn) { 112 | console.warn('no recv handler is set for connection'); 113 | return; 114 | } 115 | if (message && message.length <= this._maxBytes) { 116 | if (PacketFactory.peekPacketType(message) >= PacketType.numPackets) { 117 | console.warn('invalid netcode packet is received'); 118 | } else { 119 | this._recvHandlerFn({ 120 | data: message, 121 | from: remote, 122 | }); 123 | } 124 | } 125 | } 126 | 127 | private _conn: IUDPConn; 128 | private _isClosed: boolean; 129 | private _maxBytes: number; 130 | private _recvSize: number; 131 | private _sendSize: number; 132 | private _recvHandlerFn: NetcodeRecvHandler; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Packet.ts: -------------------------------------------------------------------------------- 1 | namespace Netcode { 2 | export enum PacketType { 3 | connectionRequest, 4 | connectionDenied, 5 | connectionChallenge, 6 | connectionResponse, 7 | connectionKeepAlive, 8 | connectionPayload, 9 | connectionDisconnect, 10 | 11 | numPackets, 12 | } 13 | 14 | export interface IReadParams { 15 | protocolId: Long; 16 | currentTimestamp: number; 17 | readPacketKey: Uint8Array; 18 | privateKey: Uint8Array; 19 | allowedPackets: Uint8Array; 20 | replayProtection: ReplayProtection; 21 | } 22 | 23 | export interface IPacket { 24 | getType(): PacketType; 25 | getSequence(): Long; 26 | write( 27 | buf: Uint8Array, 28 | protocolID: Long, 29 | sequence: Long, 30 | writePacketKey: Uint8Array 31 | ): number; 32 | read( 33 | packetData: Uint8Array, 34 | packetLen: number, 35 | readParams: IReadParams 36 | ): Errors; 37 | } 38 | 39 | export class PacketFactory { 40 | public static peekPacketType(packetBuffer: Uint8Array): PacketType { 41 | const prefix = packetBuffer[0]; 42 | return prefix & 0xf; 43 | } 44 | 45 | public static create(packetBuffer: Uint8Array): IPacket { 46 | const packetType = this.peekPacketType(packetBuffer); 47 | switch (packetType) { 48 | case PacketType.connectionRequest: 49 | return new RequestPacket(); 50 | case PacketType.connectionChallenge: 51 | return new ChallengePacket(); 52 | case PacketType.connectionResponse: 53 | return new ResponsePacket(); 54 | case PacketType.connectionKeepAlive: 55 | return new KeepAlivePacket(); 56 | case PacketType.connectionDenied: 57 | return new DeniedPacket(); 58 | case PacketType.connectionPayload: 59 | return new PayloadPacket(); 60 | case PacketType.connectionDisconnect: 61 | return new DisconnectPacket(); 62 | default: 63 | console.error('unknown connection type', packetType); 64 | return null; 65 | } 66 | } 67 | } 68 | 69 | export class RequestPacket implements IPacket { 70 | public getType(): PacketType { 71 | return PacketType.connectionRequest; 72 | } 73 | 74 | public getSequence(): Long { 75 | return new Long(0, 0); 76 | } 77 | 78 | public get token(): ConnectTokenPrivate { 79 | return this._token; 80 | } 81 | 82 | public setProperties( 83 | versionInfo: Uint8Array, 84 | protocolID: Long, 85 | expireTimeStamp: Long, 86 | sequence: Long, 87 | connectTokenData: Uint8Array 88 | ) { 89 | this._versionInfo = versionInfo; 90 | this._protocolID = protocolID; 91 | this._connectTokenExpireTimestamp = expireTimeStamp; 92 | this._connectTokenSequence = sequence; 93 | this._connectTokenData = connectTokenData; 94 | } 95 | 96 | public write( 97 | buf: Uint8Array, 98 | protocolID: Long, 99 | sequence: Long, 100 | writePacketKey: Uint8Array 101 | ): number { 102 | const bb = new ByteBuffer(buf); 103 | bb.writeUint8(PacketType.connectionRequest); 104 | bb.writeBytes(this._versionInfo); 105 | bb.writeUint64(this._protocolID); 106 | bb.writeUint64(this._connectTokenExpireTimestamp); 107 | bb.writeUint64(this._connectTokenSequence); 108 | bb.writeBytes(this._connectTokenData); 109 | const correctPosition = 1 + 13 + 8 + 8 + 8 + CONNECT_TOKEN_PRIVATE_BYTES; 110 | if (bb.position !== correctPosition) { 111 | console.error('wrong token bytes length', bb.position, correctPosition); 112 | return -1; 113 | } 114 | return bb.position; 115 | } 116 | 117 | public read( 118 | packetData: Uint8Array, 119 | packetLen: number, 120 | readParams: IReadParams 121 | ): Errors { 122 | const bb = new ByteBuffer(packetData); 123 | const packetType = bb.readUint8(); 124 | if ( 125 | packetType === undefined || 126 | packetType !== PacketType.connectionRequest 127 | ) { 128 | return Errors.invalidPacket; 129 | } 130 | if (readParams.allowedPackets[0] === 0) { 131 | return Errors.packetTypeNotAllowed; 132 | } 133 | if ( 134 | packetLen !== 135 | 1 + VERSION_INFO_BYTES + 8 + 8 + 8 + CONNECT_TOKEN_PRIVATE_BYTES 136 | ) { 137 | return Errors.badPacketLength; 138 | } 139 | if (!readParams.privateKey) { 140 | return Errors.noPrivateKey; 141 | } 142 | 143 | this._versionInfo = bb.readBytes(VERSION_INFO_BYTES); 144 | if ( 145 | this._versionInfo === undefined || 146 | !Utils.arrayEqual(this._versionInfo, VERSION_INFO_BYTES_ARRAY) 147 | ) { 148 | return Errors.badVersionInfo; 149 | } 150 | 151 | this._protocolID = bb.readUint64(); 152 | if ( 153 | this._protocolID === undefined || 154 | !this._protocolID.equals(readParams.protocolId) 155 | ) { 156 | return Errors.badProtocolID; 157 | } 158 | 159 | this._connectTokenExpireTimestamp = bb.readUint64(); 160 | if ( 161 | this._connectTokenExpireTimestamp === undefined || 162 | this._connectTokenExpireTimestamp.toNumber() <= 163 | readParams.currentTimestamp 164 | ) { 165 | return Errors.connectTokenExpired; 166 | } 167 | 168 | this._connectTokenSequence = bb.readUint64(); 169 | if (this._connectTokenSequence === undefined) { 170 | return Errors.EOF; 171 | } 172 | 173 | if (bb.position !== 1 + VERSION_INFO_BYTES + 8 + 8 + 8) { 174 | return Errors.packetInvalidLength; 175 | } 176 | 177 | const tokenBuffer = bb.readBytes(CONNECT_TOKEN_PRIVATE_BYTES); 178 | if (tokenBuffer === undefined) { 179 | return Errors.EOF; 180 | } 181 | 182 | this._token = ConnectTokenPrivate.createEncrypted(tokenBuffer); 183 | if ( 184 | !this._token.decrypt( 185 | this._protocolID, 186 | this._connectTokenExpireTimestamp, 187 | this._connectTokenSequence, 188 | readParams.privateKey 189 | ) 190 | ) { 191 | return Errors.decryptPrivateTokenData; 192 | } 193 | const err = this._token.read(); 194 | if (err !== Errors.none) { 195 | return err; 196 | } 197 | 198 | return Errors.none; 199 | } 200 | 201 | private _versionInfo: Uint8Array; 202 | private _protocolID: Long; 203 | private _connectTokenExpireTimestamp: Long; 204 | private _connectTokenSequence: Long; 205 | private _token: ConnectTokenPrivate; 206 | private _connectTokenData: Uint8Array; 207 | } 208 | 209 | export class DeniedPacket implements IPacket { 210 | public getType(): PacketType { 211 | return PacketType.connectionDenied; 212 | } 213 | 214 | public getSequence(): Long { 215 | return this._sequence; 216 | } 217 | 218 | public write( 219 | buf: Uint8Array, 220 | protocolID: Long, 221 | sequence: Long, 222 | writePacketKey: Uint8Array 223 | ): number { 224 | const bb = new ByteBuffer(buf); 225 | const prefixByte = PacketHelper.writePacketPrefix(this, bb, sequence); 226 | if (prefixByte < 0) { 227 | return -1; 228 | } 229 | 230 | return PacketHelper.encryptPacket( 231 | bb, 232 | bb.position, 233 | bb.position, 234 | prefixByte, 235 | protocolID, 236 | sequence, 237 | writePacketKey 238 | ); 239 | } 240 | 241 | public read( 242 | packetData: Uint8Array, 243 | packetLen: number, 244 | readParams: IReadParams 245 | ): Errors { 246 | const bb = new ByteBuffer(packetData); 247 | const { sequence, decrypted, err } = PacketHelper.decryptPacket( 248 | bb, 249 | packetLen, 250 | readParams.protocolId, 251 | readParams.readPacketKey, 252 | readParams.allowedPackets, 253 | readParams.replayProtection 254 | ); 255 | if (err !== Errors.none) { 256 | return err; 257 | } 258 | this._sequence = sequence; 259 | if (decrypted.bytes.length !== 0) { 260 | return Errors.invalidDenyPacketDataSize; 261 | } 262 | return Errors.none; 263 | } 264 | 265 | private _sequence: Long; 266 | } 267 | 268 | export class ChallengePacket implements IPacket { 269 | public getType(): PacketType { 270 | return PacketType.connectionChallenge; 271 | } 272 | 273 | public getSequence(): Long { 274 | return this._sequence; 275 | } 276 | 277 | public get challengeTokenSequence(): Long { 278 | return this._challengeTokenSequence; 279 | } 280 | 281 | public get tokenData(): Uint8Array { 282 | return this._tokenData; 283 | } 284 | 285 | public setProperties(tokenSequence: Long, tokenData: Uint8Array) { 286 | this._challengeTokenSequence = tokenSequence; 287 | this._tokenData = tokenData; 288 | } 289 | 290 | public write( 291 | buf: Uint8Array, 292 | protocolID: Long, 293 | sequence: Long, 294 | writePacketKey: Uint8Array 295 | ): number { 296 | const bb = new ByteBuffer(buf); 297 | const prefixByte = PacketHelper.writePacketPrefix(this, bb, sequence); 298 | if (prefixByte < 0) { 299 | return -1; 300 | } 301 | 302 | const start = bb.position; 303 | bb.writeUint64(this._challengeTokenSequence); 304 | bb.writeBytes(this._tokenData, CHALLENGE_TOKEN_BYTES); 305 | const end = bb.position; 306 | return PacketHelper.encryptPacket( 307 | bb, 308 | start, 309 | end, 310 | prefixByte, 311 | protocolID, 312 | sequence, 313 | writePacketKey 314 | ); 315 | } 316 | 317 | public read( 318 | packetData: Uint8Array, 319 | packetLen: number, 320 | readParams: IReadParams 321 | ): Errors { 322 | const bb = new ByteBuffer(packetData); 323 | const { sequence, decrypted, err } = PacketHelper.decryptPacket( 324 | bb, 325 | packetLen, 326 | readParams.protocolId, 327 | readParams.readPacketKey, 328 | readParams.allowedPackets, 329 | readParams.replayProtection 330 | ); 331 | if (err !== Errors.none) { 332 | return err; 333 | } 334 | this._sequence = sequence; 335 | if (decrypted.bytes.length !== 8 + CHALLENGE_TOKEN_BYTES) { 336 | return Errors.invalidChallengePacketDataSize; 337 | } 338 | 339 | this._challengeTokenSequence = decrypted.readUint64(); 340 | if (this._challengeTokenSequence === undefined) { 341 | return Errors.invalidResponseTokenSequence; 342 | } 343 | 344 | this._tokenData = decrypted.readBytes(CHALLENGE_TOKEN_BYTES); 345 | if (this._tokenData === undefined) { 346 | return Errors.invalidChallengeTokenData; 347 | } 348 | return Errors.none; 349 | } 350 | 351 | private _sequence: Long; 352 | private _challengeTokenSequence: Long; 353 | private _tokenData: Uint8Array; 354 | } 355 | 356 | export class ResponsePacket implements IPacket { 357 | public getType(): PacketType { 358 | return PacketType.connectionResponse; 359 | } 360 | 361 | public getSequence(): Long { 362 | return this._sequence; 363 | } 364 | 365 | public get challengeTokenSequence(): Long { 366 | return this._challengeTokenSequence; 367 | } 368 | 369 | public get tokenData(): Uint8Array { 370 | return this._tokenData; 371 | } 372 | 373 | public setProperties(tokenSequence: Long, tokenData: Uint8Array) { 374 | this._challengeTokenSequence = tokenSequence; 375 | this._tokenData = tokenData; 376 | } 377 | 378 | public write( 379 | buf: Uint8Array, 380 | protocolID: Long, 381 | sequence: Long, 382 | writePacketKey: Uint8Array 383 | ): number { 384 | const bb = new ByteBuffer(buf); 385 | const prefixByte = PacketHelper.writePacketPrefix(this, bb, sequence); 386 | if (prefixByte < 0) { 387 | return -1; 388 | } 389 | 390 | const start = bb.position; 391 | bb.writeUint64(this._challengeTokenSequence); 392 | bb.writeBytes(this._tokenData, CHALLENGE_TOKEN_BYTES); 393 | const end = bb.position; 394 | return PacketHelper.encryptPacket( 395 | bb, 396 | start, 397 | end, 398 | prefixByte, 399 | protocolID, 400 | sequence, 401 | writePacketKey 402 | ); 403 | } 404 | 405 | public read( 406 | packetData: Uint8Array, 407 | packetLen: number, 408 | readParams: IReadParams 409 | ): Errors { 410 | const bb = new ByteBuffer(packetData); 411 | const { sequence, decrypted, err } = PacketHelper.decryptPacket( 412 | bb, 413 | packetLen, 414 | readParams.protocolId, 415 | readParams.readPacketKey, 416 | readParams.allowedPackets, 417 | readParams.replayProtection 418 | ); 419 | if (err !== Errors.none) { 420 | return err; 421 | } 422 | this._sequence = sequence; 423 | if (decrypted.bytes.length !== 8 + CHALLENGE_TOKEN_BYTES) { 424 | return Errors.invalidResponsePacketDataSize; 425 | } 426 | 427 | this._challengeTokenSequence = decrypted.readUint64(); 428 | if (this._challengeTokenSequence === undefined) { 429 | return Errors.invalidResponseTokenSequence; 430 | } 431 | 432 | this._tokenData = decrypted.readBytes(CHALLENGE_TOKEN_BYTES); 433 | if (this._tokenData === undefined) { 434 | return Errors.invalidResponseTokenData; 435 | } 436 | return Errors.none; 437 | } 438 | 439 | private _sequence: Long; 440 | private _challengeTokenSequence: Long; 441 | private _tokenData: Uint8Array; 442 | } 443 | 444 | export class KeepAlivePacket implements IPacket { 445 | public getType(): PacketType { 446 | return PacketType.connectionKeepAlive; 447 | } 448 | 449 | public getSequence(): Long { 450 | return this._sequence; 451 | } 452 | 453 | public get clientIndex(): number { 454 | return this._clientIndex; 455 | } 456 | 457 | public get maxClients(): number { 458 | return this._maxClients; 459 | } 460 | 461 | public setProperties(clientIndex: number, maxClients: number) { 462 | this._clientIndex = clientIndex; 463 | this._maxClients = maxClients; 464 | } 465 | 466 | public write( 467 | buf: Uint8Array, 468 | protocolID: Long, 469 | sequence: Long, 470 | writePacketKey: Uint8Array 471 | ): number { 472 | const bb = new ByteBuffer(buf); 473 | const prefixByte = PacketHelper.writePacketPrefix(this, bb, sequence); 474 | if (prefixByte < 0) { 475 | return -1; 476 | } 477 | 478 | const start = bb.position; 479 | bb.writeUint32(this._clientIndex); 480 | bb.writeUint32(this._maxClients); 481 | const end = bb.position; 482 | return PacketHelper.encryptPacket( 483 | bb, 484 | start, 485 | end, 486 | prefixByte, 487 | protocolID, 488 | sequence, 489 | writePacketKey 490 | ); 491 | } 492 | 493 | public read( 494 | packetData: Uint8Array, 495 | packetLen: number, 496 | readParams: IReadParams 497 | ): Errors { 498 | const bb = new ByteBuffer(packetData); 499 | const { sequence, decrypted, err } = PacketHelper.decryptPacket( 500 | bb, 501 | packetLen, 502 | readParams.protocolId, 503 | readParams.readPacketKey, 504 | readParams.allowedPackets, 505 | readParams.replayProtection 506 | ); 507 | if (err !== Errors.none) { 508 | return err; 509 | } 510 | this._sequence = sequence; 511 | if (decrypted.bytes.length !== 8) { 512 | return Errors.invalidDisconnectPacketDataSize; 513 | } 514 | 515 | this._clientIndex = decrypted.readUint32(); 516 | if (this._clientIndex === undefined) { 517 | return Errors.invalidKeepAliveClientIndex; 518 | } 519 | 520 | this._maxClients = decrypted.readUint32(); 521 | if (this._maxClients === undefined) { 522 | return Errors.invalidKeepAliveMaxClients; 523 | } 524 | return Errors.none; 525 | } 526 | 527 | private _sequence: Long; 528 | private _clientIndex: number; 529 | private _maxClients: number; 530 | } 531 | 532 | export class PayloadPacket implements IPacket { 533 | public getType(): PacketType { 534 | return PacketType.connectionPayload; 535 | } 536 | 537 | public getSequence(): Long { 538 | return this._sequence; 539 | } 540 | 541 | public get payloadData(): Uint8Array { 542 | return this._payloadData; 543 | } 544 | 545 | public constructor(payloadData?: Uint8Array) { 546 | if (payloadData) { 547 | this._payloadData = payloadData; 548 | } 549 | } 550 | 551 | public write( 552 | buf: Uint8Array, 553 | protocolID: Long, 554 | sequence: Long, 555 | writePacketKey: Uint8Array 556 | ): number { 557 | const bb = new ByteBuffer(buf); 558 | const prefixByte = PacketHelper.writePacketPrefix(this, bb, sequence); 559 | if (prefixByte < 0) { 560 | return -1; 561 | } 562 | 563 | const start = bb.position; 564 | bb.writeBytes(this._payloadData); 565 | const end = bb.position; 566 | return PacketHelper.encryptPacket( 567 | bb, 568 | start, 569 | end, 570 | prefixByte, 571 | protocolID, 572 | sequence, 573 | writePacketKey 574 | ); 575 | } 576 | 577 | public read( 578 | packetData: Uint8Array, 579 | packetLen: number, 580 | readParams: IReadParams 581 | ): Errors { 582 | const bb = new ByteBuffer(packetData); 583 | const { sequence, decrypted, err } = PacketHelper.decryptPacket( 584 | bb, 585 | packetLen, 586 | readParams.protocolId, 587 | readParams.readPacketKey, 588 | readParams.allowedPackets, 589 | readParams.replayProtection 590 | ); 591 | if (err !== Errors.none) { 592 | return err; 593 | } 594 | this._sequence = sequence; 595 | 596 | const decryptedSize = decrypted.bytes.length; 597 | if (decryptedSize < 1) { 598 | return Errors.payloadPacketTooSmall; 599 | } 600 | if (decryptedSize > MAX_PACKET_BYTES) { 601 | return Errors.payloadPacketTooLarge; 602 | } 603 | 604 | this._payloadData = decrypted.bytes; 605 | return Errors.none; 606 | } 607 | 608 | private _sequence: Long; 609 | private _payloadData: Uint8Array; 610 | } 611 | 612 | export class DisconnectPacket implements IPacket { 613 | public getType(): PacketType { 614 | return PacketType.connectionDisconnect; 615 | } 616 | 617 | public getSequence(): Long { 618 | return this._sequence; 619 | } 620 | 621 | public write( 622 | buf: Uint8Array, 623 | protocolID: Long, 624 | sequence: Long, 625 | writePacketKey: Uint8Array 626 | ): number { 627 | const bb = new ByteBuffer(buf); 628 | const prefixByte = PacketHelper.writePacketPrefix(this, bb, sequence); 629 | if (prefixByte < 0) { 630 | return -1; 631 | } 632 | return PacketHelper.encryptPacket( 633 | bb, 634 | bb.position, 635 | bb.position, 636 | prefixByte, 637 | protocolID, 638 | sequence, 639 | writePacketKey 640 | ); 641 | } 642 | 643 | public read( 644 | packetData: Uint8Array, 645 | packetLen: number, 646 | readParams: IReadParams 647 | ): Errors { 648 | const bb = new ByteBuffer(packetData); 649 | const { sequence, decrypted, err } = PacketHelper.decryptPacket( 650 | bb, 651 | packetLen, 652 | readParams.protocolId, 653 | readParams.readPacketKey, 654 | readParams.allowedPackets, 655 | readParams.replayProtection 656 | ); 657 | if (err !== Errors.none) { 658 | return err; 659 | } 660 | this._sequence = sequence; 661 | if (decrypted.bytes.length !== 0) { 662 | return Errors.invalidDisconnectPacketDataSize; 663 | } 664 | return Errors.none; 665 | } 666 | 667 | private _sequence: Long; 668 | } 669 | 670 | export class PacketHelper { 671 | // Encrypts the packet data of the supplied buffer between encryptedStart and encrypedFinish. 672 | public static encryptPacket( 673 | buffer: ByteBuffer, 674 | encryptedStart: number, 675 | encryptedFinish: number, 676 | prefixByte: number, 677 | protocolId: Long, 678 | sequence: Long, 679 | writePacketKey: Uint8Array 680 | ) { 681 | // slice up the buffer for the bits we will encrypt 682 | const encryptedBuffer = buffer.bytes.subarray( 683 | encryptedStart, 684 | encryptedFinish 685 | ); 686 | const { additionalData, nonce } = this.packetCryptData( 687 | prefixByte, 688 | protocolId, 689 | sequence 690 | ); 691 | 692 | const encrypted = Utils.aead_encrypt( 693 | writePacketKey, 694 | nonce, 695 | encryptedBuffer, 696 | additionalData 697 | ); 698 | buffer.bytes.set(encrypted[0], encryptedStart); 699 | buffer.bytes.set(encrypted[1], encryptedFinish); 700 | buffer.skipPosition(MAC_BYTES); 701 | return buffer.position; 702 | } 703 | 704 | // used for encrypting the per-packet packet written with the prefix byte, 705 | // protocol id and version as the associated data. this must match to decrypt. 706 | public static packetCryptData( 707 | prefixByte: number, 708 | protocolId: Long, 709 | sequence: Long 710 | ): { additionalData: Uint8Array; nonce: Uint8Array } { 711 | if (!this._addtionalDatabuffer) { 712 | this._addtionalDatabuffer = ByteBuffer.allocate( 713 | VERSION_INFO_BYTES + 8 + 1 714 | ); 715 | } 716 | const additionalData = this._addtionalDatabuffer; 717 | additionalData.clearPosition(); 718 | additionalData.writeBytes(VERSION_INFO_BYTES_ARRAY); 719 | additionalData.writeUint64(protocolId); 720 | additionalData.writeUint8(prefixByte); 721 | 722 | if (!this._nonceBuffer) { 723 | this._nonceBuffer = ByteBuffer.allocate(8 + 4); 724 | } 725 | const nonce = this._nonceBuffer; 726 | nonce.clearPosition(); 727 | nonce.writeUint32(0); 728 | nonce.writeUint64(sequence); 729 | return { additionalData: additionalData.bytes, nonce: nonce.bytes }; 730 | } 731 | 732 | // Decrypts the packet after reading in the prefix byte and sequence id. Used for all PacketTypes except RequestPacket. Returns a buffer containing the decrypted data 733 | public static decryptPacket( 734 | packetBuffer: ByteBuffer, 735 | packetLen: number, 736 | protocolId: Long, 737 | readPacketKey: Uint8Array, 738 | allowedPackets: Uint8Array, 739 | replayProtection: ReplayProtection 740 | ): { sequence?: Long; decrypted?: ByteBuffer; err: Errors } { 741 | const prefixByte = packetBuffer.readUint8(); 742 | if (prefixByte === undefined) { 743 | return { err: Errors.invalidPacket }; 744 | } 745 | 746 | const packetSequence = this.readSequence( 747 | packetBuffer, 748 | packetLen, 749 | prefixByte 750 | ); 751 | if (!packetSequence) { 752 | return { err: Errors.badSequence }; 753 | } 754 | 755 | const err = this.validateSequence( 756 | packetLen, 757 | prefixByte, 758 | packetSequence, 759 | readPacketKey, 760 | allowedPackets, 761 | replayProtection 762 | ); 763 | if (err !== Errors.none) { 764 | return { err }; 765 | } 766 | 767 | // decrypt the per-packet type data 768 | const { additionalData, nonce } = this.packetCryptData( 769 | prefixByte, 770 | protocolId, 771 | packetSequence 772 | ); 773 | 774 | const encryptedSize = packetLen - packetBuffer.position; 775 | if (encryptedSize < MAC_BYTES) { 776 | return { err: Errors.badPacketLength }; 777 | } 778 | 779 | const encryptedBuff = packetBuffer.readBytes(encryptedSize); 780 | if (encryptedBuff === undefined) { 781 | return { err: Errors.badPacketLength }; 782 | } 783 | 784 | const decrypted = Utils.aead_decrypt( 785 | readPacketKey, 786 | nonce, 787 | encryptedBuff.subarray(0, encryptedBuff.length - MAC_BYTES), 788 | additionalData, 789 | encryptedBuff.subarray(encryptedBuff.length - MAC_BYTES) 790 | ) as Uint8Array; 791 | if (!decrypted) { 792 | return { err: Errors.errDecryptData }; 793 | } 794 | packetBuffer.bytes.set(decrypted, 0); 795 | return { 796 | sequence: packetSequence, 797 | decrypted: new ByteBuffer( 798 | packetBuffer.bytes.subarray(0, decrypted.length) 799 | ), 800 | err: Errors.none, 801 | }; 802 | } 803 | 804 | // Reads and verifies the sequence id 805 | public static readSequence( 806 | packetBuffer: ByteBuffer, 807 | packetLen: number, 808 | prefixByte: number 809 | ): Long | undefined { 810 | const sequenceBytes = prefixByte >> 4; 811 | if (sequenceBytes < 1 || sequenceBytes > 8) { 812 | return; 813 | } 814 | if (packetLen < 1 + sequenceBytes + MAC_BYTES) { 815 | return; 816 | } 817 | 818 | let sequence: Long = new Long(0, 0); 819 | // read variable length sequence number [1,8] 820 | for (let i = 0; i < sequenceBytes; i += 1) { 821 | const val = packetBuffer.readUint8(); 822 | if (val === undefined) { 823 | return; 824 | } 825 | if (i <= 3) { 826 | sequence.low |= val << (8 * i); 827 | } else { 828 | sequence.high |= val << (8 * (i - 4)); 829 | } 830 | } 831 | return sequence; 832 | } 833 | 834 | // Validates the data prior to the encrypted segment before we bother attempting to decrypt. 835 | public static validateSequence( 836 | packetLen: number, 837 | prefixByte: number, 838 | sequence: Long, 839 | readPacketKey: Uint8Array, 840 | allowedPackets: Uint8Array, 841 | replayProtection: ReplayProtection 842 | ): Errors { 843 | if (!readPacketKey) { 844 | return Errors.emptyPacketKey; 845 | } 846 | 847 | if (packetLen < 1 + 1 + MAC_BYTES) { 848 | return Errors.badPacketLength; 849 | } 850 | 851 | const packetType: PacketType = prefixByte & 0xf; 852 | if (packetType >= PacketType.numPackets) { 853 | return Errors.invalidPacket; 854 | } 855 | 856 | if (!allowedPackets[packetType]) { 857 | return Errors.packetTypeNotAllowed; 858 | } 859 | 860 | // replay protection (optional) 861 | if (replayProtection && packetType >= PacketType.connectionKeepAlive) { 862 | if (replayProtection.checkAlreadyReceived(sequence.toNumber())) { 863 | return Errors.packetAlreadyReceived; 864 | } 865 | } 866 | return Errors.none; 867 | } 868 | 869 | // write the prefix byte 870 | // (this is a combination of the packet type and number of sequence bytes) 871 | public static writePacketPrefix( 872 | p: IPacket, 873 | buffer: ByteBuffer, 874 | sequence: Long 875 | ): number { 876 | const sequenceBytes = this.sequenceNumberBytesRequired(sequence); 877 | if (sequenceBytes < 1 || sequenceBytes > 8) { 878 | return -1; 879 | } 880 | 881 | const prefixByte = p.getType() | (0xff & (sequenceBytes << 4)); 882 | buffer.writeUint8(prefixByte); 883 | 884 | this._sequenceTemp.low = sequence.low; 885 | this._sequenceTemp.high = sequence.high; 886 | for (let i = 0; i < sequenceBytes; i += 1) { 887 | buffer.writeUint8(this._sequenceTemp.low & 0xff); 888 | this._sequenceTemp.rightShiftSelf(8); 889 | } 890 | return prefixByte; 891 | } 892 | 893 | // Depending on size of sequence number, we need to reserve N bytes 894 | public static sequenceNumberBytesRequired(sequence: Long): number { 895 | this._mask.high = 0xff000000; 896 | this._mask.low = 0; 897 | let i = 0; 898 | for (; i < 7; i += 1) { 899 | if ( 900 | (sequence.high & this._mask.high) !== 0 || 901 | (sequence.low & this._mask.low) !== 0 902 | ) { 903 | break; 904 | } 905 | this._mask.rightShiftSelf(8); 906 | } 907 | return 8 - i; 908 | } 909 | 910 | private static _sequenceTemp: Long = new Long(0, 0); 911 | private static _mask: Long = new Long(0, 0); 912 | private static _addtionalDatabuffer: ByteBuffer; 913 | private static _nonceBuffer: ByteBuffer; 914 | } 915 | } 916 | -------------------------------------------------------------------------------- /src/ReplayProtection.ts: -------------------------------------------------------------------------------- 1 | namespace Netcode { 2 | export const REPLAY_PROTECTION_BUFFER_SIZE = 256; 3 | 4 | // Our type to hold replay protection of packet sequences 5 | export class ReplayProtection { 6 | public constructor() { 7 | this._receivedPacket = new Array(REPLAY_PROTECTION_BUFFER_SIZE); 8 | this.reset(); 9 | } 10 | 11 | public reset() { 12 | this._mostRecentSequence = 0; 13 | this._receivedPacket.fill(0xffffffffffffffff); 14 | } 15 | 16 | public checkAlreadyReceived(sequence: number): boolean { 17 | if ((sequence & (1 << 63)) !== 0) { 18 | return false; 19 | } 20 | if ( 21 | sequence + REPLAY_PROTECTION_BUFFER_SIZE <= 22 | this._mostRecentSequence 23 | ) { 24 | return true; 25 | } 26 | if (sequence > this._mostRecentSequence) { 27 | this._mostRecentSequence = sequence; 28 | } 29 | const index = sequence % REPLAY_PROTECTION_BUFFER_SIZE; 30 | if (this._receivedPacket[index] == 0xffffffffffffffff) { 31 | this._receivedPacket[index] = sequence; 32 | return false; 33 | } 34 | if (this._receivedPacket[index] >= sequence) { 35 | return true; 36 | } 37 | this._receivedPacket[index] = sequence; 38 | return false; 39 | } 40 | 41 | private _mostRecentSequence: number; 42 | private _receivedPacket: Array; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Token.ts: -------------------------------------------------------------------------------- 1 | namespace Netcode { 2 | // This struct contains data that is shared in both public and private parts of the 3 | // connect token. 4 | export class SharedTokenData { 5 | public timeoutSeconds: number; 6 | public serverAddrs: IUDPAddr[]; 7 | public clientKey: Uint8Array; 8 | public serverKey: Uint8Array; 9 | 10 | public generate() { 11 | this.clientKey = Utils.generateKey(); 12 | this.serverKey = Utils.generateKey(); 13 | } 14 | 15 | public read(buffer: ByteBuffer): Errors { 16 | this.timeoutSeconds = buffer.readInt32(); 17 | if (this.timeoutSeconds === undefined) { 18 | return Errors.EOF; 19 | } 20 | const servers = buffer.readUint32(); 21 | if (servers === undefined) { 22 | return Errors.EOF; 23 | } 24 | if (servers <= 0) { 25 | return Errors.emptyServer; 26 | } 27 | if (servers > MAX_SERVERS_PER_CONNECT) { 28 | return Errors.tooManyServers; 29 | } 30 | 31 | this.serverAddrs = []; 32 | for (let i = 0; i < servers; i++) { 33 | const serverType = buffer.readUint8(); 34 | if (serverType === undefined) { 35 | return Errors.EOF; 36 | } 37 | 38 | let ipBytes: Uint8Array; 39 | let isIPV6: boolean = false; 40 | if (serverType === AddressType.ipv4) { 41 | ipBytes = buffer.readBytes(4); 42 | if (ipBytes === undefined) { 43 | return Errors.EOF; 44 | } 45 | } else if (serverType === AddressType.ipv6) { 46 | ipBytes = new Uint8Array(16); 47 | for (let j = 0; j < 16; j += 2) { 48 | const n = buffer.readUint16(); 49 | if (n === undefined) { 50 | return Errors.EOF; 51 | } 52 | // decode little endian -> big endian 53 | ipBytes[j] = 0xff & (n >> 8); 54 | ipBytes[j + 1] = 0xff & n; 55 | } 56 | isIPV6 = true; 57 | } else { 58 | return Errors.unknownIPAddressType; 59 | } 60 | const port = buffer.readUint16(); 61 | if (port === undefined) { 62 | return Errors.invalidPort; 63 | } 64 | this.serverAddrs[i] = { 65 | ip: ipBytes, 66 | port: port, 67 | isIPV6, 68 | }; 69 | } 70 | 71 | this.clientKey = buffer.readBytes(KEY_BYTES); 72 | if (!this.clientKey) { 73 | return Errors.EOF; 74 | } 75 | 76 | this.serverKey = buffer.readBytes(KEY_BYTES); 77 | if (!this.serverKey) { 78 | return Errors.EOF; 79 | } 80 | 81 | return Errors.none; 82 | } 83 | 84 | public write(buffer: ByteBuffer) { 85 | buffer.writeInt32(this.timeoutSeconds); 86 | buffer.writeUint32(this.serverAddrs.length); 87 | 88 | for (const addr of this.serverAddrs) { 89 | if (addr.isIPV6) { 90 | buffer.writeUint8(AddressType.ipv6); 91 | for (let i = 0; i < addr.ip.length; i += 2) { 92 | let n = (0xffff & addr.ip[i]) << 8; 93 | n |= 0xffff & addr.ip[i + 1]; 94 | // encode big endian -> little endian 95 | buffer.writeUint16(n); 96 | } 97 | } else { 98 | buffer.writeUint8(AddressType.ipv4); 99 | buffer.writeBytes(addr.ip); 100 | } 101 | buffer.writeUint16(addr.port); 102 | } 103 | 104 | buffer.writeBytes(this.clientKey, KEY_BYTES); 105 | buffer.writeBytes(this.serverKey, KEY_BYTES); 106 | } 107 | } 108 | 109 | // The private parts of a connect token 110 | export class ConnectTokenPrivate { 111 | public static createEncrypted(buffer: Uint8Array): ConnectTokenPrivate { 112 | const p = new ConnectTokenPrivate(); 113 | p.tokenData = new ByteBuffer(buffer); 114 | return p; 115 | } 116 | 117 | public static create( 118 | clientID: Long, 119 | timeoutSeconds: number, 120 | serverAddrs: IUDPAddr[], 121 | userData: Uint8Array 122 | ): ConnectTokenPrivate { 123 | const p = new ConnectTokenPrivate(); 124 | p.tokenData = ByteBuffer.allocate(CONNECT_TOKEN_PRIVATE_BYTES); 125 | p.clientId = clientID; 126 | p.userData = userData; 127 | p.sharedTokenData.timeoutSeconds = timeoutSeconds; 128 | p.sharedTokenData.serverAddrs = serverAddrs; 129 | return p; 130 | } 131 | 132 | public static buildTokenCryptData( 133 | protocolID: Long, 134 | expireTimestamp: Long, 135 | sequence: Long 136 | ): { additionalData: ByteBuffer; nonce: ByteBuffer } { 137 | this._sharedAdditionalBytes.clearPosition(); 138 | this._sharedAdditionalBytes.writeBytes(VERSION_INFO_BYTES_ARRAY); 139 | this._sharedAdditionalBytes.writeUint64(protocolID); 140 | this._sharedAdditionalBytes.writeUint64(expireTimestamp); 141 | 142 | this._sharedNonce.clearPosition(); 143 | this._sharedNonce.writeUint32(0); 144 | this._sharedNonce.writeUint64(sequence); 145 | return { 146 | additionalData: this._sharedAdditionalBytes, 147 | nonce: this._sharedNonce, 148 | }; 149 | } 150 | private static _sharedAdditionalBytes: ByteBuffer = ByteBuffer.allocate( 151 | VERSION_INFO_BYTES + 8 + 8 152 | ); 153 | private static _sharedNonce: ByteBuffer = ByteBuffer.allocate(8 + 4); 154 | 155 | public sharedTokenData: SharedTokenData; 156 | public clientId: Long; 157 | public userData: Uint8Array; 158 | public mac: Uint8Array; 159 | public tokenData: ByteBuffer; 160 | 161 | public constructor() { 162 | this.sharedTokenData = new SharedTokenData(); 163 | this.mac = new Uint8Array(MAC_BYTES); 164 | } 165 | 166 | public get buffer(): Uint8Array { 167 | return this.tokenData.bytes; 168 | } 169 | 170 | public generate() { 171 | this.sharedTokenData.generate(); 172 | } 173 | 174 | public read(): Errors { 175 | this.clientId = this.tokenData.readUint64(); 176 | if (!this.clientId) { 177 | return Errors.EOF; 178 | } 179 | const err = this.sharedTokenData.read(this.tokenData); 180 | if (err !== Errors.none) { 181 | return err; 182 | } 183 | this.userData = this.tokenData.readBytes(USER_DATA_BYTES); 184 | if (!this.userData) { 185 | return Errors.badUserData; 186 | } 187 | return Errors.none; 188 | } 189 | 190 | public write(): Uint8Array { 191 | this.tokenData.writeUint64(this.clientId); 192 | this.sharedTokenData.write(this.tokenData); 193 | this.tokenData.writeBytes(this.userData, USER_DATA_BYTES); 194 | return this.tokenData.bytes; 195 | } 196 | 197 | public encrypt( 198 | protocolID: Long, 199 | expireTimestamp: Long, 200 | sequence: Long, 201 | privateKey: Uint8Array 202 | ): boolean { 203 | const { additionalData, nonce } = ConnectTokenPrivate.buildTokenCryptData( 204 | protocolID, 205 | expireTimestamp, 206 | sequence 207 | ); 208 | const encBuf = this.tokenData.bytes.subarray( 209 | 0, 210 | CONNECT_TOKEN_PRIVATE_BYTES - MAC_BYTES 211 | ); 212 | const encrypted = Utils.aead_encrypt( 213 | privateKey, 214 | nonce.bytes, 215 | encBuf, 216 | additionalData.bytes 217 | ); 218 | if ( 219 | encrypted[0].length + encrypted[1].length !== 220 | CONNECT_TOKEN_PRIVATE_BYTES 221 | ) { 222 | console.error( 223 | 'encrypted length not correct', 224 | encrypted[0].length, 225 | CONNECT_TOKEN_PRIVATE_BYTES 226 | ); 227 | return false; 228 | } 229 | this.tokenData.bytes.set(encrypted[0], 0); 230 | this.tokenData.bytes.set(encrypted[1], encrypted[0].length); 231 | this.mac = encrypted[1]; 232 | return true; 233 | } 234 | 235 | public decrypt( 236 | protocolID: Long, 237 | expireTimestamp: Long, 238 | sequence: Long, 239 | privateKey: Uint8Array 240 | ): Uint8Array { 241 | if (this.tokenData.bytes.length !== CONNECT_TOKEN_PRIVATE_BYTES) { 242 | console.error('wrong connect private token data length'); 243 | return; 244 | } 245 | this.mac.set( 246 | this.tokenData.bytes.subarray(CONNECT_TOKEN_PRIVATE_BYTES - MAC_BYTES), 247 | 0 248 | ); 249 | const { additionalData, nonce } = ConnectTokenPrivate.buildTokenCryptData( 250 | protocolID, 251 | expireTimestamp, 252 | sequence 253 | ); 254 | const decrypted = Utils.aead_decrypt( 255 | privateKey, 256 | nonce.bytes, 257 | this.tokenData.bytes.subarray( 258 | 0, 259 | CONNECT_TOKEN_PRIVATE_BYTES - MAC_BYTES 260 | ), 261 | additionalData.bytes, 262 | this.mac 263 | ) as Uint8Array; 264 | if (decrypted) { 265 | this.tokenData = new ByteBuffer(decrypted.slice()); 266 | } else { 267 | console.error('decrypted connect private token failed'); 268 | return; 269 | } 270 | this.tokenData.clearPosition(); 271 | return this.tokenData.bytes; 272 | } 273 | } 274 | 275 | export class ConnectToken { 276 | public sharedTokenData: SharedTokenData; 277 | public versionInfo: Uint8Array; 278 | public protocolID: Long; 279 | public createTimestamp: Long; 280 | public expireTimestamp: Long; 281 | public sequence: Long; 282 | public privateData: ConnectTokenPrivate; 283 | 284 | public constructor() { 285 | this.sharedTokenData = new SharedTokenData(); 286 | this.privateData = new ConnectTokenPrivate(); 287 | } 288 | 289 | public generate( 290 | clientID: Long, 291 | serverAddrs: IUDPAddr[], 292 | protocoalID: Long, 293 | expireSeconds: number, 294 | timeoutSeconds: number, 295 | sequence: Long, 296 | userData: Uint8Array, 297 | privateKey: Uint8Array 298 | ): boolean { 299 | const now = Date.now(); 300 | this.createTimestamp = Long.fromNumber(now); 301 | if (expireSeconds >= 0) { 302 | this.expireTimestamp = Long.fromNumber(now + expireSeconds); 303 | } else { 304 | this.expireTimestamp = Long.fromNumber(0xffffffffffffffff); 305 | } 306 | this.sharedTokenData.timeoutSeconds = timeoutSeconds; 307 | this.versionInfo = new Uint8Array(VERSION_INFO_BYTES_ARRAY); 308 | this.protocolID = protocoalID; 309 | this.sequence = new Long(sequence.low, sequence.high); 310 | 311 | this.privateData = ConnectTokenPrivate.create( 312 | clientID, 313 | timeoutSeconds, 314 | serverAddrs, 315 | userData 316 | ); 317 | this.privateData.generate(); 318 | this.sharedTokenData.clientKey = 319 | this.privateData.sharedTokenData.clientKey; 320 | this.sharedTokenData.serverKey = 321 | this.privateData.sharedTokenData.serverKey; 322 | this.sharedTokenData.serverAddrs = serverAddrs; 323 | if (this.privateData.write() === undefined) { 324 | return false; 325 | } 326 | if ( 327 | !this.privateData.encrypt( 328 | this.protocolID, 329 | this.expireTimestamp, 330 | this.sequence, 331 | privateKey 332 | ) 333 | ) { 334 | return false; 335 | } 336 | return true; 337 | } 338 | 339 | public write(): Uint8Array { 340 | const bb = ByteBuffer.allocate(CONNECT_TOKEN_BYTES); 341 | bb.writeBytes(this.versionInfo); 342 | bb.writeUint64(this.protocolID); 343 | bb.writeUint64(this.createTimestamp); 344 | bb.writeUint64(this.expireTimestamp); 345 | bb.writeUint64(this.sequence); 346 | bb.writeBytes(this.privateData.buffer); 347 | this.sharedTokenData.write(bb); 348 | return bb.bytes; 349 | } 350 | 351 | public read(buffer: Uint8Array): Errors { 352 | const bb = new ByteBuffer(buffer); 353 | this.versionInfo = bb.readBytes(VERSION_INFO_BYTES); 354 | if ( 355 | this.versionInfo === undefined || 356 | !Utils.arrayEqual(this.versionInfo, VERSION_INFO_BYTES_ARRAY) 357 | ) { 358 | return Errors.badVersionInfo; 359 | } 360 | this.protocolID = bb.readUint64(); 361 | if (this.protocolID === undefined) { 362 | return Errors.badProtocolID; 363 | } 364 | this.createTimestamp = bb.readUint64(); 365 | if (this.createTimestamp === undefined) { 366 | return Errors.badCreateTimestamp; 367 | } 368 | this.expireTimestamp = bb.readUint64(); 369 | if (this.expireTimestamp === undefined) { 370 | return Errors.badExpireTimestamp; 371 | } 372 | if (this.createTimestamp.toNumber() > this.expireTimestamp.toNumber()) { 373 | return Errors.connectTokenExpired; 374 | } 375 | this.sequence = bb.readUint64(); 376 | if (this.sequence === undefined) { 377 | return Errors.badSequence; 378 | } 379 | const privateData = bb.readBytes(CONNECT_TOKEN_PRIVATE_BYTES); 380 | if (privateData === undefined) { 381 | return Errors.badPrivateData; 382 | } 383 | this.privateData.tokenData = new ByteBuffer(privateData); 384 | const err = this.sharedTokenData.read(bb); 385 | if (err !== Errors.none) { 386 | return err; 387 | } 388 | return Errors.none; 389 | } 390 | } 391 | 392 | export class ChallengeToken { 393 | public get clientID(): Long { 394 | return this._clientID; 395 | } 396 | 397 | public get userData(): Uint8Array { 398 | return this._userData.bytes; 399 | } 400 | 401 | public constructor(clientID?: Long) { 402 | this._userData = ByteBuffer.allocate(USER_DATA_BYTES); 403 | if (clientID) { 404 | this._clientID = clientID; 405 | } 406 | } 407 | 408 | public static encrypt( 409 | tokenBuffer: Uint8Array, 410 | sequance: Long, 411 | key: Uint8Array 412 | ) { 413 | this._nonceBuffer.clearPosition(); 414 | this._nonceBuffer.writeUint32(0); 415 | this._nonceBuffer.writeUint64(sequance); 416 | const encrypted = Utils.aead_encrypt( 417 | key, 418 | this._nonceBuffer.bytes, 419 | tokenBuffer.subarray(0, CHALLENGE_TOKEN_BYTES - MAC_BYTES), 420 | [] 421 | ); 422 | tokenBuffer.set(encrypted[0], 0); 423 | tokenBuffer.set(encrypted[1], encrypted[0].length); 424 | } 425 | 426 | public static decrypt( 427 | tokenBuffer: Uint8Array, 428 | sequance: Long, 429 | key: Uint8Array 430 | ): Uint8Array { 431 | this._nonceBuffer.clearPosition(); 432 | this._nonceBuffer.writeUint32(0); 433 | this._nonceBuffer.writeUint64(sequance); 434 | const decrypted = Utils.aead_decrypt( 435 | key, 436 | this._nonceBuffer.bytes, 437 | tokenBuffer.subarray(0, CHALLENGE_TOKEN_BYTES - MAC_BYTES), 438 | [], 439 | tokenBuffer.subarray(CHALLENGE_TOKEN_BYTES - MAC_BYTES) 440 | ) as Uint8Array; 441 | if (decrypted) { 442 | return decrypted.slice(); 443 | } 444 | } 445 | 446 | private static _nonceBuffer: ByteBuffer = ByteBuffer.allocate(8 + 4); 447 | 448 | public write(userData: Uint8Array): Uint8Array { 449 | this._userData.writeBytes(userData); 450 | 451 | const tokenData = ByteBuffer.allocate(CHALLENGE_TOKEN_BYTES); 452 | tokenData.writeUint64(this._clientID); 453 | tokenData.writeBytes(this._userData.bytes); 454 | return tokenData.bytes; 455 | } 456 | 457 | public read(buffer: Uint8Array): Errors { 458 | const bb = new ByteBuffer(buffer); 459 | this._clientID = bb.readUint64(); 460 | if (this._clientID === undefined) { 461 | return Errors.invalidClientID; 462 | } 463 | 464 | const userData = bb.readBytes(USER_DATA_BYTES); 465 | if (userData === undefined) { 466 | return Errors.badUserData; 467 | } 468 | this._userData.writeBytes(userData); 469 | this._userData.clearPosition(); 470 | return Errors.none; 471 | } 472 | 473 | private _clientID: Long; 474 | private _userData: ByteBuffer; 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /src/Utils.ts: -------------------------------------------------------------------------------- 1 | // let chacha = { 2 | // aead_encrypt: this['aead_encrypt'], 3 | // aead_decrypt: this['aead_decrypt'], 4 | // getRandomBytes: this['getRandomBytes'], 5 | // }; 6 | // if (!chacha.aead_encrypt) { 7 | // import('./chacha20poly1305').then(chachaModule => { 8 | // chacha = chachaModule; 9 | // }); 10 | // } 11 | namespace Netcode { 12 | export class Utils { 13 | public static generateKey(): Uint8Array { 14 | return this.getRandomBytes(KEY_BYTES); 15 | } 16 | 17 | public static getRandomBytes(num: number): Uint8Array { 18 | return getRandomBytes(num); 19 | } 20 | 21 | public static aead_encrypt( 22 | key: Uint8Array | Array, 23 | nonce: Uint8Array | Array, 24 | plaintext: Uint8Array | Array, 25 | data: Uint8Array | Array 26 | ): Uint8Array[] { 27 | return aead_encrypt(key, nonce, plaintext, data); 28 | } 29 | 30 | public static aead_decrypt( 31 | key: Uint8Array | Array, 32 | nonce: Uint8Array | Array, 33 | ciphertext: Uint8Array | Array, 34 | data: Uint8Array | Array, 35 | mac: Uint8Array | Array 36 | ): boolean | Uint8Array { 37 | return aead_decrypt(key, nonce, ciphertext, data, mac); 38 | } 39 | 40 | public static blockCopy( 41 | src: Uint8Array, 42 | srcOffset: number, 43 | dst: Uint8Array, 44 | dstOffset: number, 45 | count: number 46 | ) { 47 | if ( 48 | dstOffset + count < 0 || 49 | dstOffset + count > dst.length || 50 | srcOffset + count < 0 || 51 | srcOffset + count > src.length 52 | ) { 53 | throw new Error('blockCopy::array out of bounds'); 54 | } 55 | for (let i = 0; i < count; i++) { 56 | dst[dstOffset + i] = src[srcOffset + i]; 57 | } 58 | } 59 | 60 | public static arrayEqual(a1: Uint8Array, a2: Uint8Array) { 61 | a1.forEach(function (item, index) { 62 | if (a2[index] !== item) { 63 | return false; 64 | } 65 | }); 66 | return true; 67 | } 68 | 69 | public static addressEqual(addr1: IUDPAddr, addr2: IUDPAddr): boolean { 70 | if (!addr1 || !addr2) { 71 | return false; 72 | } 73 | return this.arrayEqual(addr1.ip, addr2.ip) && addr1.port == addr2.port; 74 | } 75 | 76 | public static floatEquals(a: number, b: number): boolean { 77 | if (a - b < this.EPSILON && b - a < this.EPSILON) { 78 | return true; 79 | } 80 | return false; 81 | } 82 | private static EPSILON: number = 0.000001; 83 | 84 | public static stringToIPV4Address(ip: string): IUDPAddr { 85 | const ipAndPort = ip.split(':'); 86 | let port = 0; 87 | if (ipAndPort.length == 2) { 88 | port = parseInt(ipAndPort[1], 10); 89 | if (Number.isNaN(port) || port <= 0) { 90 | console.warn('port must be a valid number larger than 0'); 91 | } 92 | } 93 | const octets = ip.split('.'); 94 | if (octets.length !== 4) { 95 | console.error('only support ipv4'); 96 | return; 97 | } 98 | const bytes = new Uint8Array(4); 99 | for (var i = 0; i < octets.length; ++i) { 100 | var octet = parseInt(octets[i], 10); 101 | if (Number.isNaN(octet) || octet < 0 || octet > 255) { 102 | throw new Error('Each octet must be between 0 and 255'); 103 | } 104 | bytes[i] = octet; 105 | } 106 | return { ip: bytes, isIPV6: false, port }; 107 | } 108 | 109 | public static IPV4AddressToString( 110 | address: IUDPAddr, 111 | appendPort?: boolean 112 | ): string { 113 | if (address.isIPV6) { 114 | console.error('only support ipv4'); 115 | return ''; 116 | } else { 117 | let str = `${address.ip[0]}.${address.ip[1]}.${address.ip[2]}.${address.ip[3]}`; 118 | if (appendPort && address.port > 0) { 119 | str += `:${address.port}`; 120 | } 121 | return str; 122 | } 123 | } 124 | } 125 | 126 | export class Queue { 127 | public constructor(capacity: number) { 128 | this._capacity = capacity; 129 | this._elements = new Array(capacity); 130 | this.clear(); 131 | } 132 | 133 | public clear() { 134 | this._numElements = 0; 135 | this._startIndex = 0; 136 | this._elements.fill(null); 137 | } 138 | 139 | public push(element: T): boolean { 140 | if (this._numElements === this._capacity) { 141 | return false; 142 | } 143 | 144 | const index = (this._startIndex + this._numElements) % this._capacity; 145 | this._elements[index] = element; 146 | this._numElements++; 147 | return true; 148 | } 149 | 150 | public pop(): T { 151 | if (this._numElements === 0) { 152 | return null; 153 | } 154 | const element = this._elements[this._startIndex]; 155 | this._startIndex = (this._startIndex + 1) % this._capacity; 156 | this._numElements--; 157 | return element; 158 | } 159 | 160 | private _numElements: number; 161 | private _startIndex: number; 162 | private _elements: Array; 163 | private _capacity: number; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/chacha20poly1305.d.ts: -------------------------------------------------------------------------------- 1 | declare function aead_encrypt( 2 | key: Uint8Array | Array, 3 | nonce: Uint8Array | Array, 4 | plaintext: Uint8Array | Array, 5 | data: Uint8Array | Array 6 | ): Uint8Array[]; 7 | declare function aead_decrypt( 8 | key: Uint8Array | Array, 9 | nonce: Uint8Array | Array, 10 | ciphertext: Uint8Array | Array, 11 | data: Uint8Array | Array, 12 | mac: Uint8Array | Array 13 | ): boolean | Uint8Array; 14 | declare function getRandomBytes(n: number): Uint8Array; 15 | -------------------------------------------------------------------------------- /src/client/Client.ts: -------------------------------------------------------------------------------- 1 | namespace Netcode { 2 | export enum ClientState { 3 | tokenExpired = -6, 4 | invalidConnectToken = -5, 5 | connectionTimeout = -4, 6 | connectionResponseTimeout = -3, 7 | connectionRequestTimeout = -2, 8 | connectionDenied = -1, 9 | disconnected = 0, 10 | sendingConnectionRequest = 1, 11 | sendingConnectionResponse = 2, 12 | connected = 3, 13 | } 14 | 15 | export const PACKET_SEND_RATE = 10.0; 16 | export const NUM_DISCONNECT_PACKETS = 10; // number of disconnect packets the client/server should send when disconnecting 17 | 18 | export interface IContext { 19 | writePacketKey: Uint8Array; 20 | readPacketKey: Uint8Array; 21 | } 22 | 23 | export type PacketErrorHandler = ( 24 | err: Errors, 25 | packetType: PacketType 26 | ) => void; 27 | 28 | export class Client { 29 | public debug: boolean = false; 30 | public onPacketError: PacketErrorHandler; 31 | 32 | public get id(): Long { 33 | return this._id; 34 | } 35 | 36 | public set id(value: Long) { 37 | this._id = value; 38 | } 39 | 40 | public get state(): ClientState { 41 | return this._state; 42 | } 43 | public set state(value: ClientState) { 44 | this._state = value; 45 | } 46 | 47 | public get clientIndex(): number { 48 | return this._clientIndex; 49 | } 50 | 51 | public get maxClients(): number { 52 | return this._maxClients; 53 | } 54 | 55 | public constructor(token: ConnectToken) { 56 | this.onPacketError = this.printPacketError.bind(this); 57 | this._connectToken = token; 58 | this._lastPacketRecvTime = -0.001; 59 | this._lastPacketSendTime = -0.001; 60 | this._shouldDisconnect = false; 61 | this.state = ClientState.disconnected; 62 | this._challenTokenData = new Uint8Array(CHALLENGE_TOKEN_BYTES); 63 | this._replayProtection = new ReplayProtection(); 64 | this._payloadPacketQueue = new Queue(PACKET_QUEUE_SIZE); 65 | this._allowedPackets = new Uint8Array(PacketType.numPackets); 66 | this._allowedPackets[PacketType.connectionDenied] = 1; 67 | this._allowedPackets[PacketType.connectionChallenge] = 1; 68 | this._allowedPackets[PacketType.connectionKeepAlive] = 1; 69 | this._allowedPackets[PacketType.connectionPayload] = 1; 70 | this._allowedPackets[PacketType.connectionDisconnect] = 1; 71 | this._tempBuffer = new Uint8Array(MAX_PACKET_BYTES); 72 | } 73 | 74 | public connect(dial: UDPConnCreator): Errors { 75 | this._dialFn = dial; 76 | this._startTime = 0; 77 | if ( 78 | this._serverIndex > 79 | this._connectToken.sharedTokenData.serverAddrs.length 80 | ) { 81 | return Errors.exceededServerNumber; 82 | } 83 | this._serverAddr = 84 | this._connectToken.sharedTokenData.serverAddrs[this._serverIndex]; 85 | 86 | this._conn = new NetcodeConn(); 87 | this._conn.setRecvHandler(this.handleNetcodeData.bind(this)); 88 | if (!this._conn.dial(dial, this._serverAddr)) { 89 | return Errors.dialServer; 90 | } 91 | 92 | this._context = { 93 | readPacketKey: this._connectToken.sharedTokenData.serverKey, 94 | writePacketKey: this._connectToken.sharedTokenData.clientKey, 95 | }; 96 | this.state = ClientState.sendingConnectionRequest; 97 | return Errors.none; 98 | } 99 | 100 | public disconnect(reason: ClientState, sendDisconnect: boolean) { 101 | this.debugLog(`client[${this._id}] disconnected: ${ClientState[reason]}`); 102 | if (reason > ClientState.disconnected) { 103 | console.warn('reason > disconnected'); 104 | return; 105 | } 106 | if (this._state <= ClientState.disconnected) { 107 | console.warn('state <= disconnected'); 108 | return; 109 | } 110 | 111 | if (sendDisconnect && this._state > ClientState.disconnected) { 112 | for (let i = 0; i < NUM_DISCONNECT_PACKETS; i += 1) { 113 | const p = new DisconnectPacket(); 114 | this.sendPacket(p); 115 | } 116 | } 117 | this.resetConectionData(reason); 118 | } 119 | 120 | public close() { 121 | if (this._conn) { 122 | this._conn.close(); 123 | } 124 | } 125 | 126 | public reset() { 127 | this._lastPacketSendTime = this._time - 0.001; 128 | this._lastPacketRecvTime = this._time - 0.001; 129 | this._shouldDisconnect = false; 130 | this._shouldDisconnectState = ClientState.disconnected; 131 | this._challenTokenData.fill(0); 132 | this._challengeSequence.setZero(); 133 | this._replayProtection.reset(); 134 | } 135 | 136 | public tick(timeInSeconds: number) { 137 | this._time = timeInSeconds; 138 | for (const p of this._receivedPackets) { 139 | this.onPacketData(p.data, p.from); 140 | } 141 | this._receivedPackets = []; 142 | this.tickSend(); 143 | const state = this._state; 144 | if (state > ClientState.disconnected && state < ClientState.connected) { 145 | const expire = 146 | this._connectToken.expireTimestamp.toNumber() - 147 | this._connectToken.createTimestamp.toNumber(); 148 | if (this._startTime + expire <= this._time) { 149 | this.debugLog( 150 | `client${this.id} connect failed. connect token expired` 151 | ); 152 | this.disconnect(ClientState.tokenExpired, false); 153 | return; 154 | } 155 | } 156 | if (this._shouldDisconnect) { 157 | this.debugLog( 158 | `client${this._id} should disconnect -> ${ 159 | ClientState[this._shouldDisconnectState] 160 | }` 161 | ); 162 | if (this.connectNextServer()) { 163 | return; 164 | } 165 | this.disconnect(this._shouldDisconnectState, false); 166 | return; 167 | } 168 | switch (this._state) { 169 | case ClientState.sendingConnectionRequest: 170 | { 171 | const timeout = 172 | this._lastPacketRecvTime + 173 | this._connectToken.sharedTokenData.timeoutSeconds; 174 | if (timeout < this._time) { 175 | this.debugLog(`client[${this._id}] connection request timed out`); 176 | if (this.connectNextServer()) { 177 | return; 178 | } 179 | this.disconnect(ClientState.connectionRequestTimeout, false); 180 | } 181 | } 182 | break; 183 | case ClientState.sendingConnectionResponse: 184 | { 185 | const timeout = 186 | this._lastPacketRecvTime + 187 | this._connectToken.sharedTokenData.timeoutSeconds; 188 | if (timeout < this._time) { 189 | this.debugLog( 190 | `client[${this._id}] connection response timed out` 191 | ); 192 | if (this.connectNextServer()) { 193 | return; 194 | } 195 | this.disconnect(ClientState.connectionResponseTimeout, false); 196 | } 197 | } 198 | break; 199 | case ClientState.connected: 200 | { 201 | const timeout = 202 | this._lastPacketRecvTime + 203 | this._connectToken.sharedTokenData.timeoutSeconds; 204 | if (timeout < this._time) { 205 | this.debugLog(`client[${this._id}] connection timed out`); 206 | this.disconnect(ClientState.connectionTimeout, false); 207 | } 208 | } 209 | break; 210 | } 211 | } 212 | 213 | public sendPayload(payloadData: Uint8Array): boolean { 214 | if (this._state !== ClientState.connected) { 215 | return false; 216 | } 217 | const p = new PayloadPacket(payloadData); 218 | this.sendPacket(p); 219 | } 220 | 221 | public recvPayload(): { data: Uint8Array; sequence: Long } { 222 | const packet = this._payloadPacketQueue.pop(); 223 | if (packet) { 224 | const p = packet as PayloadPacket; 225 | return { data: p.payloadData, sequence: p.getSequence() }; 226 | } 227 | } 228 | 229 | private connectNextServer(): boolean { 230 | if ( 231 | this._serverIndex + 1 >= 232 | this._connectToken.sharedTokenData.serverAddrs.length 233 | ) { 234 | return false; 235 | } 236 | 237 | this._serverIndex++; 238 | this._serverAddr = 239 | this._connectToken.sharedTokenData.serverAddrs[this._serverIndex]; 240 | 241 | this.reset(); 242 | 243 | this.debugLog( 244 | `client[${this._id}] connecting to next server %s (${this._serverAddr}` 245 | ); 246 | const err = this.connect(this._dialFn); 247 | if (err != Errors.none) { 248 | this.debugLog('error connecting to next server: ' + Errors[err]); 249 | return false; 250 | } 251 | this.state = ClientState.sendingConnectionRequest; 252 | return true; 253 | } 254 | 255 | private tickSend() { 256 | // check our send rate prior to bother sending 257 | if (this._lastPacketSendTime + 1.0 / PACKET_SEND_RATE >= this._time) { 258 | return; 259 | } 260 | switch (this._state) { 261 | case ClientState.sendingConnectionRequest: 262 | { 263 | const p = new RequestPacket(); 264 | p.setProperties( 265 | this._connectToken.versionInfo, 266 | this._connectToken.protocolID, 267 | this._connectToken.expireTimestamp, 268 | this._connectToken.sequence, 269 | this._connectToken.privateData.buffer 270 | ); 271 | this.debugLog( 272 | `client ${this._id} sent connection request packet to server` 273 | ); 274 | this.sendPacket(p); 275 | } 276 | break; 277 | case ClientState.sendingConnectionResponse: 278 | { 279 | const p = new ResponsePacket(); 280 | p.setProperties(this._challengeSequence, this._challenTokenData); 281 | this.debugLog( 282 | `client ${this._id} sent connection response packet to server` 283 | ); 284 | this.sendPacket(p); 285 | } 286 | break; 287 | case ClientState.connected: 288 | const p = new KeepAlivePacket(); 289 | p.setProperties(0, 0); 290 | // sent connection keep-alive packet to server 291 | this.sendPacket(p); 292 | break; 293 | } 294 | } 295 | 296 | private sendPacket(packet: IPacket): boolean { 297 | const buffer = this._tempBuffer; 298 | const bytesCount = packet.write( 299 | buffer, 300 | this._connectToken.protocolID, 301 | this._sequence, 302 | this._context.writePacketKey 303 | ); 304 | if (bytesCount <= 0) { 305 | return false; 306 | } 307 | if (!this._conn.write(buffer.subarray(0, bytesCount))) { 308 | return false; 309 | } 310 | this._lastPacketSendTime = this._time; 311 | this._sequence.plusOne(); 312 | return true; 313 | } 314 | 315 | private handleNetcodeData(data: INetcodeData) { 316 | this._receivedPackets.push(data); 317 | } 318 | 319 | private onPacketData(packetData: Uint8Array, from: IUDPAddr) { 320 | const size = packetData.length; 321 | const timestamp = Date.now(); 322 | 323 | const packet = PacketFactory.create(packetData); 324 | const err = packet.read(packetData, size, { 325 | protocolId: this._connectToken.protocolID, 326 | currentTimestamp: timestamp, 327 | readPacketKey: this._context.readPacketKey, 328 | privateKey: null, 329 | allowedPackets: this._allowedPackets, 330 | replayProtection: this._replayProtection, 331 | }); 332 | if (err === Errors.none) { 333 | this.processPacket(packet); 334 | } else if (this.onPacketError) { 335 | this.onPacketError(err, packet.getType()); 336 | } 337 | } 338 | 339 | private processPacket(packet: IPacket) { 340 | switch (packet.getType()) { 341 | case PacketType.connectionDenied: 342 | if ( 343 | this._state == ClientState.sendingConnectionRequest || 344 | this._state == ClientState.sendingConnectionResponse 345 | ) { 346 | this.debugLog( 347 | `client ${this._id} got connection denied packet from server` 348 | ); 349 | this._shouldDisconnect = true; 350 | this._shouldDisconnectState = ClientState.connectionDenied; 351 | } 352 | break; 353 | case PacketType.connectionChallenge: 354 | if (this._state !== ClientState.sendingConnectionRequest) { 355 | return; 356 | } 357 | this.debugLog( 358 | `client ${this._id} got connection challenge packet from server` 359 | ); 360 | const challengePacket = packet as ChallengePacket; 361 | this._challengeSequence = challengePacket.challengeTokenSequence; 362 | this._challenTokenData = challengePacket.tokenData; 363 | this.state = ClientState.sendingConnectionResponse; 364 | break; 365 | case PacketType.connectionKeepAlive: 366 | const keepAlivePacket = packet as KeepAlivePacket; 367 | if (this._state === ClientState.sendingConnectionResponse) { 368 | this._clientIndex = keepAlivePacket.clientIndex; 369 | this._maxClients = keepAlivePacket.maxClients; 370 | this.state = ClientState.connected; 371 | } 372 | break; 373 | case PacketType.connectionPayload: 374 | if (this._state !== ClientState.connected) { 375 | return; 376 | } 377 | this.debugLog( 378 | `client ${this._id} got payload packet from server ${( 379 | packet as PayloadPacket 380 | ) 381 | .getSequence() 382 | .toNumber()}` 383 | ); 384 | this._payloadPacketQueue.push(packet); 385 | break; 386 | case PacketType.connectionDisconnect: 387 | this.debugLog( 388 | `client ${this._id} got connection disconnect packet from server` 389 | ); 390 | if (this._state !== ClientState.connected) { 391 | return; 392 | } 393 | this._shouldDisconnect = true; 394 | this._shouldDisconnectState = ClientState.disconnected; 395 | break; 396 | default: 397 | return; 398 | } 399 | this._lastPacketRecvTime = this._time; 400 | } 401 | 402 | private printPacketError(err: Errors, packetType: PacketType) { 403 | console.error( 404 | 'process packet ' + 405 | Netcode.PacketType[packetType] + 406 | ' err: ' + 407 | Netcode.Errors[err] 408 | ); 409 | } 410 | 411 | private resetConectionData(newState: ClientState) { 412 | this._sequence.setZero(); 413 | this._clientIndex = 0; 414 | this._maxClients = 0; 415 | this._startTime = 0; 416 | this._serverIndex = 0; 417 | this._serverAddr = null; 418 | this._context = null; 419 | this.state = newState; 420 | this.reset(); 421 | this._payloadPacketQueue.clear(); 422 | this._conn.close(); 423 | } 424 | 425 | private debugLog(str: string) { 426 | if (this.debug) { 427 | console.log(str); 428 | } 429 | } 430 | 431 | private _dialFn: UDPConnCreator; 432 | private _id: Long; 433 | private _connectToken: ConnectToken; 434 | private _clientIndex: number = 0; 435 | private _maxClients: number = 0; 436 | 437 | private _time: number = 0; 438 | private _startTime: number; 439 | private _serverIndex: number = 0; 440 | private _serverAddr: IUDPAddr; 441 | 442 | private _context: IContext; 443 | private _lastPacketRecvTime: number; 444 | private _lastPacketSendTime: number; 445 | private _shouldDisconnect: boolean; 446 | private _shouldDisconnectState: ClientState; 447 | 448 | private _sequence: Long = new Long(0, 0); 449 | private _challenTokenData: Uint8Array; 450 | private _challengeSequence: Long = new Long(0, 0); 451 | private _allowedPackets: Uint8Array; 452 | private _payloadPacketQueue: Queue; 453 | private _receivedPackets: INetcodeData[] = []; 454 | private _tempBuffer: Uint8Array; 455 | 456 | private _state: ClientState; 457 | private _replayProtection: ReplayProtection; 458 | private _conn: NetcodeConn; 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /src/server/ClientManager.ts: -------------------------------------------------------------------------------- 1 | namespace Netcode { 2 | export class ClientInstance { 3 | public clientId: Long; 4 | public clientIndex: number; 5 | public serverConn: NetcodeConn; 6 | public confirmed: boolean; 7 | public connected: boolean; 8 | 9 | public encryptionIndex: number = 0; 10 | public sequence: Long; 11 | public lastSendTime: number; 12 | public lastRecvTime: number; 13 | public userData: Uint8Array; 14 | public protocolId: Long; 15 | public replayProtection: ReplayProtection; 16 | public address: IUDPAddr; 17 | public packetQueue: Queue; 18 | public packetData: Uint8Array; 19 | 20 | public constructor() { 21 | this.userData = new Uint8Array(USER_DATA_BYTES); 22 | this.packetQueue = new Queue(PACKET_QUEUE_SIZE); 23 | this.packetData = new Uint8Array(MAX_PACKET_BYTES); 24 | this.replayProtection = new ReplayProtection(); 25 | this.sequence = new Long(0, 0); 26 | } 27 | 28 | public clear() { 29 | this.replayProtection.reset(); 30 | this.connected = false; 31 | this.confirmed = false; 32 | this.clientId.setZero(); 33 | this.sequence.setZero(); 34 | this.lastSendTime = 0.0; 35 | this.lastRecvTime = 0.0; 36 | this.address = null; 37 | this.clientIndex = -1; 38 | this.encryptionIndex = -1; 39 | this.packetQueue.clear(); 40 | this.userData.fill(0); 41 | this.packetData.fill(0); 42 | } 43 | 44 | public sendPacket( 45 | packet: IPacket, 46 | writePacketKey: Uint8Array, 47 | serverTime: number 48 | ): boolean { 49 | let writeCount = packet.write( 50 | this.packetData, 51 | this.protocolId, 52 | this.sequence, 53 | writePacketKey 54 | ); 55 | if (writeCount === 0) { 56 | return false; 57 | } 58 | 59 | if ( 60 | !this.serverConn.writeTo( 61 | this.packetData.subarray(0, writeCount), 62 | this.address 63 | ) 64 | ) { 65 | return false; 66 | } 67 | 68 | this.sequence.plusOne(); 69 | this.lastSendTime = serverTime; 70 | return true; 71 | } 72 | } 73 | 74 | interface IConnectTokenEntry { 75 | mac: Uint8Array; 76 | address: IUDPAddr; 77 | time: number; 78 | } 79 | 80 | interface IEncryptionEntry { 81 | expireTime: number; 82 | lastAccess: number; 83 | timeout: number; 84 | address: IUDPAddr; 85 | sendKey: Uint8Array; 86 | recvKey: Uint8Array; 87 | } 88 | 89 | export type ClientConnectionHandler = (client: ClientInstance) => void; 90 | 91 | export class ClientManager { 92 | public constructor(timeout: number, maxClients: number) { 93 | this.maxClients = maxClients; 94 | this.maxEntries = maxClients * 8; 95 | this._timeout = timeout; 96 | this.emptyMac = new Uint8Array(MAC_BYTES); 97 | this.emptyWriteKey = new Uint8Array(KEY_BYTES); 98 | this.resetClientInstances(); 99 | this.resetTokenEntries(); 100 | this.resetCryptoEntries(); 101 | } 102 | 103 | public get instances(): ClientInstance[] { 104 | return this._instances; 105 | } 106 | 107 | public set timeout(value: number) { 108 | this._timeout = value; 109 | } 110 | 111 | public findFreeClientIndex(): number { 112 | for (let i = 0; i < this.maxClients; i += 1) { 113 | if (!this._instances[i].connected) { 114 | return i; 115 | } 116 | } 117 | return -1; 118 | } 119 | 120 | // Returns the clientIds of the connected clients. To avoid allocating a buffer everytime this is called 121 | // we simply re-add all connected clients to the connectedClientIds buffer and return the slice of how 122 | // many we were able to add. 123 | public getConnectedClients(): Long[] { 124 | const connectedClientIds = []; 125 | for ( 126 | let clientIndex = 0; 127 | clientIndex < this.maxClients; 128 | clientIndex += 1 129 | ) { 130 | const client = this._instances[clientIndex]; 131 | if (client.connected && client.address) { 132 | connectedClientIds.push(client.clientId); 133 | } 134 | } 135 | return connectedClientIds; 136 | } 137 | 138 | public getConnectedClientCount(): number { 139 | return this.getConnectedClients().length; 140 | } 141 | 142 | // Initializes the client with the clientId 143 | public connectClient( 144 | addr: IUDPAddr, 145 | challengeToken: ChallengeToken 146 | ): ClientInstance { 147 | const clientIndex = this.findFreeClientIndex(); 148 | if (clientIndex == -1) { 149 | console.warn('failure to find free client index'); 150 | return null; 151 | } 152 | const client = this._instances[clientIndex]; 153 | client.clientIndex = clientIndex; 154 | client.connected = true; 155 | client.sequence.setZero(); 156 | client.clientId = challengeToken.clientID; 157 | client.address = addr; 158 | client.userData.set(challengeToken.userData); 159 | if (this.clientConnectHandler) { 160 | this.clientConnectHandler(client); 161 | } 162 | return client; 163 | } 164 | 165 | // Disconnects the client referenced by the provided clientIndex. 166 | public disconnectClientByIndex( 167 | clientIndex: number, 168 | sendDisconnect: boolean, 169 | serverTime: number 170 | ) { 171 | const instance = this._instances[clientIndex]; 172 | this.disconnectClient(instance, sendDisconnect, serverTime); 173 | } 174 | 175 | // Finds the client index referenced by the provided UDPAddr. 176 | public findClientIndexByAddress(addr: IUDPAddr): number { 177 | for (let i = 0; i < this.maxClients; i += 1) { 178 | const instance = this._instances[i]; 179 | if ( 180 | instance.address && 181 | instance.connected && 182 | Utils.addressEqual(instance.address, addr) 183 | ) { 184 | return i; 185 | } 186 | } 187 | return -1; 188 | } 189 | 190 | // Finds the client index via the provided clientId. 191 | public findClientIndexById(clientId: Long): number { 192 | for (let i = 0; i < this.maxClients; i += 1) { 193 | const instance = this._instances[i]; 194 | if ( 195 | instance.address && 196 | instance.connected && 197 | instance.clientId.equals(clientId) 198 | ) { 199 | return i; 200 | } 201 | } 202 | return -1; 203 | } 204 | 205 | // Finds the encryption index via the provided clientIndex, returns -1 if not found. 206 | public findEncryptionIndexByClientIndex(clientIndex: number): number { 207 | if (clientIndex < 0 || clientIndex > this.maxClients) { 208 | return -1; 209 | } 210 | 211 | return this._instances[clientIndex].encryptionIndex; 212 | } 213 | 214 | // Finds an encryption entry index via the provided UDPAddr. 215 | public findEncryptionEntryIndex( 216 | addr: IUDPAddr, 217 | serverTime: number 218 | ): number { 219 | for (let i = 0; i < this.numCryptoEntries; i += 1) { 220 | const entry = this.cryptoEntries[i]; 221 | if (!entry || !entry.address) { 222 | continue; 223 | } 224 | 225 | const lastAccessTimeout = entry.lastAccess + this._timeout; 226 | if ( 227 | Utils.addressEqual(entry.address, addr) && 228 | this.serverTimedout(lastAccessTimeout, serverTime) && 229 | (entry.expireTime < 0 || entry.expireTime >= serverTime) 230 | ) { 231 | entry.lastAccess = serverTime; 232 | return i; 233 | } 234 | } 235 | return -1; 236 | } 237 | 238 | // Finds or adds a token entry to our token entry slice. 239 | public findOrAddTokenEntry( 240 | connectTokenMac: Uint8Array, 241 | addr: IUDPAddr, 242 | serverTime: number 243 | ): boolean { 244 | let oldestTime: number; 245 | 246 | let tokenIndex = -1; 247 | let oldestIndex = -1; 248 | 249 | if (Utils.arrayEqual(connectTokenMac, this.emptyMac)) { 250 | return false; 251 | } 252 | 253 | // find the matching entry for the token mac and the oldest token entry. 254 | for (let i = 0; i < this.maxEntries; i += 1) { 255 | if ( 256 | Utils.arrayEqual(this.connectTokensEntries[i].mac, connectTokenMac) 257 | ) { 258 | tokenIndex = i; 259 | } 260 | 261 | if ( 262 | oldestIndex == -1 || 263 | this.connectTokensEntries[i].time < oldestTime 264 | ) { 265 | oldestTime = this.connectTokensEntries[i].time; 266 | oldestIndex = i; 267 | } 268 | } 269 | 270 | // if no entry is found with the mac, this is a new connect token. replace the oldest token entry. 271 | if (tokenIndex == -1) { 272 | this.connectTokensEntries[oldestIndex].time = serverTime; 273 | this.connectTokensEntries[oldestIndex].address = addr; 274 | this.connectTokensEntries[oldestIndex].mac = connectTokenMac; 275 | console.log('new connect token added for %s', addr); 276 | return true; 277 | } 278 | 279 | // allow connect tokens we have already seen from the same address 280 | if ( 281 | Utils.addressEqual(this.connectTokensEntries[tokenIndex].address, addr) 282 | ) { 283 | return true; 284 | } 285 | 286 | return false; 287 | } 288 | 289 | // Adds a new encryption mapping of client/server keys. 290 | public addEncryptionMapping( 291 | connectToken: ConnectTokenPrivate, 292 | addr: IUDPAddr, 293 | serverTime: number, 294 | expireTime: number 295 | ): boolean { 296 | // already list 297 | for (let i = 0; i < this.maxEntries; i += 1) { 298 | const entry = this.cryptoEntries[i]; 299 | 300 | const lastAccessTimeout = entry.lastAccess + this._timeout; 301 | if ( 302 | entry.address && 303 | Utils.addressEqual(entry.address, addr) && 304 | this.serverTimedout(lastAccessTimeout, serverTime) 305 | ) { 306 | entry.expireTime = expireTime; 307 | entry.lastAccess = serverTime; 308 | entry.sendKey.set(connectToken.sharedTokenData.serverKey); 309 | entry.recvKey.set(connectToken.sharedTokenData.clientKey); 310 | console.log('re-added encryption mapping for %s encIdx: %d', addr, i); 311 | return true; 312 | } 313 | } 314 | 315 | // not in our list. 316 | for (let i = 0; i < this.maxEntries; i += 1) { 317 | const entry = this.cryptoEntries[i]; 318 | if ( 319 | entry.lastAccess + this._timeout < serverTime || 320 | (entry.expireTime >= 0 && entry.expireTime < serverTime) 321 | ) { 322 | entry.address = addr; 323 | entry.expireTime = expireTime; 324 | entry.lastAccess = serverTime; 325 | entry.sendKey.set(connectToken.sharedTokenData.serverKey); 326 | entry.recvKey.set(connectToken.sharedTokenData.clientKey); 327 | if (i + 1 > this.numCryptoEntries) { 328 | this.numCryptoEntries = i + 1; 329 | } 330 | return true; 331 | } 332 | } 333 | 334 | return false; 335 | } 336 | 337 | // Update the encryption entry for the provided encryption index. 338 | public touchEncryptionEntry( 339 | encryptionIndex: number, 340 | addr: IUDPAddr, 341 | serverTime: number 342 | ): boolean { 343 | if (encryptionIndex < 0 || encryptionIndex > this.numCryptoEntries) { 344 | return false; 345 | } 346 | 347 | if ( 348 | !Utils.addressEqual(this.cryptoEntries[encryptionIndex].address, addr) 349 | ) { 350 | return false; 351 | } 352 | 353 | this.cryptoEntries[encryptionIndex].lastAccess = serverTime; 354 | return true; 355 | } 356 | 357 | // Sets the expiration for this encryption entry. 358 | public setEncryptionEntryExpiration( 359 | encryptionIndex: number, 360 | expireTime: number 361 | ): boolean { 362 | if (encryptionIndex < 0 || encryptionIndex > this.numCryptoEntries) { 363 | return false; 364 | } 365 | this.cryptoEntries[encryptionIndex].expireTime = expireTime; 366 | return true; 367 | } 368 | 369 | // Returns the encryption send key. 370 | public getEncryptionEntrySendKey(index: number): Uint8Array { 371 | return this.getEncryptionEntryKey(index, true); 372 | } 373 | 374 | // Returns the encryption recv key. 375 | public getEncryptionEntryRecvKey(index: number): Uint8Array { 376 | return this.getEncryptionEntryKey(index, false); 377 | } 378 | 379 | public disconnectClient( 380 | client: ClientInstance, 381 | sendDisconnect: boolean, 382 | serverTime: number 383 | ) { 384 | if (!client.connected) { 385 | console.error('client is already disconnected'); 386 | return; 387 | } 388 | 389 | if (sendDisconnect) { 390 | const packet = new DisconnectPacket(); 391 | const writePacketKey = this.getEncryptionEntrySendKey( 392 | client.encryptionIndex 393 | ); 394 | if (!writePacketKey) { 395 | console.error( 396 | 'error: unable to retrieve encryption key for client for disconnect: %d\n', 397 | client.clientId 398 | ); 399 | } else { 400 | for (let i = 0; i < NUM_DISCONNECT_PACKETS; i += 1) { 401 | client.sendPacket(packet, writePacketKey, serverTime); 402 | } 403 | } 404 | } 405 | console.log('removing encryption entry for: %s', client.address); 406 | this.removeEncryptionEntry(client.address, serverTime); 407 | if (this.clientDisconnectHandler) { 408 | this.clientDisconnectHandler(client); 409 | } 410 | client.clear(); 411 | } 412 | 413 | // Removes the encryption entry for this UDPAddr. 414 | public removeEncryptionEntry(addr: IUDPAddr, serverTime: number): boolean { 415 | for (let i = 0; i < this.numCryptoEntries; i += 1) { 416 | const entry = this.cryptoEntries[i]; 417 | if (!Utils.addressEqual(entry.address, addr)) { 418 | continue; 419 | } 420 | 421 | this.clearCryptoEntry(entry); 422 | 423 | if (i + 1 == this.numCryptoEntries) { 424 | let index = i - 1; 425 | while (index >= 0) { 426 | const lastAccessTimeout = 427 | this.cryptoEntries[index].lastAccess + this._timeout; 428 | if ( 429 | this.serverTimedout(lastAccessTimeout, serverTime) && 430 | (this.cryptoEntries[index].expireTime < 0 || 431 | this.cryptoEntries[index].expireTime > serverTime) 432 | ) { 433 | break; 434 | } 435 | index--; 436 | } 437 | this.numCryptoEntries = index + 1; 438 | } 439 | 440 | return true; 441 | } 442 | 443 | return false; 444 | } 445 | 446 | public resetClientInstances() { 447 | this._instances = []; 448 | for (let i = 0; i < this.maxClients; i += 1) { 449 | const instance = new ClientInstance(); 450 | this._instances[i] = instance; 451 | } 452 | } 453 | 454 | // preallocate the token buffers so we don't have to do nil checks 455 | public resetTokenEntries() { 456 | this.connectTokensEntries = []; 457 | for (let i = 0; i < this.maxEntries; i += 1) { 458 | const entry: any = {}; 459 | this.clearTokenEntry(entry); 460 | this.connectTokensEntries[i] = entry; 461 | } 462 | } 463 | 464 | private clearTokenEntry(entry: IConnectTokenEntry) { 465 | entry.mac = new Uint8Array(MAC_BYTES); 466 | entry.address = null; 467 | entry.time = -1; 468 | } 469 | 470 | // preallocate the crypto entries so we don't have to do nil checks 471 | public resetCryptoEntries() { 472 | this.cryptoEntries = []; 473 | for (let i = 0; i < this.maxEntries; i += 1) { 474 | const entry: any = {}; 475 | this.clearCryptoEntry(entry); 476 | this.cryptoEntries[i] = entry; 477 | } 478 | } 479 | 480 | public clearCryptoEntry(entry: IEncryptionEntry) { 481 | entry.expireTime = -1; 482 | entry.lastAccess = -1000; 483 | entry.address = null; 484 | entry.sendKey = new Uint8Array(KEY_BYTES); 485 | entry.recvKey = new Uint8Array(KEY_BYTES); 486 | } 487 | 488 | public getEncryptionEntryKey(index: number, sendKey: boolean): Uint8Array { 489 | if (index === -1 || index < 0 || index > this.numCryptoEntries) { 490 | return null; 491 | } 492 | 493 | if (sendKey) { 494 | return this.cryptoEntries[index].sendKey; 495 | } 496 | 497 | return this.cryptoEntries[index].recvKey; 498 | } 499 | 500 | // checks if last access + timeout is > or = to serverTime. 501 | public sendPayloads(payloadData: Uint8Array, serverTime: number) { 502 | for (let i = 0; i < this.maxClients; i += 1) { 503 | this.sendPayloadToInstance(i, payloadData, serverTime); 504 | } 505 | } 506 | 507 | public sendPayloadToInstance( 508 | index: number, 509 | payloadData: Uint8Array, 510 | serverTime: number 511 | ) { 512 | const instance = this._instances[index]; 513 | if (instance.encryptionIndex === -1) { 514 | return; 515 | } 516 | 517 | const writePacketKey = this.getEncryptionEntrySendKey( 518 | instance.encryptionIndex 519 | ); 520 | if ( 521 | Utils.arrayEqual(writePacketKey, this.emptyWriteKey) || 522 | !instance.address 523 | ) { 524 | return; 525 | } 526 | 527 | if (!instance.confirmed) { 528 | const packet = new KeepAlivePacket(); 529 | packet.setProperties(instance.clientIndex, this.maxClients); 530 | instance.sendPacket(packet, writePacketKey, serverTime); 531 | } 532 | 533 | if (instance.connected) { 534 | if ( 535 | !this.touchEncryptionEntry( 536 | instance.encryptionIndex, 537 | instance.address, 538 | serverTime 539 | ) 540 | ) { 541 | console.error( 542 | 'error: encryption mapping is out of date for client %d', 543 | instance.clientIndex 544 | ); 545 | return; 546 | } 547 | const packet = new PayloadPacket(payloadData); 548 | instance.sendPacket(packet, writePacketKey, serverTime); 549 | } 550 | } 551 | 552 | // Send keep alives to all connected clients. 553 | public sendKeepAlives(serverTime: number) { 554 | for (let i = 0; i < this.maxClients; i += 1) { 555 | const instance = this._instances[i]; 556 | if (!instance.connected) { 557 | continue; 558 | } 559 | 560 | const writePacketKey = this.getEncryptionEntrySendKey( 561 | instance.encryptionIndex 562 | ); 563 | if ( 564 | Utils.arrayEqual(writePacketKey, this.emptyWriteKey) || 565 | !instance.address 566 | ) { 567 | continue; 568 | } 569 | 570 | const shouldSendTime = instance.lastSendTime + 1.0 / PACKET_SEND_RATE; 571 | if ( 572 | shouldSendTime < serverTime || 573 | Utils.floatEquals(shouldSendTime, serverTime) 574 | ) { 575 | if ( 576 | !this.touchEncryptionEntry( 577 | instance.encryptionIndex, 578 | instance.address, 579 | serverTime 580 | ) 581 | ) { 582 | console.error( 583 | 'error: encryption mapping is out of date for client %d', 584 | instance.clientIndex 585 | ); 586 | continue; 587 | } 588 | 589 | const packet = new KeepAlivePacket(); 590 | packet.setProperties(instance.clientIndex, this.maxClients); 591 | instance.sendPacket(packet, writePacketKey, serverTime); 592 | } 593 | } 594 | } 595 | 596 | // Checks and disconnects any clients that have timed out. 597 | public checkTimeouts(serverTime: number) { 598 | for (let i = 0; i < this.maxClients; i += 1) { 599 | const instance = this._instances[i]; 600 | const timeout = instance.lastRecvTime + this._timeout; 601 | 602 | if ( 603 | instance.connected && 604 | (timeout < serverTime || Utils.floatEquals(timeout, serverTime)) 605 | ) { 606 | console.log('server timed out client: %d', i); 607 | this.disconnectClient(instance, false, serverTime); 608 | } 609 | } 610 | } 611 | 612 | public disconnectClients(serverTime: number) { 613 | for ( 614 | let clientIndex = 0; 615 | clientIndex < this.maxClients; 616 | clientIndex += 1 617 | ) { 618 | const instance = this._instances[clientIndex]; 619 | this.disconnectClient(instance, true, serverTime); 620 | } 621 | } 622 | 623 | private serverTimedout( 624 | lastAccessTimeout: number, 625 | serverTime: number 626 | ): boolean { 627 | return ( 628 | lastAccessTimeout > serverTime || 629 | Utils.floatEquals(lastAccessTimeout, serverTime) 630 | ); 631 | } 632 | 633 | private maxClients: number; 634 | private maxEntries: number; 635 | private _timeout: number; 636 | 637 | private _instances: ClientInstance[]; 638 | private connectTokensEntries: IConnectTokenEntry[]; 639 | private cryptoEntries: IEncryptionEntry[]; 640 | private numCryptoEntries: number = 0; 641 | 642 | private emptyMac: Uint8Array; // used to ensure empty mac (all empty bytes) doesn't match 643 | private emptyWriteKey: Uint8Array; // used to test for empty write key 644 | 645 | clientConnectHandler: ClientConnectionHandler; 646 | clientDisconnectHandler: ClientConnectionHandler; 647 | } 648 | } 649 | -------------------------------------------------------------------------------- /src/server/Server.ts: -------------------------------------------------------------------------------- 1 | namespace Netcode { 2 | export const TIMEOUT_SECONDS = 5; // default timeout for clients 3 | export const MAX_SERVER_PACKETS = 64; 4 | 5 | export class Server { 6 | public set timeout(durationSeconds: number) { 7 | this._timeout = durationSeconds; 8 | this.clientManager.timeout = this._timeout; 9 | } 10 | 11 | public constructor( 12 | serverAddress: IUDPAddr, 13 | privateKey: Uint8Array, 14 | protocolId: Long, 15 | maxClients: number 16 | ) { 17 | this.serverAddr = serverAddress; 18 | this.protocolId = protocolId; 19 | this.privateKey = privateKey; 20 | this.maxClients = maxClients; 21 | 22 | this.globalSequence = new Long(1 << 31, 0); 23 | this._timeout = TIMEOUT_SECONDS; 24 | this.clientManager = new ClientManager(this.timeout, maxClients); 25 | 26 | // set allowed packets for this server 27 | this.allowedPackets = new Uint8Array(PacketType.numPackets); 28 | this.allowedPackets[PacketType.connectionRequest] = 1; 29 | this.allowedPackets[PacketType.connectionResponse] = 1; 30 | this.allowedPackets[PacketType.connectionKeepAlive] = 1; 31 | this.allowedPackets[PacketType.connectionPayload] = 1; 32 | this.allowedPackets[PacketType.connectionDisconnect] = 1; 33 | 34 | this.challengeKey = Utils.generateKey(); 35 | this.serverConn = new NetcodeConn(); 36 | this.serverConn.setReadBuffer(SOCKET_RCVBUF_SIZE * this.maxClients); 37 | this.serverConn.setWriteBuffer(SOCKET_SNDBUF_SIZE * this.maxClients); 38 | this.serverConn.setRecvHandler(this.handleNetcodeData); 39 | 40 | this._tempBuffer = new Uint8Array(MAX_PACKET_BYTES); 41 | } 42 | 43 | public setClientConnectHandler(handler: ClientConnectionHandler) { 44 | this.clientManager.clientConnectHandler = handler; 45 | } 46 | 47 | public setClientDisconnectHandler(handler: ClientConnectionHandler) { 48 | this.clientManager.clientDisconnectHandler = handler; 49 | } 50 | 51 | public setAllowedPackets(allowedPackets: Uint8Array) { 52 | this.allowedPackets = allowedPackets; 53 | } 54 | 55 | public setIgnoreRequests(val: boolean) { 56 | this.ignoreRequests = val; 57 | } 58 | 59 | public setIgnoreResponses(val: boolean) { 60 | this.ignoreResponses = val; 61 | } 62 | 63 | // increments the challenge sequence and returns the un-incremented value 64 | public incChallengeSequence(): Long { 65 | const original = this.challengeSequence.clone(); 66 | this.challengeSequence.plusOne(); 67 | return original; 68 | } 69 | 70 | public incGlobalSequence(): Long { 71 | const original = this.globalSequence.clone(); 72 | this.globalSequence.plusOne(); 73 | return original; 74 | } 75 | 76 | public getConnectedClientIds(): Long[] { 77 | return this.clientManager.getConnectedClients(); 78 | } 79 | 80 | public getMaxClients(): number { 81 | return this.maxClients; 82 | } 83 | 84 | public getConnectedClientCount(): number { 85 | return this.clientManager.getConnectedClientCount(); 86 | } 87 | 88 | public getClientUserData(clientId: Long): Uint8Array { 89 | const i = this.getClientIndexByClientId(clientId); 90 | if (i > -1) { 91 | return this.clientManager.instances[i].userData; 92 | } 93 | } 94 | 95 | public listen(createUdpConn: UDPConnCreator) { 96 | this.running = true; 97 | this.serverConn.listen(createUdpConn, this.serverAddr); 98 | } 99 | 100 | public sendPayloads(payloadData: Uint8Array) { 101 | if (!this.running) { 102 | return; 103 | } 104 | this.clientManager.sendPayloads(payloadData, this.serverTime); 105 | } 106 | 107 | // Sends the payload to the client specified by their clientId. 108 | public sendPayloadToClient(clientId: Long, payloadData: Uint8Array) { 109 | const clientIndex = this.getClientIndexByClientId(clientId); 110 | if (clientIndex !== -1) { 111 | this.clientManager.sendPayloadToInstance( 112 | clientIndex, 113 | payloadData, 114 | this.serverTime 115 | ); 116 | } 117 | } 118 | 119 | public getClientIndexByClientId(clientId: Long): number { 120 | if (!this.running) { 121 | return -1; 122 | } 123 | return this.clientManager.findClientIndexById(clientId); 124 | } 125 | 126 | public update(time: number) { 127 | if (!this.running) { 128 | return; 129 | } 130 | 131 | this.serverTime = time; 132 | 133 | for (const p of this.packets) { 134 | this.onPacketData(p.data, p.from); 135 | } 136 | this.clientManager.sendKeepAlives(this.serverTime); 137 | this.clientManager.checkTimeouts(this.serverTime); 138 | } 139 | 140 | public stop() { 141 | if (!this.running) { 142 | return; 143 | } 144 | this.clientManager.disconnectClients(this.serverTime); 145 | this.running = false; 146 | this.maxClients = 0; 147 | this.globalSequence.setZero(); 148 | this.challengeSequence.setZero(); 149 | this.challengeKey.fill(0); 150 | this.clientManager.resetCryptoEntries(); 151 | this.clientManager.resetTokenEntries(); 152 | this.running = false; 153 | this.serverConn.close(); 154 | } 155 | 156 | public recvPayload(clientIndex: number): { 157 | data: Uint8Array; 158 | sequance: Long; 159 | } { 160 | const packet = 161 | this.clientManager.instances[clientIndex].packetQueue.pop(); 162 | if (!packet) { 163 | return null; 164 | } 165 | const p = packet as PayloadPacket; 166 | if (p.getType() === PacketType.connectionPayload) { 167 | return { data: p.payloadData, sequance: p.getSequence() }; 168 | } else { 169 | return null; 170 | } 171 | } 172 | 173 | // write the netcodeData to our buffered packet channel. The NetcodeConn verifies 174 | // that the recv'd data is > 0 < maxBytes and is of a valid packet type before 175 | // this is even called. 176 | // NOTE: we will block the netcodeConn from processing which is what we want since 177 | // we want to synchronize access from the Update call. 178 | private handleNetcodeData(packetData: INetcodeData) { 179 | this.packets.push(packetData); 180 | } 181 | 182 | private onPacketData(packetData: Uint8Array, addr: IUDPAddr) { 183 | let replayProtection: ReplayProtection; 184 | 185 | if (!this.running) { 186 | return; 187 | } 188 | 189 | const size = packetData.length; 190 | 191 | let encryptionIndex = -1; 192 | const clientIndex = this.clientManager.findClientIndexByAddress(addr); 193 | if (clientIndex != -1) { 194 | encryptionIndex = 195 | this.clientManager.findEncryptionIndexByClientIndex(clientIndex); 196 | } else { 197 | encryptionIndex = this.clientManager.findEncryptionEntryIndex( 198 | addr, 199 | this.serverTime 200 | ); 201 | } 202 | const readPacketKey = 203 | this.clientManager.getEncryptionEntryRecvKey(encryptionIndex); 204 | 205 | const timestamp = Date.now(); 206 | 207 | const packet = PacketFactory.create(packetData); 208 | if (clientIndex !== -1) { 209 | const client = this.clientManager.instances[clientIndex]; 210 | replayProtection = client.replayProtection; 211 | } 212 | 213 | const err = packet.read(packetData, size, { 214 | protocolId: this.protocolId, 215 | currentTimestamp: timestamp, 216 | readPacketKey, 217 | privateKey: this.privateKey, 218 | allowedPackets: this.allowedPackets, 219 | replayProtection, 220 | }); 221 | if (err !== Errors.none) { 222 | console.error('error reading packet: %s from %s', err, addr); 223 | return; 224 | } 225 | 226 | this.processPacket(clientIndex, encryptionIndex, packet, addr); 227 | } 228 | 229 | private processPacket( 230 | clientIndex: number, 231 | encryptionIndex: number, 232 | packet: IPacket, 233 | addr: IUDPAddr 234 | ) { 235 | switch (packet.getType()) { 236 | case PacketType.connectionRequest: 237 | { 238 | if (this.ignoreRequests) { 239 | return; 240 | } 241 | console.log('server received connection request from %s', addr); 242 | this.processConnectionRequest(packet as RequestPacket, addr); 243 | } 244 | break; 245 | case PacketType.connectionResponse: { 246 | if (this.ignoreResponses) { 247 | return; 248 | } 249 | console.log('server received connection response from %s', addr); 250 | this.processConnectionResponse( 251 | clientIndex, 252 | encryptionIndex, 253 | packet as ResponsePacket, 254 | addr 255 | ); 256 | break; 257 | } 258 | case PacketType.connectionKeepAlive: 259 | { 260 | if (clientIndex == -1) { 261 | return; 262 | } 263 | const client = this.clientManager.instances[clientIndex]; 264 | client.lastRecvTime = this.serverTime; 265 | 266 | if (!client.confirmed) { 267 | client.confirmed = true; 268 | console.log( 269 | 'server confirmed connection to client %d', 270 | client.clientId 271 | ); 272 | } 273 | } 274 | break; 275 | case PacketType.connectionPayload: 276 | { 277 | if (clientIndex == -1) { 278 | return; 279 | } 280 | const client = this.clientManager.instances[clientIndex]; 281 | client.lastRecvTime = this.serverTime; 282 | 283 | if (!client.confirmed) { 284 | client.confirmed = true; 285 | console.log( 286 | 'server confirmed connection to client %d', 287 | client.clientId 288 | ); 289 | } 290 | 291 | client.packetQueue.push(packet); 292 | } 293 | break; 294 | case PacketType.connectionDisconnect: 295 | { 296 | if (clientIndex == -1) { 297 | return; 298 | } 299 | const client = this.clientManager.instances[clientIndex]; 300 | console.log( 301 | 'server received disconnect packet from client %d', 302 | client.clientId 303 | ); 304 | this.clientManager.disconnectClient(client, false, this.serverTime); 305 | } 306 | break; 307 | } 308 | } 309 | 310 | private processConnectionRequest(packet: RequestPacket, addr: IUDPAddr) { 311 | if (packet.token.sharedTokenData.serverAddrs.length === 0) { 312 | console.log( 313 | 'server ignored connection request.' + 314 | 'server address not in connect token whitelist' 315 | ); 316 | return; 317 | } 318 | 319 | let addrFound = false; 320 | for (const tokenAddr of packet.token.sharedTokenData.serverAddrs) { 321 | if (Utils.addressEqual(this.serverAddr, tokenAddr)) { 322 | addrFound = true; 323 | break; 324 | } 325 | } 326 | 327 | if (!addrFound) { 328 | console.log( 329 | 'server ignored connection request. server address not in connect token whitelist' 330 | ); 331 | return; 332 | } 333 | 334 | let clientIndex = this.clientManager.findClientIndexByAddress(addr); 335 | if (clientIndex !== -1) { 336 | console.log( 337 | 'server ignored connection request. a client with this address is already connected' 338 | ); 339 | return; 340 | } 341 | 342 | clientIndex = this.clientManager.findClientIndexById( 343 | packet.token.clientId 344 | ); 345 | if (clientIndex !== -1) { 346 | console.log( 347 | 'server ignored connection request. a client with this id has already been used' 348 | ); 349 | return; 350 | } 351 | 352 | if ( 353 | !this.clientManager.findOrAddTokenEntry( 354 | packet.token.mac, 355 | addr, 356 | this.serverTime 357 | ) 358 | ) { 359 | console.log( 360 | 'server ignored connection request. connect token has already been used' 361 | ); 362 | return; 363 | } 364 | 365 | if (this.clientManager.getConnectedClientCount() === this.maxClients) { 366 | console.log('server denied connection request. server is full'); 367 | this.sendDeniedPacket(packet.token.sharedTokenData.serverKey, addr); 368 | return; 369 | } 370 | 371 | if ( 372 | !this.clientManager.addEncryptionMapping( 373 | packet.token, 374 | addr, 375 | this.serverTime, 376 | this.serverTime + this.timeout 377 | ) 378 | ) { 379 | console.log( 380 | 'server ignored connection request. failed to add encryption mapping' 381 | ); 382 | return; 383 | } 384 | 385 | this.sendChallengePacket(packet, addr); 386 | } 387 | 388 | private sendChallengePacket(requestPacket: RequestPacket, addr: IUDPAddr) { 389 | const challenge = new ChallengeToken(requestPacket.token.clientId); 390 | const challengeBuf = challenge.write(requestPacket.token.userData); 391 | const challengeSequence = this.incChallengeSequence(); 392 | 393 | ChallengeToken.encrypt( 394 | challengeBuf, 395 | challengeSequence, 396 | this.challengeKey 397 | ); 398 | const challengePacket = new ChallengePacket(); 399 | challengePacket.setProperties(challengeSequence, challengeBuf); 400 | 401 | const buffer = this._tempBuffer; 402 | const bytesWritten = challengePacket.write( 403 | buffer, 404 | this.protocolId, 405 | this.incGlobalSequence(), 406 | requestPacket.token.sharedTokenData.serverKey 407 | ); 408 | if (bytesWritten <= 0) { 409 | console.log('server error while writing challenge packet'); 410 | return; 411 | } 412 | this.sendGlobalPacket(buffer.subarray(0, bytesWritten), addr); 413 | } 414 | 415 | public sendGlobalPacket(packetBuffer: Uint8Array, addr: IUDPAddr) { 416 | this.serverConn.writeTo(packetBuffer, addr); 417 | } 418 | 419 | private sendDeniedPacket(sendKey: Uint8Array, addr: IUDPAddr) { 420 | const deniedPacket = new DeniedPacket(); 421 | const packetBuffer = this._tempBuffer; 422 | const bytesWritten = deniedPacket.write( 423 | packetBuffer, 424 | this.protocolId, 425 | this.incGlobalSequence(), 426 | sendKey 427 | ); 428 | if (bytesWritten <= 0) { 429 | console.log('error creating denied packet'); 430 | return; 431 | } 432 | this.sendGlobalPacket(packetBuffer.subarray(0, bytesWritten), addr); 433 | } 434 | 435 | private processConnectionResponse( 436 | clientIndex: number, 437 | encryptionIndex: number, 438 | packet: ResponsePacket, 439 | addr: IUDPAddr 440 | ) { 441 | const tokenBuffer = ChallengeToken.decrypt( 442 | packet.tokenData, 443 | packet.challengeTokenSequence, 444 | this.challengeKey 445 | ); 446 | if (!tokenBuffer) { 447 | console.log('failed to decrypt challenge token: %s'); 448 | return; 449 | } 450 | const challengeToken = new ChallengeToken(); 451 | const err = challengeToken.read(tokenBuffer); 452 | if (err !== Errors.none) { 453 | console.log('failed to read challenge token: %s', Errors[err]); 454 | return; 455 | } 456 | const sendKey = 457 | this.clientManager.getEncryptionEntrySendKey(encryptionIndex); 458 | if (!sendKey) { 459 | console.log('server ignored connection response. no packet send key'); 460 | return; 461 | } 462 | if (this.clientManager.findClientIndexByAddress(addr) !== -1) { 463 | console.log( 464 | 'server ignored connection response. a client with this address is already connected' 465 | ); 466 | return; 467 | } 468 | if ( 469 | this.clientManager.findClientIndexById(challengeToken.clientID) !== -1 470 | ) { 471 | console.log( 472 | 'server ignored connection response. a client with this id is already connected' 473 | ); 474 | return; 475 | } 476 | if (this.clientManager.getConnectedClientCount() === this.maxClients) { 477 | console.log('server denied connection response. server is full'); 478 | this.sendDeniedPacket(sendKey, addr); 479 | return; 480 | } 481 | this.connectClient(encryptionIndex, challengeToken, addr); 482 | } 483 | 484 | private connectClient( 485 | encryptionIndex: number, 486 | challengeToken: ChallengeToken, 487 | addr: IUDPAddr 488 | ) { 489 | if (this.clientManager.getConnectedClientCount() > this.maxClients) { 490 | console.warn('maxium number of clients reached'); 491 | return; 492 | } 493 | this.clientManager.setEncryptionEntryExpiration(encryptionIndex, -1); 494 | const client = this.clientManager.connectClient(addr, challengeToken); 495 | if (!client) { 496 | return; 497 | } 498 | client.serverConn = this.serverConn; 499 | client.encryptionIndex = encryptionIndex; 500 | client.protocolId = this.protocolId; 501 | client.lastSendTime = this.serverTime; 502 | client.lastRecvTime = this.serverTime; 503 | console.log( 504 | 'server accepted client %d from %s in slot: %d', 505 | client.clientId, 506 | addr, 507 | client.clientIndex 508 | ); 509 | this.sendKeepAlive(client); 510 | } 511 | 512 | private sendKeepAlive(client: ClientInstance) { 513 | const clientIndex = client.clientIndex; 514 | const packet = new KeepAlivePacket(); 515 | packet.setProperties(clientIndex, this.maxClients); 516 | if ( 517 | !this.clientManager.touchEncryptionEntry( 518 | client.encryptionIndex, 519 | client.address, 520 | this.serverTime 521 | ) 522 | ) { 523 | console.warn( 524 | 'error: encryption mapping is out of date for client %d encIndex: %d addr: %s', 525 | clientIndex, 526 | client.encryptionIndex, 527 | client.address 528 | ); 529 | return; 530 | } 531 | const writePacketKey = this.clientManager.getEncryptionEntrySendKey( 532 | client.encryptionIndex 533 | ); 534 | if (!writePacketKey) { 535 | console.error( 536 | 'error: unable to retrieve encryption key for client: %d', 537 | clientIndex 538 | ); 539 | return; 540 | } 541 | client.sendPacket(packet, writePacketKey, this.serverTime); 542 | } 543 | 544 | private serverConn: NetcodeConn; 545 | private serverAddr: IUDPAddr; 546 | 547 | private serverTime: number; 548 | private running: boolean; 549 | private maxClients: number; 550 | private connectedClients: number; 551 | private _timeout: number; 552 | 553 | private clientManager: ClientManager; 554 | private globalSequence: Long; 555 | 556 | private ignoreRequests: boolean; 557 | private ignoreResponses: boolean; 558 | private allowedPackets: Uint8Array; 559 | private protocolId: Long; 560 | 561 | private privateKey: Uint8Array; 562 | private challengeKey: Uint8Array; 563 | 564 | private challengeSequence: Long; 565 | private recvByte: number; 566 | 567 | private packets: INetcodeData[] = []; 568 | private _tempBuffer: Uint8Array; 569 | } 570 | } 571 | -------------------------------------------------------------------------------- /tests/bytebuffer_test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var { Netcode } = require('../dist/node/netcode'); 3 | 4 | describe('ByteBuffer tests', function () { 5 | it('correct write/read bytes', function () { 6 | var bb = new Netcode.ByteBuffer(new Uint8Array(64)); 7 | var input = new Uint8Array(Netcode.VERSION_INFO_BYTES_ARRAY); 8 | assert.equal(bb.position, 0, 'oh no'); 9 | bb.writeBytes(input); 10 | assert.equal(bb.position, 13, 'oh no'); 11 | 12 | bb.clearPosition(); 13 | assert.equal(bb.position, 0, 'oh no'); 14 | var output = bb.readBytes(13); 15 | output.forEach(function (item, index) { 16 | if (Netcode.VERSION_INFO_BYTES_ARRAY[index] !== item) { 17 | assert(false, 'oh no'); 18 | } 19 | }); 20 | assert.equal(bb.position, 13, 'oh no'); 21 | }); 22 | 23 | it('correct write/read byte', function () { 24 | var array = new Uint8Array(1); 25 | let bb = new Netcode.ByteBuffer(array); 26 | bb.writeUint8(0xfe); 27 | assert.equal(bb.position, 1, 'oh no'); 28 | 29 | bb.clearPosition(); 30 | var out = bb.readUint8(); 31 | assert.equal(bb.position, 1, 'oh no'); 32 | assert.equal(out, 0xfe, 'oh no'); 33 | }); 34 | 35 | it('correct write/read int8', function () { 36 | var array = new Uint8Array(1); 37 | let bb = new Netcode.ByteBuffer(array); 38 | bb.writeInt8(-3); 39 | assert.equal(bb.position, 1, 'oh no'); 40 | 41 | bb.clearPosition(); 42 | var out = bb.readInt8(); 43 | assert.equal(bb.position, 1, 'oh no'); 44 | assert.equal(out, -3, 'oh no'); 45 | }); 46 | 47 | it('correct write/read uint16', function () { 48 | var array = new Uint8Array(2); 49 | let bb = new Netcode.ByteBuffer(array); 50 | bb.writeUint16(9999); 51 | assert.equal(bb.position, 2, 'oh no'); 52 | 53 | bb.clearPosition(); 54 | var out = bb.readUint16(); 55 | assert.equal(bb.position, 2, 'oh no'); 56 | assert.equal(out, 9999, 'oh no'); 57 | }); 58 | 59 | it('correct write/read int16', function () { 60 | var array = new Uint8Array(2); 61 | let bb = new Netcode.ByteBuffer(array); 62 | bb.writeInt16(-9999); 63 | assert.equal(bb.position, 2, 'oh no'); 64 | 65 | bb.clearPosition(); 66 | var out = bb.readInt16(); 67 | assert.equal(bb.position, 2, 'oh no'); 68 | assert.equal(out, -9999, 'oh no'); 69 | }); 70 | 71 | it('correct write/read uint32', function () { 72 | var array = new Uint8Array(4); 73 | let bb = new Netcode.ByteBuffer(array); 74 | bb.writeUint32(99999); 75 | assert.equal(bb.position, 4, 'oh no'); 76 | 77 | bb.clearPosition(); 78 | var out = bb.readUint32(); 79 | assert.equal(bb.position, 4, 'oh no'); 80 | assert.equal(out, 99999, 'oh no'); 81 | }); 82 | 83 | it('correct write/read int32', function () { 84 | var array = new Uint8Array(4); 85 | let bb = new Netcode.ByteBuffer(array); 86 | bb.writeUint32(-99999); 87 | assert.equal(bb.position, 4, 'oh no'); 88 | 89 | bb.clearPosition(); 90 | var out = bb.readInt32(); 91 | assert.equal(bb.position, 4, 'oh no'); 92 | assert.equal(out, -99999, 'oh no'); 93 | }); 94 | 95 | it('correct write/read uint64', function () { 96 | var array = new Uint8Array(8); 97 | let bb = new Netcode.ByteBuffer(array); 98 | const l = new Netcode.Long(65534, 99999); 99 | bb.writeUint64(l); 100 | assert.equal(bb.position, 8, 'oh no'); 101 | 102 | bb.clearPosition(); 103 | var out = bb.readUint64(); 104 | assert.equal(bb.position, 8, 'oh no'); 105 | assert.equal(out.equals(l), true, 'oh no'); 106 | }); 107 | 108 | it('test Long', function () { 109 | var n = 3293924239; 110 | var l = Netcode.Long.fromNumber(n); 111 | assert.equal(l.toNumber(), n, 'oh no'); 112 | 113 | var l2 = new Netcode.Long(2432, 9923); 114 | var n2 = l2.toNumber(); 115 | var l3 = Netcode.Long.fromNumber(n2); 116 | assert.equal(l2.equals(l3), true, 'on no'); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /tests/client_manager_test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var { Netcode } = require('../dist/node/netcode'); 3 | 4 | var TEST_PROTOCOL_ID = Netcode.Long.fromNumber(0x1122334455667788); 5 | var TEST_CONNECT_TOKEN_EXPIRY = 30; 6 | var TEST_SERVER_PORT = 40000; 7 | var TEST_CLIENT_ID = Netcode.Long.fromNumber(0x1); 8 | var TEST_SEQUENCE_START = Netcode.Long.fromNumber(1000); 9 | var TEST_TIMEOUT_SECONDS = 15; 10 | 11 | describe('ClientManager tests', function () { 12 | it('test new ClientManager', function () { 13 | var timeout = 4; 14 | var maxClient = 2; 15 | const mgr = new Netcode.ClientManager(timeout, maxClient); 16 | if (mgr.findFreeClientIndex() === -1) { 17 | assert.fail('free client index should not return -1 when empty'); 18 | } 19 | var addr = { 20 | ip: new Uint8Array(), 21 | port: 0, 22 | }; 23 | if (mgr.findClientIndexByAddress(addr) !== -1) { 24 | assert.fail('empty address should get no client'); 25 | } 26 | if (mgr.findClientIndexById(0) !== -1) { 27 | assert.fail('should not have any clients'); 28 | } 29 | }); 30 | 31 | function ipStringToBytes(ip) { 32 | var addr = Netcode.Utils.stringToIPV4Address(ip); 33 | return addr.ip; 34 | } 35 | 36 | it('test add encryption mapping', function () { 37 | var timeout = 4; 38 | var maxClient = 2; 39 | var servers = []; 40 | const ip = ipStringToBytes('127.0.0.1'); 41 | servers.push({ 42 | ip, 43 | port: 40000, 44 | }); 45 | var addr = { ip, port: 62424 }; 46 | var addr2 = { ip, port: 62425 }; 47 | var overAddrs = []; 48 | for (var i = 0; i < maxClient * 8; i++) { 49 | overAddrs.push({ ip, port: 6000 + i }); 50 | } 51 | 52 | var key = Netcode.Utils.generateKey(); 53 | var userData = Netcode.Utils.getRandomBytes(Netcode.USER_DATA_BYTES); 54 | var connectToken = new Netcode.ConnectToken(); 55 | connectToken.generate( 56 | TEST_CLIENT_ID, 57 | servers, 58 | TEST_PROTOCOL_ID, 59 | TEST_CONNECT_TOKEN_EXPIRY, 60 | TEST_TIMEOUT_SECONDS, 61 | TEST_SEQUENCE_START, 62 | userData, 63 | key 64 | ); 65 | 66 | var mgr = new Netcode.ClientManager(timeout, maxClient); 67 | 68 | var serverTime = 1; 69 | var expireTime = 1; 70 | if ( 71 | !mgr.addEncryptionMapping( 72 | connectToken.privateData, 73 | addr, 74 | serverTime, 75 | expireTime 76 | ) 77 | ) { 78 | assert.fail('error adding encryption mapping'); 79 | } 80 | 81 | // add it again 82 | if ( 83 | !mgr.addEncryptionMapping( 84 | connectToken.privateData, 85 | addr, 86 | serverTime, 87 | expireTime 88 | ) 89 | ) { 90 | assert.fail('error re-adding encryption mapping'); 91 | } 92 | if ( 93 | !mgr.addEncryptionMapping( 94 | connectToken.privateData, 95 | addr2, 96 | serverTime, 97 | expireTime 98 | ) 99 | ) { 100 | assert.fail('error adding 2nd encryption mapping'); 101 | } 102 | var failed = false; 103 | for (var i = 0; i < overAddrs.length; i++) { 104 | if ( 105 | mgr.addEncryptionMapping( 106 | connectToken.privateData, 107 | overAddrs[i], 108 | serverTime, 109 | expireTime 110 | ) 111 | ) { 112 | failed = true; 113 | } 114 | } 115 | if (!failed) { 116 | assert.fail( 117 | 'error we added more encryption mappings than should have been allowed' 118 | ); 119 | } 120 | }); 121 | 122 | it('test add encryption mapping time out', function () { 123 | var timeout = 4; 124 | var maxClient = 2; 125 | var servers = []; 126 | var ip = ipStringToBytes('127.0.0.1'); 127 | servers.push({ ip, port: 40000 }); 128 | var addr = { ip, port: 62424 }; 129 | 130 | var key = Netcode.Utils.generateKey(); 131 | var userData = Netcode.Utils.getRandomBytes(Netcode.USER_DATA_BYTES); 132 | var connectToken = new Netcode.ConnectToken(); 133 | connectToken.generate( 134 | TEST_CLIENT_ID, 135 | servers, 136 | TEST_PROTOCOL_ID, 137 | TEST_CONNECT_TOKEN_EXPIRY, 138 | TEST_TIMEOUT_SECONDS, 139 | TEST_SEQUENCE_START, 140 | userData, 141 | key 142 | ); 143 | 144 | var mgr = new Netcode.ClientManager(timeout, maxClient); 145 | 146 | var serverTime = 1; 147 | var expireTime = 1; 148 | if ( 149 | !mgr.addEncryptionMapping( 150 | connectToken.privateData, 151 | addr, 152 | serverTime, 153 | expireTime 154 | ) 155 | ) { 156 | assert.fail('error adding encryption mapping'); 157 | } 158 | 159 | var idx = mgr.findEncryptionEntryIndex(addr, serverTime); 160 | if (idx === -1) { 161 | assert.fail('error getting encryption entry index'); 162 | } 163 | if (!mgr.setEncryptionEntryExpiration(idx, 0.1)) { 164 | assert.fail('error setting entry expiration'); 165 | } 166 | // remove the client. 167 | mgr.checkTimeouts(serverTime); 168 | idx = mgr.findEncryptionEntryIndex(addr, serverTime); 169 | if (idx !== -1) { 170 | assert.fail( 171 | 'error got encryption entry index when it should have been removed' 172 | ); 173 | } 174 | }); 175 | 176 | it('test disconnect client', function () { 177 | var timeout = 4; 178 | var maxClient = 2; 179 | var servers = []; 180 | var ip = ipStringToBytes('127.0.0.1'); 181 | servers.push({ ip, port: 40000 }); 182 | var addr = { ip, port: 62424 }; 183 | 184 | var key = Netcode.Utils.generateKey(); 185 | var userData = Netcode.Utils.getRandomBytes(Netcode.USER_DATA_BYTES); 186 | var connectToken = new Netcode.ConnectToken(); 187 | connectToken.generate( 188 | TEST_CLIENT_ID, 189 | servers, 190 | TEST_PROTOCOL_ID, 191 | TEST_CONNECT_TOKEN_EXPIRY, 192 | TEST_TIMEOUT_SECONDS, 193 | TEST_SEQUENCE_START, 194 | userData, 195 | key 196 | ); 197 | 198 | var mgr = new Netcode.ClientManager(timeout, maxClient); 199 | 200 | var serverTime = 1; 201 | var expireTime = 1; 202 | if ( 203 | !mgr.addEncryptionMapping( 204 | connectToken.privateData, 205 | addr, 206 | serverTime, 207 | expireTime 208 | ) 209 | ) { 210 | assert.fail('error adding encryption mapping'); 211 | } 212 | 213 | var token = new Netcode.ChallengeToken(TEST_CLIENT_ID); 214 | var client = mgr.connectClient(addr, token); 215 | if (!client.connected) { 216 | assert.fail('error client should be connected'); 217 | } 218 | assert.equal(client.clientId, TEST_CLIENT_ID, 'client id should be same'); 219 | var clientIndex = mgr.findClientIndexById(TEST_CLIENT_ID); 220 | if (clientIndex === -1) { 221 | assert.fail('error finding client index'); 222 | } 223 | assert.equal( 224 | clientIndex, 225 | client.clientIndex, 226 | 'client index should be same' 227 | ); 228 | 229 | if (mgr.getConnectedClientCount() !== 1) { 230 | assert.fail('error client connected count should be 1'); 231 | } 232 | 233 | mgr.disconnectClientByIndex(clientIndex, false, serverTime); 234 | if (client.connected) { 235 | assert.fail('error client should be disconnected'); 236 | } 237 | }); 238 | }); 239 | -------------------------------------------------------------------------------- /tests/packet_test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var { Netcode } = require('../dist/node/netcode'); 3 | 4 | var TEST_PROTOCOL_ID = Netcode.Long.fromNumber(0x1122334455667788); 5 | var TEST_CONNECT_TOKEN_EXPIRY = 30; 6 | var TEST_SEQUENCE_START = Netcode.Long.fromNumber(1000); 7 | var TEST_TIMEOUT_SECONDS = 15; 8 | var TEST_PRIVATE_KEY = new Uint8Array([ 9 | 0x60, 10 | 0x6a, 11 | 0xbe, 12 | 0x6e, 13 | 0xc9, 14 | 0x19, 15 | 0x10, 16 | 0xea, 17 | 0x9a, 18 | 0x65, 19 | 0x62, 20 | 0xf6, 21 | 0x6f, 22 | 0x2b, 23 | 0x30, 24 | 0xe4, 25 | 0x43, 26 | 0x71, 27 | 0xd6, 28 | 0x2c, 29 | 0xd1, 30 | 0x99, 31 | 0x27, 32 | 0x26, 33 | 0x6b, 34 | 0x3c, 35 | 0x60, 36 | 0xf4, 37 | 0xb7, 38 | 0x15, 39 | 0xab, 40 | 0xa1, 41 | ]); 42 | var TEST_CLIENT_ID = Netcode.Long.fromNumber(0x1); 43 | 44 | function assertBytesEqual(a1, a2, str) { 45 | assert.equal(Netcode.Utils.arrayEqual(a1, a2), true, str); 46 | } 47 | 48 | describe('Test Packet', function () { 49 | it('test sequence', function () { 50 | assert.equal( 51 | Netcode.PacketHelper.sequenceNumberBytesRequired(new Netcode.Long(0, 0)), 52 | 1, 53 | 'oh no' 54 | ); 55 | assert.equal( 56 | Netcode.PacketHelper.sequenceNumberBytesRequired( 57 | Netcode.Long.fromNumber(0x11) 58 | ), 59 | 1, 60 | 'oh no' 61 | ); 62 | assert.equal( 63 | Netcode.PacketHelper.sequenceNumberBytesRequired( 64 | Netcode.Long.fromNumber(0x1122) 65 | ), 66 | 2, 67 | 'oh no' 68 | ); 69 | assert.equal( 70 | Netcode.PacketHelper.sequenceNumberBytesRequired( 71 | Netcode.Long.fromNumber(0x112233) 72 | ), 73 | 3, 74 | 'oh no' 75 | ); 76 | assert.equal( 77 | Netcode.PacketHelper.sequenceNumberBytesRequired( 78 | Netcode.Long.fromNumber(0x11223344) 79 | ), 80 | 4, 81 | 'oh no' 82 | ); 83 | assert.equal( 84 | Netcode.PacketHelper.sequenceNumberBytesRequired( 85 | new Netcode.Long(0x11223344, 0x55) 86 | ), 87 | 5, 88 | 'oh no' 89 | ); 90 | assert.equal( 91 | Netcode.PacketHelper.sequenceNumberBytesRequired( 92 | Netcode.Long.fromNumber(0x112233445566) 93 | ), 94 | 6, 95 | 'oh no' 96 | ); 97 | assert.equal( 98 | Netcode.PacketHelper.sequenceNumberBytesRequired( 99 | Netcode.Long.fromNumber(0x11223344556677) 100 | ), 101 | 7, 102 | 'oh no' 103 | ); 104 | assert.equal( 105 | Netcode.PacketHelper.sequenceNumberBytesRequired( 106 | Netcode.Long.fromNumber(0x1122334455667788) 107 | ), 108 | 8, 109 | 'oh no' 110 | ); 111 | }); 112 | 113 | it('test sequence write/read', function () { 114 | var p = new Netcode.DisconnectPacket(); 115 | var buf = Netcode.ByteBuffer.allocate(4); 116 | var prefixByte = Netcode.PacketHelper.writePacketPrefix( 117 | p, 118 | buf, 119 | TEST_SEQUENCE_START 120 | ); 121 | assert.equal(prefixByte, 38); 122 | buf.clearPosition(); 123 | assert.equal(buf.readUint8(), 38); 124 | assert.equal(buf.readUint16(), TEST_SEQUENCE_START.low); 125 | buf.clearPosition(); 126 | buf.skipPosition(1); 127 | var seq = Netcode.PacketHelper.readSequence(buf, 100, prefixByte); 128 | assert.equal(seq.equals(TEST_SEQUENCE_START), true); 129 | 130 | var buf2 = Netcode.ByteBuffer.allocate(9); 131 | var maxSeq = new Netcode.Long(0xffffffff, 0xffffffff); 132 | var prefixByte2 = Netcode.PacketHelper.writePacketPrefix(p, buf2, maxSeq); 133 | assert.equal(prefixByte2, (8 << 4) + 6); 134 | buf2.clearPosition(); 135 | buf2.skipPosition(1); 136 | assert.equal(buf2.readUint64().equals(maxSeq), true); 137 | buf2.clearPosition(); 138 | buf2.skipPosition(1); 139 | var seq2 = Netcode.PacketHelper.readSequence(buf2, 100, prefixByte2); 140 | assert.equal(seq2.equals(maxSeq), true); 141 | }); 142 | 143 | it('test connection request packet', function () { 144 | var tokenKey = Netcode.Utils.generateKey(); 145 | var builds = testBuildRequestPacket(tokenKey); 146 | var inputPacket = builds[0]; 147 | var decryptedToken = builds[1]; 148 | 149 | var buffer = new Uint8Array(2048); 150 | var packetKey = Netcode.Utils.generateKey(); 151 | var bytesWritten = inputPacket.write( 152 | buffer, 153 | TEST_PROTOCOL_ID, 154 | TEST_SEQUENCE_START, 155 | packetKey 156 | ); 157 | assert.equal(bytesWritten > 0, true); 158 | var allowedPackets = new Uint8Array(Netcode.PacketType.numPackets); 159 | for (let i = 0; i < allowedPackets.length; i++) { 160 | allowedPackets[i] = 1; 161 | } 162 | 163 | var outPacket = new Netcode.RequestPacket(); 164 | const err = outPacket.read(buffer, bytesWritten, { 165 | protocolId: TEST_PROTOCOL_ID, 166 | currentTimestamp: Date.now(), 167 | readPacketKey: packetKey, 168 | privateKey: tokenKey, 169 | allowedPackets, 170 | replayProtection: null, 171 | }); 172 | assert.equal(err, Netcode.Errors.none, Netcode.Errors[err]); 173 | assertBytesEqual(outPacket._versionInfo, inputPacket._versionInfo, 'oh no'); 174 | assertBytesEqual( 175 | outPacket._versionInfo, 176 | Netcode.VERSION_INFO_BYTES_ARRAY, 177 | 'oh no' 178 | ); 179 | assert.equal(outPacket._protocolID.equals(inputPacket._protocolID), true); 180 | assert.equal( 181 | outPacket._connectTokenExpireTimestamp.equals( 182 | inputPacket._connectTokenExpireTimestamp 183 | ), 184 | true 185 | ); 186 | assert.equal( 187 | outPacket._connectTokenSequence.equals(inputPacket._connectTokenSequence), 188 | true 189 | ); 190 | assertBytesEqual(decryptedToken, outPacket._token.tokenData.bytes, 'oh no'); 191 | }); 192 | 193 | it('test connection denied packet', function () { 194 | var inputPacket = new Netcode.DeniedPacket(); 195 | var buffer = new Uint8Array(Netcode.MAX_PACKET_BYTES); 196 | var packetKey = Netcode.Utils.generateKey(); 197 | var bytesWritten = inputPacket.write( 198 | buffer, 199 | TEST_PROTOCOL_ID, 200 | TEST_SEQUENCE_START, 201 | packetKey 202 | ); 203 | assert.equal(bytesWritten > 0, true); 204 | var allowedPackets = new Uint8Array(Netcode.PacketType.numPackets); 205 | for (let i = 0; i < allowedPackets.length; i++) { 206 | allowedPackets[i] = 1; 207 | } 208 | 209 | var outP = new Netcode.DeniedPacket(); 210 | var err = outP.read(buffer, bytesWritten, { 211 | protocolId: TEST_PROTOCOL_ID, 212 | currentTimestamp: Date.now(), 213 | readPacketKey: packetKey, 214 | privateKey: null, 215 | allowedPackets, 216 | replayProtection: null, 217 | }); 218 | assert.equal(err === Netcode.Errors.none, true, 'oh no'); 219 | assert.equal(outP.getSequence().equals(TEST_SEQUENCE_START), true); 220 | }); 221 | 222 | it('test challenge packet', function () { 223 | var inputPacket = new Netcode.ChallengePacket(); 224 | inputPacket.setProperties( 225 | Netcode.Long.ZERO, 226 | Netcode.Utils.getRandomBytes(Netcode.CHALLENGE_TOKEN_BYTES) 227 | ); 228 | var buffer = new Uint8Array(Netcode.MAX_PACKET_BYTES); 229 | var packetKey = Netcode.Utils.generateKey(); 230 | var bytesWritten = inputPacket.write( 231 | buffer, 232 | TEST_PROTOCOL_ID, 233 | TEST_SEQUENCE_START, 234 | packetKey 235 | ); 236 | assert.equal(bytesWritten > 0, true); 237 | var allowedPackets = new Uint8Array(Netcode.PacketType.numPackets); 238 | for (let i = 0; i < allowedPackets.length; i++) { 239 | allowedPackets[i] = 1; 240 | } 241 | 242 | var outP = new Netcode.ChallengePacket(); 243 | var err = outP.read(buffer, bytesWritten, { 244 | protocolId: TEST_PROTOCOL_ID, 245 | currentTimestamp: Date.now(), 246 | readPacketKey: packetKey, 247 | privateKey: null, 248 | allowedPackets, 249 | replayProtection: null, 250 | }); 251 | assert.equal(err === Netcode.Errors.none, true, 'oh no'); 252 | assert.equal(outP.getSequence().equals(TEST_SEQUENCE_START), true); 253 | assert.equal( 254 | outP.challengeTokenSequence.equals(inputPacket.challengeTokenSequence), 255 | true 256 | ); 257 | assertBytesEqual(outP.tokenData, inputPacket.tokenData, 'oh no'); 258 | }); 259 | 260 | it('test connection response packet', function () { 261 | var inputPacket = new Netcode.ResponsePacket(); 262 | inputPacket.setProperties( 263 | Netcode.Long.ZERO, 264 | Netcode.Utils.getRandomBytes(Netcode.CHALLENGE_TOKEN_BYTES) 265 | ); 266 | var buffer = new Uint8Array(Netcode.MAX_PACKET_BYTES); 267 | var packetKey = Netcode.Utils.generateKey(); 268 | var bytesWritten = inputPacket.write( 269 | buffer, 270 | TEST_PROTOCOL_ID, 271 | TEST_SEQUENCE_START, 272 | packetKey 273 | ); 274 | assert.equal(bytesWritten > 0, true); 275 | var allowedPackets = new Uint8Array(Netcode.PacketType.numPackets); 276 | for (let i = 0; i < allowedPackets.length; i++) { 277 | allowedPackets[i] = 1; 278 | } 279 | 280 | var outP = new Netcode.ResponsePacket(); 281 | var err = outP.read(buffer, bytesWritten, { 282 | protocolId: TEST_PROTOCOL_ID, 283 | currentTimestamp: Date.now(), 284 | readPacketKey: packetKey, 285 | privateKey: null, 286 | allowedPackets, 287 | replayProtection: null, 288 | }); 289 | assert.equal(err === Netcode.Errors.none, true, 'oh no'); 290 | assert.equal(outP.getSequence().equals(TEST_SEQUENCE_START), true); 291 | assert.equal( 292 | outP.challengeTokenSequence.equals(inputPacket.challengeTokenSequence), 293 | true 294 | ); 295 | assertBytesEqual(outP.tokenData, inputPacket.tokenData, 'oh no'); 296 | }); 297 | 298 | it('test keep alive packet', function () { 299 | var inputPacket = new Netcode.KeepAlivePacket(); 300 | inputPacket.setProperties(10, 128); 301 | var buffer = new Uint8Array(Netcode.MAX_PACKET_BYTES); 302 | var packetKey = Netcode.Utils.generateKey(); 303 | var bytesWritten = inputPacket.write( 304 | buffer, 305 | TEST_PROTOCOL_ID, 306 | TEST_SEQUENCE_START, 307 | packetKey 308 | ); 309 | assert.equal(bytesWritten > 0, true); 310 | var allowedPackets = new Uint8Array(Netcode.PacketType.numPackets); 311 | for (let i = 0; i < allowedPackets.length; i++) { 312 | allowedPackets[i] = 1; 313 | } 314 | 315 | var outP = new Netcode.KeepAlivePacket(); 316 | var err = outP.read(buffer, bytesWritten, { 317 | protocolId: TEST_PROTOCOL_ID, 318 | currentTimestamp: Date.now(), 319 | readPacketKey: packetKey, 320 | privateKey: null, 321 | allowedPackets, 322 | replayProtection: null, 323 | }); 324 | assert.equal(err === Netcode.Errors.none, true, 'oh no'); 325 | assert.equal(outP.getSequence().equals(TEST_SEQUENCE_START), true); 326 | assert.equal(outP.clientIndex, inputPacket.clientIndex); 327 | assert.equal(outP.maxClients, inputPacket.maxClients); 328 | }); 329 | 330 | it('test payload packet', function () { 331 | var payload = Netcode.Utils.getRandomBytes(Netcode.MAX_PAYLOAD_BYTES); 332 | var inputPacket = new Netcode.PayloadPacket(payload); 333 | var buffer = new Uint8Array(Netcode.MAX_PACKET_BYTES); 334 | var packetKey = Netcode.Utils.generateKey(); 335 | var bytesWritten = inputPacket.write( 336 | buffer, 337 | TEST_PROTOCOL_ID, 338 | TEST_SEQUENCE_START, 339 | packetKey 340 | ); 341 | assert.equal(bytesWritten > 0, true); 342 | var allowedPackets = new Uint8Array(Netcode.PacketType.numPackets); 343 | for (let i = 0; i < allowedPackets.length; i++) { 344 | allowedPackets[i] = 1; 345 | } 346 | 347 | var outP = new Netcode.PayloadPacket(); 348 | var err = outP.read(buffer, bytesWritten, { 349 | protocolId: TEST_PROTOCOL_ID, 350 | currentTimestamp: Date.now(), 351 | readPacketKey: packetKey, 352 | privateKey: null, 353 | allowedPackets, 354 | replayProtection: null, 355 | }); 356 | assert.equal(err === Netcode.Errors.none, true, Netcode.Errors[err]); 357 | assert.equal(outP.getSequence().equals(TEST_SEQUENCE_START), true); 358 | assertBytesEqual(outP.payloadData, inputPacket.payloadData, 'oh no'); 359 | }); 360 | 361 | it('test disconnect packet', function () { 362 | var p = new Netcode.DisconnectPacket(); 363 | var buf = new Uint8Array(Netcode.MAX_PACKET_BYTES); 364 | var key = Netcode.Utils.generateKey(); 365 | var writeLen = p.write(buf, TEST_PROTOCOL_ID, TEST_SEQUENCE_START, key); 366 | assert.equal(writeLen, 3 + Netcode.MAC_BYTES, 'oh no'); 367 | var allowedPackets = new Uint8Array(Netcode.PacketType.numPackets); 368 | for (let i = 0; i < allowedPackets.length; i++) { 369 | allowedPackets[i] = 1; 370 | } 371 | var outP = new Netcode.DisconnectPacket(); 372 | var err = outP.read(buf, writeLen, { 373 | protocolId: TEST_PROTOCOL_ID, 374 | currentTimestamp: Date.now(), 375 | readPacketKey: key, 376 | privateKey: null, 377 | allowedPackets, 378 | replayProtection: null, 379 | }); 380 | assert.equal(err === Netcode.Errors.none, true, 'oh no'); 381 | assert.equal(outP.getSequence().equals(TEST_SEQUENCE_START), true); 382 | }); 383 | }); 384 | 385 | function ipStringToBytes(ip) { 386 | var octets = ip.split('.'); 387 | if (octets.length !== 4) { 388 | console.error('only support ipv4'); 389 | return; 390 | } 391 | const bytes = new Uint8Array(4); 392 | for (var i = 0; i < octets.length; ++i) { 393 | var octet = parseInt(octets[i], 10); 394 | if (Number.isNaN(octet) || octet < 0 || octet > 255) { 395 | throw new Error('Each octet must be between 0 and 255'); 396 | } 397 | bytes[i] = octet; 398 | } 399 | return bytes; 400 | } 401 | 402 | function testBuildRequestPacket(key) { 403 | var addr = { 404 | ip: ipStringToBytes('10.20.30.40'), 405 | port: 40000, 406 | }; 407 | var userData = Netcode.Utils.getRandomBytes(Netcode.USER_DATA_BYTES); 408 | var connectToken = new Netcode.ConnectToken(); 409 | assert.equal( 410 | connectToken.generate( 411 | TEST_CLIENT_ID, 412 | [addr], 413 | TEST_PROTOCOL_ID, 414 | TEST_CONNECT_TOKEN_EXPIRY, 415 | TEST_TIMEOUT_SECONDS, 416 | TEST_SEQUENCE_START, 417 | userData, 418 | key 419 | ), 420 | true 421 | ); 422 | var tokenBuffer = connectToken.write(); 423 | assert.equal(tokenBuffer !== undefined, true, 'oh no'); 424 | 425 | var outToken = new Netcode.ConnectToken(); 426 | var readRet = outToken.read(tokenBuffer); 427 | assert.equal(readRet, Netcode.Errors.none, 'oh no'); 428 | 429 | var tokenData = connectToken.privateData.decrypt( 430 | TEST_PROTOCOL_ID, 431 | connectToken.expireTimestamp, 432 | connectToken.sequence, 433 | key 434 | ); 435 | assert(tokenData !== undefined, true); 436 | var decryptedToken = new Uint8Array(tokenData.length); 437 | decryptedToken.set(tokenData); 438 | 439 | connectToken.privateData.tokenData.clearPosition(); 440 | var mac = new Uint8Array(Netcode.MAC_BYTES); 441 | const arr = new Uint8Array([ 442 | ...connectToken.privateData.tokenData.bytes, 443 | ...mac, 444 | ]); 445 | connectToken.privateData.tokenData = new Netcode.ByteBuffer(arr); 446 | assert.equal( 447 | connectToken.privateData.encrypt( 448 | TEST_PROTOCOL_ID, 449 | connectToken.expireTimestamp, 450 | TEST_SEQUENCE_START, 451 | key 452 | ), 453 | true 454 | ); 455 | 456 | var p = new Netcode.RequestPacket(); 457 | p.setProperties( 458 | Netcode.VERSION_INFO_BYTES_ARRAY, 459 | TEST_PROTOCOL_ID, 460 | connectToken.expireTimestamp, 461 | TEST_SEQUENCE_START, 462 | connectToken.privateData.buffer 463 | ); 464 | p._token = connectToken.privateData; 465 | return [p, decryptedToken]; 466 | } 467 | -------------------------------------------------------------------------------- /tests/replayprotection_test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var { Netcode } = require('../dist/node/netcode'); 3 | 4 | describe('ReplayProtection tests', function () { 5 | it('test reset', function () { 6 | const rp = new Netcode.ReplayProtection(); 7 | for (const p of rp._receivedPacket) { 8 | assert.equal(p, 0xffffffffffffffff, 'oh no'); 9 | } 10 | }); 11 | 12 | it('test replay protection', function () { 13 | const rp = new Netcode.ReplayProtection(); 14 | for (let i = 0; i < 2; i += 1) { 15 | rp.reset(); 16 | if (rp._mostRecentSequence !== 0) { 17 | assert.fail('sequence not 0'); 18 | } 19 | } 20 | 21 | var sequence = 1 << 63; 22 | if (rp.checkAlreadyReceived(sequence)) { 23 | assert.fail('sequence numbers with high bit set should be ignored'); 24 | } 25 | 26 | if (rp._mostRecentSequence !== 0) { 27 | assert.fail( 28 | 'sequence was not 0 after high-bit check got', 29 | rp._mostRecentSequence 30 | ); 31 | } 32 | 33 | // the first time we receive packets, they should not be already received 34 | var maxSequence = Netcode.REPLAY_PROTECTION_BUFFER_SIZE * 4; 35 | for (let sequence = 0; sequence < maxSequence; sequence += 1) { 36 | if (rp.checkAlreadyReceived(sequence)) { 37 | assert.fail( 38 | 'the first time we receive packets, they should not be already received' 39 | ); 40 | } 41 | } 42 | 43 | // old packets outside buffer should be considered already received 44 | if (!rp.checkAlreadyReceived(0)) { 45 | assert.fail( 46 | 'old packets outside buffer should be considered already received' 47 | ); 48 | } 49 | 50 | // packets received a second time should be flagged already received 51 | for ( 52 | let sequence = maxSequence - 10; 53 | sequence < maxSequence; 54 | sequence += 1 55 | ) { 56 | if (!rp.checkAlreadyReceived(sequence)) { 57 | assert.fail( 58 | 'packets received a second time should be flagged already received' 59 | ); 60 | } 61 | } 62 | 63 | // jumping ahead to a much higher sequence should be considered not already received 64 | if ( 65 | rp.checkAlreadyReceived( 66 | maxSequence + Netcode.REPLAY_PROTECTION_BUFFER_SIZE 67 | ) 68 | ) { 69 | assert.fail( 70 | 'jumping ahead to a much higher sequence should be considered not already received' 71 | ); 72 | } 73 | 74 | // old packets should be considered already received 75 | for (let sequence = 0; sequence < maxSequence; sequence += 1) { 76 | if (!rp.checkAlreadyReceived(sequence)) { 77 | assert.fail('old packets should be considered already received'); 78 | } 79 | } 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /tests/token_test.js: -------------------------------------------------------------------------------- 1 | var { Netcode } = require('../dist/node/Netcode'); 2 | var assert = require('assert'); 3 | 4 | var TEST_PROTOCOL_ID = Netcode.Long.fromNumber(0x1122334455667788); 5 | var TEST_CONNECT_TOKEN_EXPIRY = 30; 6 | var TEST_SERVER_PORT = 40000; 7 | var TEST_CLIENT_ID = Netcode.Long.fromNumber(0x1); 8 | var TEST_SEQUENCE_START = Netcode.Long.fromNumber(1000); 9 | var TEST_TIMEOUT_SECONDS = 15; 10 | var TEST_PRIVATE_KEY = new Uint8Array([ 11 | 0x60, 12 | 0x6a, 13 | 0xbe, 14 | 0x6e, 15 | 0xc9, 16 | 0x19, 17 | 0x10, 18 | 0xea, 19 | 0x9a, 20 | 0x65, 21 | 0x62, 22 | 0xf6, 23 | 0x6f, 24 | 0x2b, 25 | 0x30, 26 | 0xe4, 27 | 0x43, 28 | 0x71, 29 | 0xd6, 30 | 0x2c, 31 | 0xd1, 32 | 0x99, 33 | 0x27, 34 | 0x26, 35 | 0x6b, 36 | 0x3c, 37 | 0x60, 38 | 0xf4, 39 | 0xb7, 40 | 0x15, 41 | 0xab, 42 | 0xa1, 43 | ]); 44 | 45 | function assertBytesEqual(a1, a2, str) { 46 | assert.equal(Netcode.Utils.arrayEqual(a1, a2), true, str); 47 | } 48 | 49 | function ipStringToBytes(ip) { 50 | var addr = Netcode.Utils.stringToIPV4Address(ip); 51 | return addr.ip; 52 | } 53 | 54 | describe('ConnectToken tests', function () { 55 | it('read/write shared connect token', function () { 56 | var clientKey = Netcode.Utils.generateKey(); 57 | assert.equal(clientKey.length, Netcode.KEY_BYTES, 'oh no'); 58 | var serverKey = Netcode.Utils.generateKey(); 59 | assert.equal(serverKey.length, Netcode.KEY_BYTES, 'oh no'); 60 | 61 | var addr = { 62 | ip: ipStringToBytes('10.20.30.40'), 63 | port: TEST_SERVER_PORT, 64 | }; 65 | var data = new Netcode.SharedTokenData(); 66 | data.timeoutSeconds = 10; 67 | data.serverAddrs = [addr]; 68 | data.clientKey = clientKey; 69 | data.serverKey = serverKey; 70 | 71 | var buffer = new Netcode.ByteBuffer( 72 | new Uint8Array(Netcode.CONNECT_TOKEN_BYTES) 73 | ); 74 | data.write(buffer); 75 | buffer.clearPosition(); 76 | 77 | var outData = new Netcode.SharedTokenData(); 78 | var ret = outData.read(buffer); 79 | assert.equal(ret, Netcode.Errors.none, 'oh no'); 80 | assertBytesEqual(clientKey, outData.clientKey, 'oh no'); 81 | assertBytesEqual(serverKey, outData.serverKey, 'oh no'); 82 | assert.equal(data.timeoutSeconds, outData.timeoutSeconds, 'oh no'); 83 | assert.equal(outData.serverAddrs.length, 1, 'oh no'); 84 | assertBytesEqual(outData.serverAddrs[0].ip, addr.ip, 'oh no'); 85 | assert.equal(outData.serverAddrs[0].port, addr.port, 'oh no'); 86 | }); 87 | 88 | it('read/write private connect token', function () { 89 | var addr = { 90 | ip: ipStringToBytes('10.20.30.40'), 91 | port: TEST_SERVER_PORT, 92 | }; 93 | var ts = Date.now(); 94 | var expireTs = ts + TEST_CONNECT_TOKEN_EXPIRY; 95 | var timeoutSeconds = 10; 96 | var userData = Netcode.Utils.getRandomBytes(Netcode.USER_DATA_BYTES); 97 | 98 | var token1 = Netcode.ConnectTokenPrivate.create( 99 | TEST_CLIENT_ID, 100 | timeoutSeconds, 101 | [addr], 102 | userData 103 | ); 104 | token1.generate(); 105 | token1.write(); 106 | var ret = token1.encrypt( 107 | TEST_PROTOCOL_ID, 108 | expireTs, 109 | TEST_SEQUENCE_START, 110 | TEST_PRIVATE_KEY 111 | ); 112 | assert.equal(ret, true, 'oh no'); 113 | 114 | var encryptedToken = new Uint8Array(token1.buffer); 115 | var token2 = Netcode.ConnectTokenPrivate.createEncrypted(encryptedToken); 116 | const decrypted = token2.decrypt( 117 | TEST_PROTOCOL_ID, 118 | expireTs, 119 | TEST_SEQUENCE_START, 120 | TEST_PRIVATE_KEY 121 | ); 122 | assert.equal(decrypted !== undefined, true, 'decrypt failed'); 123 | ret = token2.read(); 124 | assert.equal(ret, Netcode.Errors.none, 'oh no'); 125 | 126 | // compare tokens 127 | assert.equal(token1.clientId.equals(token2.clientId), true, 'oh no'); 128 | assert.equal( 129 | token1.sharedTokenData.serverAddrs.length, 130 | token2.sharedTokenData.serverAddrs.length, 131 | 'oh no' 132 | ); 133 | assertBytesEqual( 134 | token1.sharedTokenData.serverAddrs[0].ip, 135 | token2.sharedTokenData.serverAddrs[0].ip, 136 | 'oh no' 137 | ); 138 | assert.equal( 139 | token1.sharedTokenData.serverAddrs[0].port, 140 | token2.sharedTokenData.serverAddrs[0].port, 141 | 'oh no' 142 | ); 143 | assertBytesEqual( 144 | token1.sharedTokenData.clientKey, 145 | token2.sharedTokenData.clientKey, 146 | 'oh no' 147 | ); 148 | assertBytesEqual( 149 | token1.sharedTokenData.serverKey, 150 | token2.sharedTokenData.serverKey, 151 | 'oh no' 152 | ); 153 | 154 | const buffer = new Uint8Array(Netcode.CONNECT_TOKEN_PRIVATE_BYTES); 155 | Netcode.Utils.blockCopy(token2.buffer, 0, buffer, 0, token2.buffer.length); 156 | token2.tokenData = new Netcode.ByteBuffer(buffer); 157 | token2.write(); 158 | var ret = token2.encrypt( 159 | TEST_PROTOCOL_ID, 160 | expireTs, 161 | TEST_SEQUENCE_START, 162 | TEST_PRIVATE_KEY 163 | ); 164 | assert.equal(ret, true, 'oh no'); 165 | assert.equal(token1.buffer.length, token2.buffer.length, 'oh no'); 166 | assertBytesEqual(token1.buffer, token2.buffer, 'oh no'); 167 | }); 168 | 169 | it('read/write connect token', function () { 170 | var addr = { 171 | ip: ipStringToBytes('10.20.30.40'), 172 | port: TEST_SERVER_PORT, 173 | }; 174 | var key = Netcode.Utils.generateKey(); 175 | var userData = Netcode.Utils.getRandomBytes(Netcode.USER_DATA_BYTES); 176 | var inToken = new Netcode.ConnectToken(); 177 | var ret = inToken.generate( 178 | TEST_CLIENT_ID, 179 | [addr], 180 | TEST_PROTOCOL_ID, 181 | TEST_CONNECT_TOKEN_EXPIRY, 182 | TEST_TIMEOUT_SECONDS, 183 | TEST_SEQUENCE_START, 184 | userData, 185 | key 186 | ); 187 | assertBytesEqual( 188 | inToken.versionInfo, 189 | Netcode.VERSION_INFO_BYTES_ARRAY, 190 | 'oh no' 191 | ); 192 | assert.equal(ret, true, 'oh no'); 193 | var tokenBuffer = inToken.write(); 194 | assert.equal(tokenBuffer !== undefined, true, 'oh no'); 195 | 196 | var outToken = new Netcode.ConnectToken(); 197 | var readRet = outToken.read(tokenBuffer); 198 | assert.equal(readRet, Netcode.Errors.none, 'oh no'); 199 | assertBytesEqual( 200 | outToken.versionInfo, 201 | Netcode.VERSION_INFO_BYTES_ARRAY, 202 | 'oh no' 203 | ); 204 | assert.equal(outToken.protocolID.equals(TEST_PROTOCOL_ID), true, 'oh no'); 205 | assert.equal( 206 | outToken.createTimestamp.equals(inToken.createTimestamp), 207 | true, 208 | 'oh no' 209 | ); 210 | assert.equal( 211 | outToken.expireTimestamp.equals(inToken.expireTimestamp), 212 | true, 213 | 'oh no' 214 | ); 215 | assert.equal(outToken.sequence.equals(inToken.sequence), true, 'oh no'); 216 | 217 | assert.equal(outToken.sharedTokenData.serverAddrs.length, 1, 'oh no'); 218 | assert.equal( 219 | outToken.sharedTokenData.serverAddrs[0].port, 220 | addr.port, 221 | 'oh no' 222 | ); 223 | assertBytesEqual( 224 | outToken.sharedTokenData.serverAddrs[0].ip, 225 | addr.ip, 226 | 'oh no' 227 | ); 228 | assertBytesEqual( 229 | outToken.sharedTokenData.clientKey, 230 | inToken.sharedTokenData.clientKey, 231 | 'oh no' 232 | ); 233 | assertBytesEqual( 234 | outToken.sharedTokenData.serverKey, 235 | inToken.sharedTokenData.serverKey, 236 | 'oh no' 237 | ); 238 | assertBytesEqual( 239 | outToken.privateData.buffer, 240 | inToken.privateData.buffer, 241 | 'oh no' 242 | ); 243 | 244 | assert.equal( 245 | outToken.privateData.decrypt( 246 | TEST_PROTOCOL_ID, 247 | outToken.expireTimestamp, 248 | outToken.sequence, 249 | key 250 | ) !== undefined, 251 | true, 252 | 'oh no' 253 | ); 254 | assert.equal( 255 | inToken.privateData.decrypt( 256 | TEST_PROTOCOL_ID, 257 | inToken.expireTimestamp, 258 | inToken.sequence, 259 | key 260 | ) !== undefined, 261 | true, 262 | 'oh no' 263 | ); 264 | assert.equal(outToken.privateData.read(), Netcode.Errors.none, 'oh no'); 265 | 266 | assert.equal(outToken.sharedTokenData.serverAddrs.length, 1, 'oh no'); 267 | assert.equal( 268 | outToken.sharedTokenData.serverAddrs[0].port, 269 | addr.port, 270 | 'oh no' 271 | ); 272 | assertBytesEqual( 273 | outToken.sharedTokenData.serverAddrs[0].ip, 274 | addr.ip, 275 | 'oh no' 276 | ); 277 | assertBytesEqual( 278 | outToken.sharedTokenData.clientKey, 279 | inToken.sharedTokenData.clientKey, 280 | 'oh no' 281 | ); 282 | assertBytesEqual( 283 | outToken.sharedTokenData.serverKey, 284 | inToken.sharedTokenData.serverKey, 285 | 'oh no' 286 | ); 287 | }); 288 | }); 289 | 290 | describe('ChallengeToken tests', function () { 291 | it('read/write challenge token', function () { 292 | var token = new Netcode.ChallengeToken(TEST_CLIENT_ID); 293 | var userData = Netcode.Utils.getRandomBytes(Netcode.USER_DATA_BYTES); 294 | var tokenBuffer = token.write(userData); 295 | 296 | var sequence = Netcode.Long.fromNumber(9999); 297 | var key = Netcode.Utils.generateKey(); 298 | 299 | assert.equal(tokenBuffer.length, Netcode.CHALLENGE_TOKEN_BYTES, 'oh no'); 300 | Netcode.ChallengeToken.encrypt(tokenBuffer, sequence, key); 301 | assert.equal(tokenBuffer.length, Netcode.CHALLENGE_TOKEN_BYTES, 'oh no'); 302 | 303 | var decrypted = Netcode.ChallengeToken.decrypt(tokenBuffer, sequence, key); 304 | assert.equal(decrypted instanceof Uint8Array, true, 'oh no'); 305 | assert.equal( 306 | decrypted.length, 307 | Netcode.CHALLENGE_TOKEN_BYTES - Netcode.MAC_BYTES, 308 | 'oh no' 309 | ); 310 | 311 | var token2 = new Netcode.ChallengeToken(); 312 | var err = token2.read(decrypted); 313 | assert.equal(err, Netcode.Errors.none, 'oh no'); 314 | assert.equal(token2.clientID.equals(token.clientID), true, 'oh no'); 315 | assertBytesEqual(token2.userData, token.userData, 'oh no'); 316 | }); 317 | }); 318 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "amd", 4 | "target": "es6", 5 | "composite": true, 6 | "experimentalDecorators": true, 7 | "sourceMap": true, 8 | "alwaysStrict": true, 9 | "rootDir": "./src", 10 | "outFile": "./dist/browser/netcode.js", 11 | "removeComments": true, 12 | "skipLibCheck": true 13 | }, 14 | "include": ["./src/**/*.ts", "src/chacha20poly1305.d.ts"], 15 | "exclude": ["./bin/"] 16 | } 17 | --------------------------------------------------------------------------------