From 26ca4c5de18bd864f9ac5ee79eb4e6995b7c349d Mon Sep 17 00:00:00 2001 From: snowinszu <86755838@qq.com> Date: Fri, 6 Oct 2023 01:03:54 +0800 Subject: [PATCH] add dist --- .gitignore | 1 - dist/fast-tracker.d.ts | 37 ++++ dist/fast-tracker.js | 363 ++++++++++++++++++++++++++++++++++++++ dist/index.d.ts | 19 ++ dist/index.js | 24 +++ dist/run-uws-tracker.d.ts | 45 +++++ dist/run-uws-tracker.js | 181 +++++++++++++++++++ dist/tracker.d.ts | 29 +++ dist/tracker.js | 21 +++ dist/uws-tracker.d.ts | 47 +++++ dist/uws-tracker.js | 204 +++++++++++++++++++++ 11 files changed, 970 insertions(+), 1 deletion(-) create mode 100644 dist/fast-tracker.d.ts create mode 100644 dist/fast-tracker.js create mode 100644 dist/index.d.ts create mode 100644 dist/index.js create mode 100644 dist/run-uws-tracker.d.ts create mode 100644 dist/run-uws-tracker.js create mode 100644 dist/tracker.d.ts create mode 100644 dist/tracker.js create mode 100644 dist/uws-tracker.d.ts create mode 100644 dist/uws-tracker.js diff --git a/.gitignore b/.gitignore index 46e0c7d..0cc3a5e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /node_modules -/dist /test_dist /coverage /.nyc_output diff --git a/dist/fast-tracker.d.ts b/dist/fast-tracker.d.ts new file mode 100644 index 0000000..6eafbd2 --- /dev/null +++ b/dist/fast-tracker.d.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2019 Novage LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Tracker, PeerContext } from "./tracker"; +interface Settings { + maxOffers: number; + announceInterval: number; +} +export declare class FastTracker implements Tracker { + #private; + readonly settings: Settings; + constructor(settings?: Partial); + get swarms(): ReadonlyMap; + processMessage(jsonObject: object, peer: PeerContext): void; + disconnectPeer(peer: PeerContext): void; + private processAnnounce; + private addPeerToSwarm; + private sendOffersToPeers; + private processAnswer; + private processStop; + private processScrape; +} +export {}; diff --git a/dist/fast-tracker.js b/dist/fast-tracker.js new file mode 100644 index 0000000..f43a721 --- /dev/null +++ b/dist/fast-tracker.js @@ -0,0 +1,363 @@ +"use strict"; +/** + * Copyright 2019 Novage LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) { + if (!privateMap.has(receiver)) { + throw new TypeError("attempted to get private field on non-instance"); + } + return privateMap.get(receiver); +}; +var _swarms, _peers, _peers_1; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FastTracker = void 0; +/* eslint-disable camelcase */ +const Debug = require("debug"); +const tracker_1 = require("./tracker"); +// eslint-disable-next-line new-cap +const debug = Debug("wt-tracker:fast-tracker"); +const debugEnabled = debug.enabled; +class FastTracker { + constructor(settings) { + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + _swarms.set(this, new Map()); + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + _peers.set(this, new Map()); + this.settings = Object.assign({ maxOffers: 20, announceInterval: 120 }, settings); + } + get swarms() { + return __classPrivateFieldGet(this, _swarms); + } + processMessage(jsonObject, peer) { + const json = jsonObject; + const action = json.action; + if (action === "announce") { + const event = json.event; + if (event === undefined) { + if (json.answer === undefined) { + this.processAnnounce(json, peer); + } + else { + this.processAnswer(json, peer); + } + } + else if (event === "started") { + this.processAnnounce(json, peer); + } + else if (event === "stopped") { + this.processStop(json, peer); + } + else if (event === "completed") { + this.processAnnounce(json, peer, true); + } + else { + throw new tracker_1.TrackerError("unknown announce event"); + } + } + else if (action === "scrape") { + this.processScrape(json, peer); + } + else { + throw new tracker_1.TrackerError("unknown action"); + } + } + disconnectPeer(peer) { + const peerId = peer.id; + if (peerId === undefined) { + return; + } + if (debugEnabled) { + debug("disconnect peer:", Buffer.from(peerId).toString("hex")); + } + // eslint-disable-next-line guard-for-in + for (const infoHash in peer) { + const swarm = peer[infoHash]; + if (!(swarm instanceof Swarm)) { + continue; + } + swarm.removePeer(peer); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete peer[infoHash]; + if (debugEnabled) { + debug("disconnect peer: peer", Buffer.from(peerId).toString("hex"), "removed from swarm", Buffer.from(infoHash).toString("hex")); + } + if (swarm.peers.length === 0) { + if (debugEnabled) { + debug("disconnect peer: swarm removed (empty)", Buffer.from(swarm.infoHash).toString("hex")); + } + __classPrivateFieldGet(this, _swarms).delete(swarm.infoHash); + } + } + __classPrivateFieldGet(this, _peers).delete(peerId); + peer.id = undefined; + } + processAnnounce(json, peer, completed = false) { + const infoHash = json.info_hash; + const peerId = json.peer_id; + let swarm = undefined; + if (peer.id === undefined) { + if (typeof peerId !== "string") { + throw new tracker_1.TrackerError("announce: peer_id field is missing or wrong"); + } + peer.id = peerId; + const oldPeer = __classPrivateFieldGet(this, _peers).get(peerId); + if (oldPeer !== undefined) { + this.disconnectPeer(oldPeer); + } + __classPrivateFieldGet(this, _peers).set(peerId, peer); + } + else if (peer.id === peerId) { + swarm = peer[infoHash]; + } + else { + throw new tracker_1.TrackerError("announce: different peer_id on the same connection"); + } + const isPeerCompleted = (completed || json.left === 0); + if (swarm === undefined) { + swarm = this.addPeerToSwarm(peer, infoHash, isPeerCompleted); + } + else if (swarm instanceof Swarm) { + if (debugEnabled) { + debug("announce: peer", Buffer.from(peer.id).toString("hex"), "in swarm", Buffer.from(infoHash).toString("hex")); + } + if (isPeerCompleted) { + swarm.setCompleted(peer); + } + } + else { + throw new tracker_1.TrackerError("announce: illegal info_hash field"); + } + peer.sendMessage({ + action: "announce", + interval: this.settings.announceInterval, + info_hash: infoHash, + complete: swarm.completedCount, + incomplete: swarm.peers.length - swarm.completedCount, + }, peer); + this.sendOffersToPeers(json, swarm.peers, peer, infoHash); + } + addPeerToSwarm(peer, infoHash, completed) { + let swarm = __classPrivateFieldGet(this, _swarms).get(infoHash); + if (swarm === undefined) { + if (typeof infoHash !== "string") { + throw new tracker_1.TrackerError("announce: info_hash field is missing or wrong"); + } + if (debugEnabled) { + debug("announce: swarm created:", Buffer.from(infoHash).toString("hex")); + } + swarm = new Swarm(infoHash); + __classPrivateFieldGet(this, _swarms).set(infoHash, swarm); + } + if (debugEnabled) { + debug("announce: peer", Buffer.from(peer.id).toString("hex"), "added to swarm", Buffer.from(infoHash).toString("hex")); + } + swarm.addPeer(peer, completed); + peer[infoHash] = swarm; + return swarm; + } + sendOffersToPeers(json, peers, peer, infoHash) { + if (peers.length <= 1) { + return; + } + const offers = json.offers; + if (offers === undefined) { + return; + } + else if (!(offers instanceof Array)) { + throw new tracker_1.TrackerError("announce: offers field is not an array"); + } + const numwant = json.numwant; + if (!Number.isInteger(numwant)) { + return; + } + const countPeersToSend = peers.length - 1; + const countOffersToSend = Math.min(countPeersToSend, offers.length, this.settings.maxOffers, numwant); + if (countOffersToSend === countPeersToSend) { + // we have offers for all the peers from the swarm - send offers to all + const offersIterator = offers.values(); + for (const toPeer of peers) { + if (toPeer !== peer) { + sendOffer(offersIterator.next().value, peer.id, toPeer, infoHash); + } + } + } + else { + // send offers to random peers + let peerIndex = Math.floor(Math.random() * peers.length); + for (let i = 0; i < countOffersToSend; i++) { + const toPeer = peers[peerIndex]; + if (toPeer === peer) { + i--; // do one more iteration + } + else { + sendOffer(offers[i], peer.id, toPeer, infoHash); + } + peerIndex++; + if (peerIndex === peers.length) { + peerIndex = 0; + } + } + } + debug("announce: sent offers", (countOffersToSend < 0) ? 0 : countOffersToSend); + } + processAnswer(json, peer) { + const toPeerId = json.to_peer_id; + const toPeer = __classPrivateFieldGet(this, _peers).get(toPeerId); + if (toPeer === undefined) { + throw new tracker_1.TrackerError("answer: to_peer_id is not in the swarm"); + } + json.peer_id = peer.id; + delete json.to_peer_id; + toPeer.sendMessage(json, toPeer); + if (debugEnabled) { + debug("answer: from peer", Buffer.from(peer.id).toString("hex"), "to peer", Buffer.from(toPeerId).toString("hex")); + } + } + processStop(json, peer) { + const infoHash = json.info_hash; + const swarm = peer[infoHash]; + if (!(swarm instanceof Swarm)) { + debug("stop event: peer not in the swarm"); + return; + } + if (debugEnabled) { + debug("stop event: peer", Buffer.from(peer.id).toString("hex"), "removed from swarm", Buffer.from(infoHash).toString("hex")); + } + swarm.removePeer(peer); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete peer[infoHash]; + if (swarm.peers.length === 0) { + if (debugEnabled) { + debug("stop event: swarm removed (empty)", Buffer.from(infoHash).toString("hex")); + } + __classPrivateFieldGet(this, _swarms).delete(infoHash); + } + } + processScrape(json, peer) { + const infoHash = json.info_hash; + const files = {}; + if (infoHash === undefined) { + for (const swarm of __classPrivateFieldGet(this, _swarms).values()) { + files[swarm.infoHash] = { + complete: swarm.completedCount, + incomplete: swarm.peers.length - swarm.completedCount, + downloaded: swarm.completedCount, + }; + } + } + else if (infoHash instanceof Array) { + for (const singleInfoHash of infoHash) { + const swarm = __classPrivateFieldGet(this, _swarms).get(singleInfoHash); + if (swarm !== undefined) { + files[singleInfoHash] = { + complete: swarm.completedCount, + incomplete: swarm.peers.length - swarm.completedCount, + downloaded: swarm.completedCount, + }; + } + else if (typeof singleInfoHash === "string") { + files[singleInfoHash] = { + complete: 0, + incomplete: 0, + downloaded: 0, + }; + } + } + } + else { + const swarm = __classPrivateFieldGet(this, _swarms).get(infoHash); + if (swarm !== undefined) { + files[infoHash] = { + complete: swarm.completedCount, + incomplete: swarm.peers.length - swarm.completedCount, + downloaded: swarm.completedCount, + }; + } + else if (typeof infoHash === "string") { + files[infoHash] = { + complete: 0, + incomplete: 0, + downloaded: 0, + }; + } + } + peer.sendMessage({ action: "scrape", files: files }, peer); + } +} +exports.FastTracker = FastTracker; +_swarms = new WeakMap(), _peers = new WeakMap(); +class Swarm { + constructor(infoHash) { + this.infoHash = infoHash; + this.completedCount = 0; + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + _peers_1.set(this, []); + } + addPeer(peer, completed) { + __classPrivateFieldGet(this, _peers_1).push(peer); + if (completed) { + if (this.completedPeers === undefined) { + this.completedPeers = new Set(); + } + this.completedPeers.add(peer.id); + this.completedCount++; + } + } + removePeer(peer) { + var _a; + const index = __classPrivateFieldGet(this, _peers_1).indexOf(peer); + if (((_a = this.completedPeers) === null || _a === void 0 ? void 0 : _a.delete(peer.id)) === true) { + this.completedCount--; + } + // Delete peerId from array without calling splice + const last = __classPrivateFieldGet(this, _peers_1).pop(); + if (index < __classPrivateFieldGet(this, _peers_1).length) { + __classPrivateFieldGet(this, _peers_1)[index] = last; + } + } + setCompleted(peer) { + if (this.completedPeers === undefined) { + this.completedPeers = new Set(); + } + if (!this.completedPeers.has(peer.id)) { + this.completedPeers.add(peer.id); + this.completedCount++; + } + } + get peers() { + return __classPrivateFieldGet(this, _peers_1); + } +} +_peers_1 = new WeakMap(); +function sendOffer(offerItem, fromPeerId, toPeer, infoHash) { + if (!(offerItem instanceof Object)) { + throw new tracker_1.TrackerError("announce: wrong offer item format"); + } + const offer = offerItem.offer; + const offerId = offerItem.offer_id; + if (!(offer instanceof Object)) { + throw new tracker_1.TrackerError("announce: wrong offer item field format"); + } + toPeer.sendMessage({ + action: "announce", + info_hash: infoHash, + offer_id: offerId, + peer_id: fromPeerId, + offer: { + type: "offer", + sdp: offer.sdp, + }, + }, toPeer); +} diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..76a3a4a --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,19 @@ +/** + * @license Apache-2.0 + * Copyright 2019 Novage LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { UWebSocketsTracker } from "./uws-tracker"; +export { FastTracker } from "./fast-tracker"; +export { Tracker, PeerContext, TrackerError } from "./tracker"; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..a9ec29b --- /dev/null +++ b/dist/index.js @@ -0,0 +1,24 @@ +"use strict"; +/** + * @license Apache-2.0 + * Copyright 2019 Novage LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +var uws_tracker_1 = require("./uws-tracker"); +Object.defineProperty(exports, "UWebSocketsTracker", { enumerable: true, get: function () { return uws_tracker_1.UWebSocketsTracker; } }); +var fast_tracker_1 = require("./fast-tracker"); +Object.defineProperty(exports, "FastTracker", { enumerable: true, get: function () { return fast_tracker_1.FastTracker; } }); +var tracker_1 = require("./tracker"); +Object.defineProperty(exports, "TrackerError", { enumerable: true, get: function () { return tracker_1.TrackerError; } }); diff --git a/dist/run-uws-tracker.d.ts b/dist/run-uws-tracker.d.ts new file mode 100644 index 0000000..df78443 --- /dev/null +++ b/dist/run-uws-tracker.d.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2019 Novage LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export interface Settings { + servers: ServerItemSettings[]; + tracker?: object; + websocketsAccess?: Partial; +} +export interface ServerItemSettings { + server?: Partial; + websockets?: Partial; +} +export interface ServerSettings { + port: number; + host: string; + key_file_name?: string; + cert_file_name?: string; + passphrase?: string; + dh_params_file_name?: string; + ssl_prefer_low_memory_usage?: boolean; +} +export interface WebSocketsSettings { + path: string; + maxPayloadLength: number; + idleTimeout: number; + compression: number; + maxConnections: number; +} +export interface WebSocketsAccessSettings { + allowOrigins?: readonly string[]; + denyOrigins?: readonly string[]; + denyEmptyOrigin: boolean; +} diff --git a/dist/run-uws-tracker.js b/dist/run-uws-tracker.js new file mode 100644 index 0000000..1863f98 --- /dev/null +++ b/dist/run-uws-tracker.js @@ -0,0 +1,181 @@ +"use strict"; +/* eslint-disable no-console */ +/** + * Copyright 2019 Novage LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const fs_1 = require("fs"); +const Debug = require("debug"); +const uws_tracker_1 = require("./uws-tracker"); +const fast_tracker_1 = require("./fast-tracker"); +// eslint-disable-next-line new-cap +const debugRequests = Debug("wt-tracker:uws-tracker-requests"); +const debugRequestsEnabled = debugRequests.enabled; +async function main() { + let settingsFileData = undefined; + if (process.argv.length <= 2) { + try { + settingsFileData = fs_1.readFileSync("config.json"); + } + catch (e) { + if (e.code !== "ENOENT") { + console.error("failed to read configuration file:", e); + return; + } + } + } + else { + try { + settingsFileData = fs_1.readFileSync(process.argv[2]); + } + catch (e) { + console.error("failed to read configuration file:", e); + return; + } + } + let jsonSettings = undefined; + try { + jsonSettings = (settingsFileData === undefined) + ? {} + : JSON.parse(settingsFileData.toString()); + } + catch (e) { + console.error("failed to parse JSON configuration file:", e); + return; + } + const settings = validateSettings(jsonSettings); + if (settings === undefined) { + return; + } + const tracker = new fast_tracker_1.FastTracker(settings.tracker); + try { + await runServers(tracker, settings); + } + catch (e) { + console.error("failed to start the web server:", e); + } +} +function validateSettings(jsonSettings) { + if ((jsonSettings.servers !== undefined) && !(jsonSettings.servers instanceof Array)) { + console.error("failed to parse JSON configuration file: 'servers' property should be an array"); + return undefined; + } + const servers = []; + if (jsonSettings.servers === undefined) { + servers.push({}); + } + else { + for (const serverSettings of jsonSettings.servers) { + if (serverSettings instanceof Object) { + servers.push(serverSettings); + } + else { + console.error("failed to parse JSON configuration file: 'servers' property should be an array of objects"); + return undefined; + } + } + } + if ((jsonSettings.tracker !== undefined) && !(jsonSettings.tracker instanceof Object)) { + console.error("failed to parse JSON configuration file: 'tracker' property should be an object"); + return undefined; + } + if ((jsonSettings.websocketsAccess !== undefined) && !(jsonSettings.websocketsAccess instanceof Object)) { + console.error("failed to parse JSON configuration file: 'websocketsAccess' property should be an object"); + return undefined; + } + return { + servers: servers, + tracker: jsonSettings.tracker, + websocketsAccess: jsonSettings.websocketsAccess, + }; +} +async function runServers(tracker, settings) { + let indexHtml = undefined; + try { + indexHtml = fs_1.readFileSync("index.html"); + } + catch (e) { + if (e.code !== "ENOENT") { + throw e; + } + } + const servers = []; + const serverPromises = settings.servers.map(async (serverSettings) => { + const server = buildServer(tracker, serverSettings, settings.websocketsAccess, indexHtml, servers); + servers.push(server); + await server.run(); + console.info(`listening ${server.settings.server.host}:${server.settings.server.port}`); + }); + await Promise.all(serverPromises); +} +function buildServer(tracker, serverSettings, websocketsAccess, indexHtml, servers) { + if (!(serverSettings instanceof Object)) { + throw Error("failed to parse JSON configuration file: 'servers' property should be an array of objects"); + } + const server = new uws_tracker_1.UWebSocketsTracker(tracker, Object.assign(Object.assign({}, serverSettings), { access: websocketsAccess })); + server.app.get("/", (response, request) => { + debugRequest(server, request); + if (indexHtml === undefined) { + const status = "404 Not Found"; + response.writeStatus(status).end(status); + } + else { + response.end(indexHtml); + } + }).get("/stats.json", (response, request) => { + debugRequest(server, request); + const swarms = tracker.swarms; + let peersCount = 0; + for (const swarm of swarms.values()) { + peersCount += swarm.peers.length; + } + const serversStats = new Array(); + for (const serverForStats of servers) { + const settings = serverForStats.settings; + serversStats.push({ + server: `${settings.server.host}:${settings.server.port}`, + webSocketsCount: serverForStats.stats.webSocketsCount, + }); + } + response. + writeHeader("Content-Type", "application/json"). + end(JSON.stringify({ + torrentsCount: swarms.size, + peersCount: peersCount, + servers: serversStats, + memory: process.memoryUsage(), + })); + }).any("/*", (response, request) => { + debugRequest(server, request); + const status = "404 Not Found"; + response.writeStatus(status).end(status); + }); + return server; +} +function debugRequest(server, request) { + if (debugRequestsEnabled) { + debugRequests(server.settings.server.host, server.settings.server.port, "request method:", request.getMethod(), "url:", request.getUrl(), "query:", request.getQuery()); + } +} +async function run() { + try { + await main(); + } + catch (e) { + console.error(e); + } +} +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dist/tracker.d.ts b/dist/tracker.d.ts new file mode 100644 index 0000000..7ed59b3 --- /dev/null +++ b/dist/tracker.d.ts @@ -0,0 +1,29 @@ +/** + * Copyright 2019 Novage LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export interface PeerContext { + id?: string; + sendMessage: (json: object, peer: PeerContext) => void; +} +export interface Tracker { + readonly swarms: ReadonlyMap; + readonly settings: object; + processMessage: (json: object, peer: PeerContext) => void; + disconnectPeer: (peer: PeerContext) => void; +} +export declare class TrackerError extends Error { +} diff --git a/dist/tracker.js b/dist/tracker.js new file mode 100644 index 0000000..ac47219 --- /dev/null +++ b/dist/tracker.js @@ -0,0 +1,21 @@ +"use strict"; +/** + * Copyright 2019 Novage LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.TrackerError = void 0; +class TrackerError extends Error { +} +exports.TrackerError = TrackerError; diff --git a/dist/uws-tracker.d.ts b/dist/uws-tracker.d.ts new file mode 100644 index 0000000..817bb2b --- /dev/null +++ b/dist/uws-tracker.d.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2019 Novage LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TemplatedApp } from "uWebSockets.js"; +import { Tracker } from "./tracker"; +import { ServerSettings, WebSocketsSettings, WebSocketsAccessSettings } from "./run-uws-tracker"; +export interface UwsTrackerSettings { + server: ServerSettings; + websockets: WebSocketsSettings; + access: WebSocketsAccessSettings; +} +export interface PartialUwsTrackerSettings { + server?: Partial; + websockets?: Partial; + access?: Partial; +} +export declare class UWebSocketsTracker { + #private; + readonly tracker: Readonly; + readonly settings: UwsTrackerSettings; + private webSocketsCount; + private validateOrigin; + private readonly maxConnections; + get app(): TemplatedApp; + get stats(): { + webSocketsCount: number; + }; + constructor(tracker: Readonly, settings: PartialUwsTrackerSettings); + run(): Promise; + private validateAccess; + private buildApplication; + private readonly onOpen; + private readonly onMessage; + private readonly onClose; +} diff --git a/dist/uws-tracker.js b/dist/uws-tracker.js new file mode 100644 index 0000000..c29c487 --- /dev/null +++ b/dist/uws-tracker.js @@ -0,0 +1,204 @@ +"use strict"; +/** + * Copyright 2019 Novage LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) { + if (!privateMap.has(receiver)) { + throw new TypeError("attempted to set private field on non-instance"); + } + privateMap.set(receiver, value); + return value; +}; +var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) { + if (!privateMap.has(receiver)) { + throw new TypeError("attempted to get private field on non-instance"); + } + return privateMap.get(receiver); +}; +var _app; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.UWebSocketsTracker = void 0; +const string_decoder_1 = require("string_decoder"); +const uWebSockets_js_1 = require("uWebSockets.js"); +const Debug = require("debug"); +const tracker_1 = require("./tracker"); +// eslint-disable-next-line new-cap +const debugWebSockets = Debug("wt-tracker:uws-tracker"); +const debugWebSocketsEnabled = debugWebSockets.enabled; +// eslint-disable-next-line new-cap +const debugMessages = Debug("wt-tracker:uws-tracker-messages"); +const debugMessagesEnabled = debugMessages.enabled; +// eslint-disable-next-line new-cap +const debugRequests = Debug("wt-tracker:uws-tracker-requests"); +const debugRequestsEnabled = debugRequests.enabled; +const decoder = new string_decoder_1.StringDecoder(); +class UWebSocketsTracker { + constructor(tracker, settings) { + this.tracker = tracker; + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + _app.set(this, void 0); + this.webSocketsCount = 0; + this.validateOrigin = false; + this.onOpen = (ws, request) => { + var _a, _b; + this.webSocketsCount++; + if ((this.maxConnections !== 0) && (this.webSocketsCount > this.maxConnections)) { + if (debugRequestsEnabled) { + debugRequests(this.settings.server.host, this.settings.server.port, "ws-denied-max-connections url:", request.getUrl(), "query:", request.getQuery(), "origin:", request.getHeader("origin"), "total:", this.webSocketsCount); + } + ws.close(); + return; + } + if (debugWebSocketsEnabled) { + debugWebSockets("connected via URL", request.getUrl()); + } + if (this.validateOrigin) { + const origin = request.getHeader("origin"); + const shoulDeny = ((this.settings.access.denyEmptyOrigin && (origin.length === 0)) + || (((_a = this.settings.access.denyOrigins) === null || _a === void 0 ? void 0 : _a.includes(origin)) === true) + || (((_b = this.settings.access.allowOrigins) === null || _b === void 0 ? void 0 : _b.includes(origin)) === false)); + if (shoulDeny) { + if (debugRequestsEnabled) { + debugRequests(this.settings.server.host, this.settings.server.port, "ws-denied url:", request.getUrl(), "query:", request.getQuery(), "origin:", origin, "total:", this.webSocketsCount); + } + ws.close(); + return; + } + } + if (debugRequestsEnabled) { + debugRequests(this.settings.server.host, this.settings.server.port, "ws-open url:", request.getUrl(), "query:", request.getQuery(), "origin:", request.getHeader("origin"), "total:", this.webSocketsCount); + } + }; + this.onMessage = (ws, message) => { + debugWebSockets("message of size", message.byteLength); + let json = undefined; + try { + json = JSON.parse(decoder.end(new Uint8Array(message))); + } + catch (e) { + debugWebSockets("failed to parse JSON message", e); + ws.close(); + return; + } + if (ws.sendMessage === undefined) { + ws.sendMessage = sendMessage; + } + if (debugMessagesEnabled) { + debugMessages("in", (ws.id === undefined) ? "unknown peer" : Buffer.from(ws.id).toString("hex"), json); + } + try { + this.tracker.processMessage(json, ws); + } + catch (e) { + if (e instanceof tracker_1.TrackerError) { + debugWebSockets("failed to process message from the peer:", e); + ws.close(); + } + else { + throw e; + } + } + }; + this.onClose = (ws, code) => { + this.webSocketsCount--; + if (ws.sendMessage !== undefined) { + this.tracker.disconnectPeer(ws); + } + debugWebSockets("closed with code", code); + }; + this.settings = { + server: Object.assign({ port: 8000, host: "0.0.0.0" }, settings.server), + websockets: Object.assign({ path: "/*", maxPayloadLength: 64 * 1024, idleTimeout: 240, compression: 1, maxConnections: 0 }, settings.websockets), + access: Object.assign({ allowOrigins: undefined, denyOrigins: undefined, denyEmptyOrigin: false }, settings.access), + }; + this.maxConnections = this.settings.websockets.maxConnections; + this.validateAccess(); + __classPrivateFieldSet(this, _app, (this.settings.server.key_file_name === undefined) + // eslint-disable-next-line new-cap + ? uWebSockets_js_1.App(this.settings.server) + // eslint-disable-next-line new-cap + : uWebSockets_js_1.SSLApp(this.settings.server)); + this.buildApplication(); + } + get app() { + return __classPrivateFieldGet(this, _app); + } + get stats() { + return { + webSocketsCount: this.webSocketsCount, + }; + } + async run() { + await new Promise((resolve, reject) => { + __classPrivateFieldGet(this, _app).listen(this.settings.server.host, this.settings.server.port, (token) => { + if (token === false) { + reject(new Error(`failed to listen to ${this.settings.server.host}:${this.settings.server.port}`)); + } + else { + resolve(); + } + }); + }); + } + validateAccess() { + if (this.settings.access.allowOrigins !== undefined) { + if (this.settings.access.denyOrigins !== undefined) { + throw new Error("allowOrigins and denyOrigins can't be set simultaneously"); + } + else if (!(this.settings.access.allowOrigins instanceof Array)) { + throw new Error("allowOrigins configuration paramenters should be an array of strings"); + } + } + else if ((this.settings.access.denyOrigins !== undefined) && !(this.settings.access.denyOrigins instanceof Array)) { + throw new Error("denyOrigins configuration paramenters should be an array of strings"); + } + const origins = (this.settings.access.allowOrigins === undefined + ? this.settings.access.denyOrigins + : this.settings.access.allowOrigins); + if (origins !== undefined) { + for (const origin of origins) { + if (typeof origin !== "string") { + throw new Error("allowOrigins and denyOrigins configuration paramenters should be arrays of strings"); + } + } + } + this.validateOrigin = (this.settings.access.denyEmptyOrigin + || (this.settings.access.allowOrigins !== undefined) + || (this.settings.access.denyOrigins !== undefined)); + } + buildApplication() { + __classPrivateFieldGet(this, _app).ws(this.settings.websockets.path, { + compression: this.settings.websockets.compression, + maxPayloadLength: this.settings.websockets.maxPayloadLength, + idleTimeout: this.settings.websockets.idleTimeout, + open: this.onOpen, + drain: (ws) => { + if (debugWebSocketsEnabled) { + debugWebSockets("drain", ws.getBufferedAmount()); + } + }, + message: this.onMessage, + close: this.onClose, + }); + } +} +exports.UWebSocketsTracker = UWebSocketsTracker; +_app = new WeakMap(); +function sendMessage(json, ws) { + ws.send(JSON.stringify(json), false, false); + if (debugMessagesEnabled) { + debugMessages("out", (ws.id === undefined) ? "unknown peer" : Buffer.from(ws.id).toString("hex"), json); + } +}