├── .npmignore
├── index.ts
├── .gitignore
├── testy.json
├── example
├── example-browser.html
├── example-readme.ts
├── example-browser-server.ts
├── example-browser-client.ts
├── example-ws.ts
└── example-tcp.ts
├── tsconfig.json
├── BUMP_VERSION.md
├── src
├── index.ts
├── VirtualServer.ts
├── TCPServer.ts
├── WrappedClient.ts
├── WSServer.ts
├── ByteSize.ts
├── TCPClient.ts
├── VirtualClient.ts
├── Server.ts
├── WSClient.ts
├── Client.ts
├── types.ts
└── SocketFactory.ts
├── GENERATING_DOCS.md
├── .eslintrc.js
├── test
├── cert
│ └── generate_self_signed_cert.sh
├── VirtualClient.spec.ts
├── connection.spec.ts
├── connectionTCP.spec.ts
├── TCPServer.spec.ts
├── WSClient.spec.ts
├── WSServer.spec.ts
├── connectionTLS.spec.ts
├── connectionTCPTLS.spec.ts
├── TCPClient.spec.ts
└── Server.spec.ts
├── LICENSE
├── package.json
├── CHANGELOG.md
└── README.md
/.npmignore:
--------------------------------------------------------------------------------
1 | test/cert
2 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | node_modules
3 | docs
4 |
--------------------------------------------------------------------------------
/testy.json:
--------------------------------------------------------------------------------
1 | {"include":["test/**/*.spec.ts"]}
2 |
--------------------------------------------------------------------------------
/example/example-browser.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | pocket-sockets (Example)
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node12/tsconfig.json",
3 | "compilerOptions": {
4 | "experimentalDecorators": true,
5 | "preserveConstEnums": true,
6 | "outDir": "build",
7 | "module": "commonjs",
8 | "declaration": true
9 | },
10 | "files": [
11 | "./index.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/BUMP_VERSION.md:
--------------------------------------------------------------------------------
1 | # How to bump version
2 |
3 | 1. Update CHANGELOG.md
4 | 2. Update package.json to new version
5 | 3. Run `npm i` to update package-lock.json
6 | 4. Run `npm run build` to build
7 | 5. Commit changes
8 | 6. Tag commit with new version
9 | 7. Push to remote
10 | 8. Publish to the npm registry
11 | 9. Done
12 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./TCPClient";
2 | export * from "./TCPServer";
3 | export * from "./VirtualClient";
4 | export * from "./VirtualServer";
5 | export * from "./WSClient";
6 | export * from "./WSServer";
7 | export * from "./Client";
8 | export * from "./Server";
9 | export * from "./ByteSize";
10 | export * from "./SocketFactory";
11 | export * from "./WrappedClient";
12 | export * from "./types";
13 |
--------------------------------------------------------------------------------
/GENERATING_DOCS.md:
--------------------------------------------------------------------------------
1 | For each and every file under the _./src_ directory, run an isolated export command line as follows:
2 | ```
3 | ./node_modules/.bin/typedoc --entryDocument Home.md --hideBreadcrumbs true --hideInPageTOC true --cleanOutputDir false ./src/$FILE_NAME_HERE.ts
4 | ```
5 |
6 | Remove the modules index file, then copy the results over to the wiki repository:
7 | ```
8 | rm ./docs/modules.md
9 | cp -r ./docs/* ../pocket-sockets.wiki/.
10 | ```
11 |
12 | Updating the wiki repository:
13 | ```
14 | cd ../pocket-sockets.wiki
15 | git add .
16 | git commit -S -m "Updating documentation to reflect latest code"
17 | git push origin master
18 | ```
19 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | "eslint:recommended",
4 | "plugin:@typescript-eslint/recommended"
5 | ],
6 | parser: "@typescript-eslint/parser",
7 | plugins: [
8 | "@typescript-eslint"
9 | ],
10 | rules: {
11 | "no-cond-assign": "off",
12 | "@typescript-eslint/no-empty-interface": "off",
13 | "@typescript-eslint/no-explicit-any": "off",
14 | "@typescript-eslint/ban-ts-comment": "off",
15 | "@typescript-eslint/no-inferrable-types": "off",
16 | "@typescript-eslint/no-empty-function": "off",
17 | "no-constant-condition": "off",
18 | "@typescript-eslint/no-non-null-assertion": "off"
19 | },
20 | root: true,
21 | };
22 |
--------------------------------------------------------------------------------
/example/example-readme.ts:
--------------------------------------------------------------------------------
1 | // example-readme.ts
2 | // Run: npx ts-node ./example/example-readme.ts
3 |
4 | import {WSServer, WSClient, Client} from "../index";
5 |
6 | const server = new WSServer({
7 | host: "localhost",
8 | port: 8181
9 | });
10 | server.listen();
11 |
12 | server.onConnection( (client: Client) => {
13 | client.onData( (data: Buffer) => {
14 | client.sendString("This is server: received!");
15 | });
16 | client.onClose( () => {
17 | server.close();
18 | });
19 | });
20 |
21 | const client = new WSClient({
22 | host: "localhost",
23 | port: 8181
24 | });
25 | client.connect();
26 |
27 | client.onConnect( () => {
28 | client.onData( (data: Buffer) => {
29 | client.close();
30 | });
31 | client.sendString("This is client: hello");
32 | });
33 |
34 | console.log("pocket-sockets OK");
35 |
--------------------------------------------------------------------------------
/test/cert/generate_self_signed_cert.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | SUBJECT=/C=SE/ST=Unknown/L=Stockholm/O=PocketSocketsTestCA
4 |
5 | # Root Certificate Authority
6 | CA="testCA"
7 | openssl genrsa -out "${CA}".key 2048
8 | openssl req -x509 -new -nodes -key "${CA}".key -sha256 -days 36500 -out "${CA}".cert -subj "${SUBJECT}"/CN="${CA}"
9 | openssl x509 -in "${CA}".cert -out "${CA}".pem -text
10 |
11 | # Server
12 | NAME="localhost"
13 | openssl genrsa -out "${NAME}".key 2048
14 | openssl req -new -out "${NAME}".csr -key "${NAME}".key -subj "${SUBJECT}"/CN="${NAME}"
15 | openssl x509 -req -days 36500 -in "${NAME}".csr -out "${NAME}".cert -CA "${CA}".cert -CAkey "${CA}".key -CAcreateserial
16 |
17 | # Client
18 | NAME="testClient"
19 | openssl genrsa -out "${NAME}".key 2048
20 | openssl req -new -key "${NAME}".key -out "${NAME}".csr -subj "${SUBJECT}"/CN="${NAME}"
21 | openssl x509 -req -days 36500 -in "${NAME}".csr -CA "${CA}".pem -CAkey "${CA}".key -CAcreateserial -out "${NAME}".cert
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021-2022 Thomas Backlund and others
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/example/example-browser-server.ts:
--------------------------------------------------------------------------------
1 | //
2 | // example-browser-server.ts
3 | //
4 | // Run: npx ts-node ./example/example-browser-server.ts
5 | //
6 | // Expected output:
7 | // pocket-sockets: WS server example
8 | // Server: listening...
9 | // Server: socket accepted
10 | // Server: incoming client data
11 | // Server: client connection closed
12 | //
13 |
14 | import {WSServer} from "../src/WSServer";
15 | import {Client} from "../src/Client";
16 |
17 | console.log("pocket-sockets: WS server example");
18 | const serverOptions = {
19 | host: "0.0.0.0",
20 | port: 8181
21 | };
22 | const server = new WSServer(serverOptions);
23 | server.listen();
24 | console.log("Server: listening...");
25 |
26 | server.onConnection( (client: Client) => {
27 | console.log("Server: socket accepted");
28 | client.onData( (data: Buffer) => {
29 | console.log("Server: incoming client data", data);
30 | client.sendString("This is server: received!");
31 | });
32 | client.onClose( () => {
33 | console.log("Server: client connection closed");
34 | server.close();
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/src/VirtualServer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Server,
3 | } from "./Server";
4 |
5 | import {
6 | VirtualClient,
7 | } from "./VirtualClient";
8 |
9 | import {
10 | ServerOptions,
11 | } from "./types";
12 |
13 | /**
14 | * Simulate a server socket listening.
15 | */
16 | export class VirtualServer extends Server
17 | {
18 | /**
19 | * @param serverOptions are ignored.
20 | */
21 | constructor(serverOptions: ServerOptions) {
22 | super(serverOptions);
23 | this.serverCreate();
24 | }
25 |
26 | /**
27 | * Simulate that a client has connected.
28 | * @param client the client which has connected to this server.
29 | * Note that the given client must also be paired with its counterpart.
30 | */
31 | public simulateConnection(client: VirtualClient) {
32 | this.addClient(client);
33 | }
34 |
35 | /**
36 | * Trigger an error event.
37 | */
38 | public simulateError(error: Error) {
39 | this.serverError(error.message);
40 | }
41 |
42 | /**
43 | * Specifies how the server gets initialized, then creates the server with the specified options.
44 | */
45 | protected serverCreate()
46 | {
47 | // Do nothing
48 | }
49 |
50 | protected serverListen()
51 | {
52 | // Do nothing
53 | }
54 |
55 | protected serverClose() {
56 | this.serverClosed();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/example/example-browser-client.ts:
--------------------------------------------------------------------------------
1 | //
2 | // example-browser-client.ts
3 | //
4 | // Last tested with Parcel version 1.12.3 and example-browser-server.ts.
5 | //
6 | // Setup and build client:
7 | // npm add parcel@1.12.3 --save-dev
8 | // npx parcel build --no-minify --no-source-maps --out-dir build --public-url . --target browser ./example/example-browser.html
9 | //
10 | // Run server:
11 | // npx ts-node ./example/example-browser-server.ts
12 | //
13 | // Browse to _./build/example-browser.html_
14 | //
15 | // Expected output:
16 | // pocket-sockets: WS browser client example
17 | // Client: connecting...
18 | // GET ws://localhost:8181/
19 | // Client: connected
20 | // Client: incoming server data
21 | // Uint8Array(25) [ 84, 104, 105, 115, 32, 105, 115, 32, 115, 101, … ]
22 | // Client: closed
23 | //
24 |
25 | import {WSClient} from "../src/WSClient";
26 |
27 | console.log("pocket-sockets: WS browser client example");
28 |
29 | const clientOptions = {
30 | host: "localhost",
31 | port: 8181
32 | };
33 | const client = new WSClient(clientOptions);
34 | client.connect();
35 | console.log("Client: connecting...");
36 |
37 | client.onConnect( () => {
38 | console.log("Client: connected");
39 | client.onData( (data: Buffer) => {
40 | console.log("Client: incoming server data", data);
41 | client.close();
42 | });
43 | client.onClose( () => {
44 | console.log("Client: closed");
45 | });
46 | client.sendString("This is client: hello");
47 | });
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pocket-sockets",
3 | "version": "4.1.0",
4 | "description": "A powerful and smooth client/server sockets library for browser and NodeJS. Supports both WebSockets and regular TCP sockets, TLS encrypted or not.",
5 | "keywords": [
6 | "sockets",
7 | "library",
8 | "browser",
9 | "nodejs",
10 | "websockets",
11 | "raw",
12 | "tcp",
13 | "encrypted",
14 | "encryption",
15 | "networking",
16 | "communication",
17 | "layer"
18 | ],
19 | "homepage": "",
20 | "author": "Thomas Backlund",
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/bashlund/pocket-sockets.git"
24 | },
25 | "bugs": {
26 | "url": "https://github.com/bashlund/pocket-sockets/issues"
27 | },
28 | "private": false,
29 | "license": "MIT",
30 | "engines": {
31 | "node": ">=12"
32 | },
33 | "main": "build/index.js",
34 | "types": "build/index.d.ts",
35 | "scripts": {
36 | "test": "testyts",
37 | "docs": "typedoc --entryDocument Home.md --hideBreadcrumbs true --hideInPageTOC true --cleanOutputDir false ./src/*.ts",
38 | "build": "tsc",
39 | "tsc": "tsc",
40 | "lint": "npx eslint ./src",
41 | "prepublishOnly": "tsc"
42 | },
43 | "dependencies": {
44 | "ws": "^8.16.0"
45 | },
46 | "devDependencies": {
47 | "@tsconfig/node12": "^1.0.7",
48 | "@types/node": "^14.17.11",
49 | "@types/ws": "^8.5.10",
50 | "@typescript-eslint/eslint-plugin": "^6.19.1",
51 | "@typescript-eslint/parser": "^6.19.1",
52 | "eslint": "^8.56.0",
53 | "testyts": "^1.3.0",
54 | "ts-node": "^9.1.1",
55 | "typedoc": "^0.22.15",
56 | "typedoc-plugin-markdown": "^3.11.0",
57 | "typescript": "^4.3.5"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/example/example-ws.ts:
--------------------------------------------------------------------------------
1 | //
2 | // example-ws.ts
3 | //
4 | // Run: npx ts-node ./example/example-ws.ts
5 | //
6 | // Expected output:
7 | // pocket-sockets: WS example
8 | // Server: listening...
9 | // Client: connecting...
10 | // Server: socket accepted
11 | // Client: connected
12 | // Server: incoming client data
13 | // Client: incoming server data
14 | // Client: closed
15 | // Server: client connection closed
16 | //
17 |
18 | import {WSServer, WSClient, ClientInterface} from "../index";
19 |
20 | console.log("pocket-sockets: WS example");
21 | const serverOptions = {
22 | host: "localhost",
23 | port: 8181,
24 | // If wanting to send/recieve in text-mode set textMode to true.
25 | //textMode: true,
26 | };
27 | const server = new WSServer(serverOptions);
28 | server.listen();
29 | console.log("Server: listening...");
30 |
31 | server.onConnection( (client: ClientInterface) => {
32 | console.log("Server: socket accepted");
33 | client.onData( (data: Buffer | string) => {
34 | console.log("Server: incoming client data", data);
35 | client.send("This is server: received!");
36 | });
37 | client.onClose( () => {
38 | console.log("Server: client connection closed");
39 | server.close();
40 | });
41 | });
42 |
43 | const clientOptions = {
44 | host: "localhost",
45 | port: 8181,
46 | // If wanting to send/recieve in text-mode set textMode to true.
47 | //textMode: true,
48 | };
49 | const client = new WSClient(clientOptions);
50 | client.connect();
51 | console.log("Client: connecting...");
52 |
53 | client.onConnect( () => {
54 | console.log("Client: connected");
55 | client.onData( (data: Buffer | string) => {
56 | console.log("Client: incoming server data", data);
57 | client.close();
58 | });
59 | client.onClose( () => {
60 | console.log("Client: closed");
61 | });
62 | client.send("This is client: hello");
63 | });
64 |
--------------------------------------------------------------------------------
/example/example-tcp.ts:
--------------------------------------------------------------------------------
1 | //
2 | // example-tcp.ts
3 | //
4 | // Run: npx ts-node ./example/example-tcp.ts
5 | //
6 | // Expected output:
7 | // pocket-sockets: TCP example
8 | // Server: listening...
9 | // Client: connecting...
10 | // Server: socket accepted
11 | // Client: connected
12 | // Server: incoming client data
13 | // Client: incoming server data
14 | // Server: client connection closed
15 | // Client: closed
16 | //
17 |
18 | import {TCPServer, TCPClient, ClientInterface} from "../index";
19 |
20 | console.log("pocket-sockets: TCP example");
21 | const serverOptions = {
22 | host: "localhost",
23 | port: 8181,
24 | // If wanting to send/recieve in text-mode set textMode to true.
25 | //textMode: true,
26 | };
27 | const server = new TCPServer(serverOptions);
28 | server.listen();
29 | console.log("Server: listening...");
30 |
31 | server.onConnection( (client: ClientInterface) => {
32 | console.log("Server: socket accepted");
33 | client.onData( (data: Buffer | string) => {
34 | console.log("Server: incoming client data", data);
35 | client.send("This is server: received!");
36 | });
37 | client.onClose( () => {
38 | console.log("Server: client connection closed");
39 | server.close();
40 | });
41 | });
42 |
43 | const clientOptions = {
44 | host: "localhost",
45 | port: 8181,
46 | // If wanting to send/recieve in text-mode set textMode to true.
47 | //textMode: true,
48 | };
49 | const client = new TCPClient(clientOptions);
50 | client.connect();
51 | console.log("Client: connecting...");
52 |
53 | client.onConnect( () => {
54 | console.log("Client: connected");
55 | client.onData( (data: Buffer | string) => {
56 | console.log("Client: incoming server data", data);
57 | client.close();
58 | });
59 | client.onClose( () => {
60 | console.log("Client: closed");
61 | });
62 | client.send("This is client: hello");
63 | });
64 |
--------------------------------------------------------------------------------
/src/TCPServer.ts:
--------------------------------------------------------------------------------
1 | import * as net from "net";
2 | import * as tls from "tls";
3 |
4 | import {Server} from "./Server";
5 | import {TCPClient} from "./TCPClient";
6 | import {ServerOptions} from "./types";
7 |
8 | /**
9 | * TCP server implementation.
10 | */
11 | export class TCPServer extends Server
12 | {
13 | protected server?: net.Server;
14 |
15 | constructor(serverOptions: ServerOptions) {
16 | super(serverOptions);
17 | this.serverCreate();
18 | }
19 |
20 | /**
21 | * Specifies how the server gets initialized, then creates the server with the specified options.
22 | */
23 | protected serverCreate()
24 | {
25 | const USE_TLS = this.serverOptions.cert != null;
26 |
27 | if(USE_TLS) {
28 | const tlsOptions = {
29 | cert: this.serverOptions.cert,
30 | key: this.serverOptions.key,
31 | requestCert: this.serverOptions.requestCert,
32 | rejectUnauthorized: this.serverOptions.rejectUnauthorized,
33 | ca: this.serverOptions.ca,
34 | handshakeTimeout: 30000,
35 | };
36 | this.server = tls.createServer(tlsOptions);
37 | this.server?.on("secureConnection", this.clientConnected);
38 | }
39 | else {
40 | this.server = net.createServer();
41 | this.server?.on("connection", this.clientConnected);
42 | }
43 |
44 | this.server?.on("error", this.error);
45 | this.server?.on("close", this.serverClosed);
46 | this.server?.on("tlsClientError", (_err: any, socket: net.Socket) => socket.end());
47 | }
48 |
49 | /**
50 | * Starts a previously created server listening for connections.
51 | * Assumes the server is instantiated during object creation.
52 | */
53 | protected serverListen()
54 | {
55 | this.server?.listen({
56 | host: this.serverOptions.host,
57 | port: this.serverOptions.port,
58 | ipv6Only: this.serverOptions.ipv6Only,
59 | });
60 | }
61 |
62 | protected serverClose() {
63 | if (this.server) {
64 | this.server.close();
65 | }
66 | }
67 |
68 | protected clientConnected = (socket: net.Socket) => {
69 | const client = new TCPClient({
70 | bufferData: this.serverOptions.bufferData,
71 | port: this.serverOptions.port,
72 | textMode: this.serverOptions.textMode,
73 | }, socket);
74 |
75 | this.addClient(client);
76 | }
77 |
78 | protected error = (error: Error) => {
79 | this.serverError(error.message);
80 | };
81 | }
82 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG: pocket-sockets
2 |
3 | ## [4.1.0] - 20250807
4 | Simplify event handling..
5 | Improve data unread functionality..
6 |
7 | ## [4.0.0] - 20240307
8 | Improve event handling.
9 | Bug fix.
10 |
11 | ## [3.0.0] - 20240220
12 | Add text-mode options to socket.
13 | Add WrappedClient to allow for more complex clients.
14 | Add helper functions getSocket and isWebSocket to ClientInterface.
15 | Block possibility to listen to port 0.
16 | Add linting.
17 | Add ClientInterface.isClosed() and Server.isClosed().
18 | Add ClientInterface.init() to allow for other more complex clients.
19 | Add WrappedClient to allow wrapping a Client object.
20 |
21 | ## [2.0.2] - 20230628
22 | Fix tests to consider member data attributes and other API changes
23 | Add missing class access modifiers.
24 | Update package lock to version 2.
25 | Bump dependencies based on security advisory.
26 | Implement ClientInterface and SocketFactoryInterface.
27 | Make ErrorCallback type more specific.
28 |
29 | ## [2.0.0] - 20221209
30 | Change onError callback signature (breaking).
31 | Change class access modifiers from private to protected.
32 | Add optional reconnect attempt in case of connection overflow.
33 | Add isServer argument to checkConnectionsOverflow.
34 | npm audit fix minimatch.
35 |
36 | ## [1.2.0] - 20221010
37 | Refactor throw "..." to throw new Error("...")
38 | Add SocketFactory + tests
39 | Add VirtualServer class to complement VirtualClient
40 | Implement getRemoteAddress, getRemotePort, getLocalPort, getLocalAddress for WSClient and VirtualClient
41 | Add optional closeClients parameter to Server.close()
42 | Improve self-signed cert generation script and add rejectUnauthorized tests
43 | Add TLS connection test suites
44 | Add TCP connection test suite covering IPv4 and IPv6 host validation
45 | Add TCP connection test suite covering IPv4 and IPv6 host validation
46 | Ensure WSClient IPv6 host gets surrounded with brackets during socketConnect
47 | Verify and set default error message when ws.ErrorEvent is undefined
48 |
49 | ## [1.1.1] - 20220516
50 | Audit npm packages version
51 |
52 | ## [1.1.0] - 20220516
53 | Fix timing bug about unreferenced variable.
54 | Add ByteSize class to await chunks of incoming data.
55 | Add Client.unRead function to be able to put read data back into buffer.
56 |
57 | ## [1.0.1] - 20210928
58 | Add configuration settings to allow direct import from node modules (npm)
59 |
60 | ## [1.0.0] - 20210928
61 | Test suite added.
62 | Wiki added.
63 | Examples added.
64 | README.md updated.
65 | AbstractClient class refactored to Client and declared as abstract class.
66 | AbstractServer class refactored to Server and declared as abstract class.
67 |
68 | ## [0.9.0] - 20210824
69 | First release.
70 |
--------------------------------------------------------------------------------
/src/WrappedClient.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ClientInterface,
3 | WrappedClientInterface,
4 | SocketErrorCallback,
5 | SocketDataCallback,
6 | SocketConnectCallback,
7 | SocketCloseCallback,
8 | } from "./types";
9 |
10 | /**
11 | * The WrappedClient wraps an already connected client and is a base class to extend where
12 | * specific methods can be overriden to transform incoming and outgoint socket data.
13 | */
14 | export class WrappedClient implements WrappedClientInterface {
15 | constructor(protected client: ClientInterface) {}
16 |
17 | public getClient(): ClientInterface {
18 | return this.client;
19 | }
20 |
21 | public async init() {
22 | }
23 |
24 | public getSocket(): any {
25 | return this.client.getSocket();
26 | }
27 |
28 | public isWebSocket(): boolean {
29 | return this.client.isWebSocket();
30 | }
31 |
32 | public isTextMode(): boolean {
33 | return this.client.isTextMode();
34 | }
35 |
36 | public connect() {
37 | throw new Error("The WrappedClient's underlaying socket must already have been connected");
38 | }
39 |
40 | public unRead(data: Buffer | string) {
41 | this.client.unRead(data);
42 | }
43 |
44 | public close() {
45 | this.client.close();
46 | }
47 |
48 | public isClosed(): boolean {
49 | return this.client.isClosed();
50 | }
51 |
52 | public send(data: Buffer | string) {
53 | this.client.send(data);
54 | }
55 |
56 | public onError(fn: SocketErrorCallback) {
57 | this.client.onError(fn);
58 | }
59 |
60 | public offError(fn: SocketErrorCallback) {
61 | this.client.offError(fn);
62 | }
63 |
64 | public onData(fn: SocketDataCallback) {
65 | this.client.onData(fn);
66 | }
67 |
68 | public offData(fn: SocketDataCallback) {
69 | this.client.offData(fn);
70 | }
71 |
72 | public onConnect(fn: SocketConnectCallback) {
73 | this.client.onConnect(fn);
74 | }
75 |
76 | public offConnect(fn: SocketConnectCallback) {
77 | this.client.offConnect(fn);
78 | }
79 |
80 | public onClose(fn: SocketCloseCallback) {
81 | this.client.onClose(fn);
82 | }
83 |
84 | public offClose(fn: SocketCloseCallback) {
85 | this.client.offClose(fn);
86 | }
87 |
88 | public getLocalAddress(): string | undefined {
89 | return this.client.getLocalAddress();
90 | }
91 |
92 | public getRemoteAddress(): string | undefined {
93 | return this.client.getRemoteAddress();
94 | }
95 |
96 | public getRemotePort(): number | undefined {
97 | return this.client.getRemotePort();
98 | }
99 |
100 | public getLocalPort(): number | undefined {
101 | return this.client.getLocalPort();
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pocket-sockets
2 |
3 | A powerful and smooth client/server sockets library for browser and _Node.js_, written in TypeScript with very few dependencies.
4 |
5 | :heavy_check_mark: Written in TypeScript.
6 |
7 | :heavy_check_mark: Support for both _WebSockets_ and regular _TCP_ sockets with a unified interface.
8 |
9 | :heavy_check_mark: Works both in _NodeJS_ and browser.
10 |
11 | :heavy_check_mark: Supports SSL/TLS encryption with certificates.
12 |
13 | :heavy_check_mark: Test suite of 105 tests.
14 |
15 | ## WebSockets vs. regular TCP sockets
16 | _WebSockets_ are a must when using a browser, however plain _TCP_ sockets are faster and a good choice when no browser is involved.
17 |
18 | The overall interface for _pocket-sockets_ _WebSocket_ and _TCP_ sockets **are identical** so it is easy to switch between the underlying implementations.
19 |
20 | ## Example
21 | For a quick glimpse of what it looks like to set up a server that receives a string from clients, then replies back and closes the connection afterwards, follow the example below:
22 | ```typescript
23 | import {WSServer, WSClient, ClientInterface} from "pocket-sockets";
24 |
25 | const server = new WSServer({
26 | host: "localhost",
27 | port: 8181
28 | });
29 | server.listen();
30 |
31 | server.onConnection( (client: ClientInterface) => {
32 | client.onData( (data: Buffer | string) => {
33 | client.send("This is server: received!");
34 | });
35 | client.onClose( () => {
36 | server.close();
37 | });
38 | });
39 |
40 | const client = new WSClient({
41 | host: "localhost",
42 | port: 8181
43 | });
44 | client.connect();
45 |
46 | client.onConnect( () => {
47 | client.onData( (data: Buffer | string) => {
48 | client.close();
49 | });
50 | client.send("This is client: hello");
51 | });
52 | ```
53 |
54 | For complete examples, please refer to the files under the [./example](https://github.com/bashlund/pocket-sockets/tree/main/example) directory.
55 |
56 | ## Run tests
57 | ```sh
58 | git clone https://github.com/bashlund/pocket-sockets.git
59 | cd pocket-sockets
60 | npm isntall
61 | cd ./test/cert/ && ./generate_self_signed_cert.sh && cd ../..
62 | npm test
63 | ```
64 |
65 | ## Run examples
66 | ```sh
67 | git clone https://github.com/bashlund/pocket-sockets.git
68 | cd pocket-sockets
69 | npm isntall
70 | npx ts-node ./example/example-ws.ts
71 | npx ts-node ./example/example-tcp.ts
72 | ```
73 |
74 | ## Use in browser
75 | For browser examples, please refer to the files under the [./example](https://github.com/bashlund/pocket-sockets/tree/main/example) directory.
76 |
77 | ## NPM
78 | ```sh
79 | npm add --save pocket-sockets
80 | ```
81 |
82 | ## Reference
83 | Code documentation and API references are available in the official [Wiki](https://github.com/bashlund/pocket-sockets/wiki): [https://github.com/bashlund/pocket-sockets/wiki](https://github.com/bashlund/pocket-sockets/wiki).
84 |
85 | ## Credits
86 | Lib written by @bashlund, tests and wiki nicely crafted by @filippsen.
87 |
88 | ## License
89 | This project is released under the _MIT_ license.
90 |
--------------------------------------------------------------------------------
/src/WSServer.ts:
--------------------------------------------------------------------------------
1 | import {Server} from "./Server";
2 | import {WSClient} from "./WSClient";
3 | import {ServerOptions} from "./types";
4 | import * as WebSocket from "ws";
5 | import * as http from "http";
6 | import * as https from "https";
7 |
8 | /**
9 | * WebSocket server implementation.
10 | */
11 | export class WSServer extends Server
12 | {
13 | protected server?: http.Server | https.Server;
14 | protected wsServer?: WebSocket.Server;
15 |
16 | constructor(serverOptions: ServerOptions) {
17 | super(serverOptions);
18 | this.serverCreate();
19 | }
20 |
21 | /**
22 | * Specifies how the server gets initialized, then creates the server with the specified options.
23 | */
24 | protected serverCreate()
25 | {
26 | const USE_TLS = this.serverOptions.cert != null;
27 |
28 | if(USE_TLS) {
29 | const tlsOptions = {
30 | cert: this.serverOptions.cert,
31 | key: this.serverOptions.key,
32 | requestCert: this.serverOptions.requestCert,
33 | rejectUnauthorized: this.serverOptions.rejectUnauthorized,
34 | ca: this.serverOptions.ca,
35 | handshakeTimeout: 30000,
36 | };
37 | this.server = https.createServer(tlsOptions);
38 | if (this.server) {
39 | this.server.on("tlsClientError", this.error);
40 | }
41 | }
42 | else {
43 | this.server = http.createServer();
44 | }
45 | }
46 |
47 | /**
48 | * Starts a previously created server listening for connections.
49 | * Assumes the server is instantiated during object creation.
50 | */
51 | protected serverListen() {
52 | this.wsServer = new WebSocket.Server({
53 | path: "/",
54 | server: this.server,
55 | clientTracking: true,
56 | perMessageDeflate: false,
57 | maxPayload: 100 * 1024 * 1024,
58 | });
59 |
60 | this.wsServer.on("connection", this.clientConnected);
61 | this.wsServer.on("error", this.error);
62 | this.wsServer.on("close", this.serverClosed);
63 |
64 | this.server?.listen({
65 | host: this.serverOptions.host,
66 | port: this.serverOptions.port,
67 | ipv6Only: this.serverOptions.ipv6Only,
68 | });
69 | }
70 |
71 | /**
72 | * Overrides server close procedure.
73 | */
74 | protected serverClose() {
75 | if (this.wsServer) {
76 | this.wsServer.close();
77 | }
78 | if (this.server) {
79 | this.server.close();
80 | }
81 | }
82 |
83 | protected clientConnected = (socket: WebSocket) => {
84 | const client = new WSClient({
85 | bufferData: this.serverOptions.bufferData,
86 | port: this.serverOptions.port,
87 | textMode: this.serverOptions.textMode,
88 | }, socket);
89 |
90 | this.addClient(client);
91 | }
92 |
93 | protected error = (error: Error) => {
94 | this.serverError(error.message);
95 | };
96 | }
97 |
--------------------------------------------------------------------------------
/src/ByteSize.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This can be used to await X nr of bytes on the client socket.
3 | * It is allowed to use text mode sockets, but the data resolve will always be as Buffer.
4 | *
5 | */
6 | import {ClientInterface} from "./types";
7 |
8 | export class ByteSize
9 | {
10 | protected client: ClientInterface;
11 | protected data: Buffer;
12 | protected resolve?: (data: Buffer) => void;
13 | protected reject?: (error: Error) => void;
14 | protected ended: boolean;
15 | protected nrBytes: number = 0;
16 | protected timeoutId?: ReturnType;
17 |
18 | constructor(client: ClientInterface) {
19 | this.client = client;
20 | this.data = Buffer.alloc(0);
21 | this.client.onData(this.onData);
22 | this.client.onClose(this.onClose);
23 | this.ended = false;
24 | }
25 |
26 | /**
27 | * @param nrBytes nr bytes to wait for and resolve. The reminder is "unread" back to socket buffer.
28 | * If set to -1 then wait for any data and resolve all data available at that point.
29 | */
30 | public async read(nrBytes: number, timeout: number = 3000): Promise {
31 | if (this.ended || this.timeoutId) {
32 | throw new Error("Cannot reuse a ByteSize");
33 | }
34 | this.nrBytes = nrBytes;
35 | if (timeout) {
36 | this.timeoutId = setTimeout( () => {
37 | delete this.timeoutId;
38 | if (this.reject) {
39 | const reject = this.reject;
40 | this.end();
41 | reject(new Error("Timeout"));
42 | }
43 | }, timeout);
44 | }
45 | return new Promise( (resolve, reject) => {
46 | this.resolve = resolve;
47 | this.reject = reject;
48 | this.onData(Buffer.alloc(0));
49 | });
50 | }
51 |
52 | protected onClose = () => {
53 | if (this.reject) {
54 | const reject = this.reject;
55 | this.end();
56 | reject(new Error("Socket closed"));
57 | }
58 | }
59 |
60 | protected onData = (buf: Buffer | string) => {
61 | if (this.ended) {
62 | return;
63 | }
64 |
65 | if (!Buffer.isBuffer(buf)) {
66 | buf = Buffer.from(buf);
67 | }
68 |
69 | this.data = Buffer.concat([this.data, buf]);
70 | if (!this.resolve) {
71 | return;
72 | }
73 |
74 | if (this.data.length >= this.nrBytes) {
75 | if (this.nrBytes < 0 && this.data.length === 0) {
76 | return;
77 | }
78 |
79 | const nrBytes = this.nrBytes < 0 ? this.data.length : this.nrBytes;
80 |
81 | const bite = this.data.slice(0, nrBytes);
82 | this.data = this.data.slice(nrBytes);
83 | const resolve = this.resolve;
84 | this.end();
85 | resolve(bite);
86 | }
87 | }
88 |
89 | protected end() {
90 | if (this.ended) {
91 | return;
92 | }
93 | this.ended = true;
94 | if (this.timeoutId) {
95 | clearTimeout(this.timeoutId);
96 | }
97 | this.client.offData(this.onData);
98 | this.client.offClose(this.onClose);
99 | delete this.reject;
100 | delete this.resolve;
101 | if (this.client.isTextMode()) {
102 | this.client.unRead(this.data.toString());
103 | }
104 | else {
105 | this.client.unRead(this.data);
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/TCPClient.ts:
--------------------------------------------------------------------------------
1 | import * as net from "net";
2 | import * as tls from "tls";
3 |
4 | import {Client} from "./Client";
5 | import {ClientOptions} from "./types";
6 |
7 | /**
8 | * TCP client socket implementation.
9 | */
10 | export class TCPClient extends Client
11 | {
12 | protected socket?: net.Socket;
13 |
14 | constructor(clientOptions?: ClientOptions, socket?: net.Socket) {
15 | super(clientOptions);
16 | this.socket = socket;
17 |
18 | if (this.socket) {
19 | this.socketHook();
20 | }
21 | }
22 |
23 | public getSocket(): net.Socket {
24 | if (!this.socket) {
25 | throw new Error("Socket not initiated.");
26 | }
27 |
28 | return this.socket;
29 | }
30 |
31 | /**
32 | * @return {string | undefined} local IP address
33 | */
34 | public getLocalAddress(): string | undefined {
35 | if (this.socket && typeof this.socket.localAddress === "string") {
36 | return this.socket.localAddress;
37 | }
38 | }
39 |
40 | /**
41 | * @return {string | undefined} remote IP address
42 | */
43 | public getRemoteAddress(): string | undefined {
44 | if (this.socket && typeof this.socket.remoteAddress === "string") {
45 | return this.socket.remoteAddress;
46 | }
47 | }
48 |
49 | /**
50 | * @return {number | undefined} remote port
51 | */
52 | public getRemotePort(): number | undefined {
53 | if (this.socket && typeof this.socket.remotePort === "number") {
54 | return this.socket.remotePort;
55 | }
56 | }
57 |
58 | /**
59 | * @return {number | undefined} local port
60 | */
61 | public getLocalPort(): number | undefined {
62 | if (this.socket && typeof this.socket.localPort === "number") {
63 | return this.socket.localPort;
64 | }
65 | }
66 |
67 | /**
68 | * Specifies how the socket gets initialized and created, then establishes a connection.
69 | */
70 | protected socketConnect() {
71 | if (this.socket) {
72 | throw new Error("Socket already created.");
73 | }
74 |
75 | if (!this.clientOptions) {
76 | throw new Error("clientOptions is required to create socket.");
77 | }
78 |
79 | const USE_TLS = this.clientOptions.secure ? true: false;
80 |
81 | if(USE_TLS) {
82 | this.socket = tls.connect({
83 | host: this.clientOptions.host,
84 | port: this.clientOptions.port,
85 | cert: this.clientOptions.cert,
86 | key: this.clientOptions.key,
87 | rejectUnauthorized: this.clientOptions.rejectUnauthorized,
88 | ca: this.clientOptions.ca
89 | });
90 | if (this.socket) {
91 | this.socket.on("secureConnect", this.socketConnected);
92 | }
93 | }
94 | else {
95 | this.socket = net.connect({
96 | host: this.clientOptions.host,
97 | port: this.clientOptions.port,
98 | });
99 | if (this.socket) {
100 | this.socket.on("connect", this.socketConnected);
101 | }
102 | }
103 |
104 | if (!this.socket) {
105 | throw new Error("Could not create socket.");
106 | }
107 | }
108 |
109 | /**
110 | * Specifies hooks to be called as part of the connect procedure.
111 | */
112 | protected socketHook() {
113 | if (!this.socket) {
114 | return;
115 | }
116 |
117 | this.socket.on("data", this.socketData); // Incoming data
118 | this.socket.on("error", this.error); // Error connecting
119 | this.socket.on("close", this.socketClosed); // Socket closed
120 | }
121 |
122 | protected unhookError() {
123 | this.socket?.off("error", this.error);
124 | }
125 |
126 | /**
127 | * Defines how data gets written to the socket.
128 | *
129 | * @param {data} Buffer or string - data to be sent
130 | */
131 | protected socketSend(data: Buffer | string) {
132 | if (!this.socket) {
133 | throw new Error("Socket not instantiated");
134 | }
135 |
136 | if (!Buffer.isBuffer(data)) {
137 | this.socket.write(Buffer.from(data));
138 | }
139 | else {
140 | this.socket.write(data);
141 | }
142 | }
143 |
144 | /**
145 | * Defines the steps to be performed during close.
146 | */
147 | protected socketClose() {
148 | if (this.socket) {
149 | this.socket.end();
150 | }
151 | }
152 |
153 | protected error = (error: Error) => {
154 | this.socketError(error.message || "TCP socket could not connect");
155 | };
156 | }
157 |
--------------------------------------------------------------------------------
/test/VirtualClient.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestSuite, Test } from "testyts";
2 | import {CreatePair} from "../index";
3 | //@ts-ignore
4 | import {VirtualClient} from "../src/VirtualClient";
5 |
6 | const assert = require("assert");
7 |
8 | @TestSuite()
9 | export class VirtualClientConstructor {
10 | /*@Test()
11 | public missing_pairedSocket() {
12 | let client;
13 | assert.doesNotThrow(() => {
14 | client = new VirtualClient();
15 | //@ts-ignore
16 | assert(!client.pairedSocket);
17 | });
18 | }*/
19 |
20 | @Test()
21 | public successful_call() {
22 | let client;
23 | assert.doesNotThrow(() => {
24 | let client1, client2;
25 | [client1, client2 ] = CreatePair();
26 | //@ts-ignore
27 | assert(client1.pairedSocket);
28 | //@ts-ignore
29 | assert(client2.pairedSocket);
30 | //@ts-ignore
31 | assert(client2.pairedSocket!.pairedSocket);
32 | //@ts-ignore
33 | assert(client2.pairedSocket!.outQueue.length == 0);
34 | });
35 | }
36 | }
37 |
38 | @TestSuite()
39 | export class VirtualClientSetLatency {
40 | @Test()
41 | public successful_call() {
42 | let client;
43 | assert.doesNotThrow(() => {
44 | let client1, client2;
45 | [client1, client2 ] = CreatePair();
46 | //@ts-ignore
47 | assert(client1.latency == 0);
48 | client1.setLatency(20);
49 | //@ts-ignore
50 | assert(client1.latency == 20);
51 | });
52 | }
53 | }
54 |
55 | @TestSuite()
56 | export class VirtualClientSocketHook {
57 | @Test()
58 | public noop_call() {
59 | let client;
60 | assert.doesNotThrow(() => {
61 | let client1, client2;
62 | [client1, client2 ] = CreatePair();
63 | //@ts-ignore: protected method
64 | client1.socketHook();
65 | //@ts-ignore: protected method
66 | client2.socketHook();
67 | });
68 | }
69 | }
70 |
71 | @TestSuite()
72 | export class VirtualClientSocketSend {
73 | /*@Test()
74 | public call_without_paired_socket() {
75 | let client;
76 | assert.doesNotThrow(() => {
77 | client = new VirtualClient();
78 | assert(client.outQueue.length == 0);
79 | //@ts-ignore
80 | client.socketSend(Buffer.from("testdata"));
81 | assert(client.outQueue.length == 0);
82 | });
83 | }*/
84 |
85 | @Test()
86 | public successful_call() {
87 | let client;
88 | assert.doesNotThrow(() => {
89 | let client1, client2;
90 | [client1, client2 ] = CreatePair();
91 | //@ts-ignore
92 | assert(client1.outQueue.length == 0);
93 |
94 | let called = false;
95 | //@ts-ignore
96 | client1.copyToPaired = function() {
97 | called = true;
98 | }
99 |
100 | //@ts-ignore
101 | client1.socketSend(Buffer.from("testdata"));
102 | //@ts-ignore: value is expected to be changed by call to copyToPaired
103 | assert(called == true);
104 | });
105 | }
106 | }
107 |
108 | @TestSuite()
109 | export class VirtualClientSocketClosed {
110 | @Test()
111 | public successful_call() {
112 | let client;
113 | assert.doesNotThrow(() => {
114 | let client1: any, client2: any;
115 | [client1, client2 ] = CreatePair();
116 |
117 | let closeCounter = 0;
118 |
119 | //@ts-ignore: protected method
120 | client1.socketClosed = function() {
121 | closeCounter++;
122 | }
123 | //@ts-ignore: protected method
124 | client2.socketClosed = function() {
125 | closeCounter++;
126 | }
127 |
128 | assert(closeCounter == 0);
129 | //@ts-ignore: protected method
130 | client1.socketClose();
131 | assert(closeCounter == 2);
132 | });
133 | }
134 | }
135 |
136 | @TestSuite()
137 | export class VirtualClientCopyToPaired {
138 | @Test()
139 | public successful_call() {
140 | let client;
141 | assert.doesNotThrow(() => {
142 | let client1, client2;
143 | [client1, client2 ] = CreatePair();
144 |
145 | let copiedBuffer: Buffer;
146 | //@ts-ignore
147 | client2.socketData = function(buffer) {
148 | if(Buffer.isBuffer(buffer)) {
149 | copiedBuffer = buffer;
150 | }
151 | }
152 |
153 | //@ts-ignore
154 | client1.socketSend(Buffer.from("fromclient1"));
155 | //@ts-ignore
156 | client1.copyToPaired();
157 | assert(copiedBuffer!.toString() == "fromclient1");
158 | });
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/VirtualClient.ts:
--------------------------------------------------------------------------------
1 | import {Client} from "./Client";
2 |
3 | /**
4 | * A mock client socket which can be used for simulating regular socket communications.
5 | * This allows to have the same interface when working with sockets but when not needing
6 | * the actual socket because both parties are in the same process.
7 | */
8 | export class VirtualClient extends Client
9 | {
10 | protected pairedSocket?: VirtualClient;
11 | protected latency: number;
12 | protected outQueue: (Buffer | string)[];
13 | protected remoteAddress: string | undefined;
14 | protected localAddress: string | undefined;
15 | protected remotePort: number | undefined;
16 | protected localPort: number | undefined;
17 |
18 | /**
19 | * @constructor
20 | * @param {VirtualClient} [pairedSocket] When creating the second socket of a socket-pair provide the first socket as argument to get them paired.
21 | */
22 | constructor(pairedSocket?: VirtualClient) {
23 | super({port: 0});
24 |
25 | this.pairedSocket = pairedSocket;
26 |
27 | /** We can set this to simulate some latency in the paired socket communication */
28 | this.latency = 0; // Milliseconds
29 |
30 | /**
31 | * Queue of outgoing messages.
32 | * We need this if we use simulated latency,
33 | * because the ordering of setTimeout might not be guaranteed
34 | * for identical timeout values.
35 | */
36 | this.outQueue = [];
37 |
38 | /* Complete the pairing by assigning this socket as our paired socket's paired socket */
39 | if (this.pairedSocket) {
40 | this.pairedSocket.pairedSocket = this;
41 | }
42 | }
43 |
44 | /**
45 | * Pair this socket with given socket and emit connected events on this socket and then on given socket.
46 | * @param pairedSocket the client to pair this client with.
47 | */
48 | public pair(pairedSocket: VirtualClient) {
49 | if (this.pairedSocket) {
50 | throw new Error("Socket can only be paired once.");
51 | }
52 | this.pairedSocket = pairedSocket;
53 | this.pairedSocket.pairedSocket = this;
54 | this.socketConnected();
55 | this.pairedSocket.pairedSocket.socketConnected();
56 | }
57 |
58 | /**
59 | * Set a simulated latency of the socket communications.
60 | *
61 | * @param {number} latency in milliseconds for each send
62 | */
63 | public setLatency(latency: number) {
64 | if (latency < this.latency && this.outQueue.length > 0) {
65 | throw new Error("Cannot decrease latency while data is still waiting to send.");
66 | }
67 | this.latency = latency;
68 | }
69 |
70 | public setLocalAddress(localAddress: string | undefined) {
71 | this.localAddress = localAddress;
72 | }
73 |
74 | public setRemoteAddress(remoteAddress: string | undefined) {
75 | this.remoteAddress = remoteAddress;
76 | }
77 |
78 | public setRemotePort(remotePort: number | undefined) {
79 | this.remotePort = remotePort;
80 | }
81 |
82 | public setLocalPort(localPort: number | undefined) {
83 | this.localPort = localPort;
84 | }
85 |
86 | public getLocalAddress(): string | undefined {
87 | return this.localAddress;
88 | }
89 |
90 | public getRemoteAddress(): string | undefined {
91 | return this.remoteAddress;
92 | }
93 |
94 | public getRemotePort(): number | undefined {
95 | return this.remotePort;
96 | }
97 |
98 | public getLocalPort(): number | undefined {
99 | return this.localPort;
100 | }
101 |
102 | /**
103 | * Hook events on the socket.
104 | */
105 | protected socketHook() {
106 | // Do nothing
107 | // We handle events in different ways since this is not an actual socket.
108 | }
109 |
110 | /**
111 | * Send the given data on socket.
112 | * @param {Buffer | string} data
113 | */
114 | protected socketSend(data: Buffer | string) {
115 | // Put msg into paired socket.
116 | if (this.pairedSocket) {
117 | this.outQueue.push(data);
118 | if (this.latency > 0) {
119 | setTimeout( () => this.copyToPaired(), this.latency);
120 | } else {
121 | this.copyToPaired();
122 | }
123 | }
124 | }
125 |
126 | /**
127 | * Specify the paired close procedure.
128 | */
129 | protected socketClose() {
130 | const hadError = false;
131 | if (this.pairedSocket) {
132 | this.pairedSocket.socketClosed(hadError);
133 | }
134 | this.socketClosed(hadError);
135 | }
136 |
137 | /**
138 | * Internal function to copy one message in the out queue to the paired socket.
139 | *
140 | */
141 | protected copyToPaired() {
142 | if (this.pairedSocket) {
143 | const data = this.outQueue.shift();
144 | if (data !== undefined) {
145 | this.pairedSocket.socketData(data);
146 | }
147 | }
148 | }
149 | }
150 |
151 | /**
152 | * Create two paired virtual clients.
153 | * @returns tuple of two clients.
154 | */
155 | export function CreatePair(): [VirtualClient, VirtualClient] {
156 | const socket1 = new VirtualClient();
157 | const socket2 = new VirtualClient(socket1);
158 | return [socket1, socket2];
159 | }
160 |
--------------------------------------------------------------------------------
/src/Server.ts:
--------------------------------------------------------------------------------
1 | import {ClientInterface} from "./types";
2 | import {
3 | ServerOptions,
4 | SocketErrorCallback,
5 | SocketCloseCallback,
6 | SocketAcceptCallback,
7 | } from "./types";
8 |
9 | /**
10 | * Boilerplate for creating and wrapping a server socket listener (TCP or Websocket) under a common interface.
11 | *
12 | * Socket specific functions need to be overridden/implemented.
13 | *
14 | */
15 | export abstract class Server
16 | {
17 | protected serverOptions: ServerOptions;
18 | protected eventHandlers: {[key: string]: ((data: any) => void)[]};
19 | protected _isClosed: boolean;
20 | protected clients: ClientInterface[];
21 |
22 | constructor(serverOptions: ServerOptions) {
23 | this.serverOptions = serverOptions;
24 | this.eventHandlers = {};
25 | this.clients = [];
26 | this._isClosed = false;
27 | }
28 |
29 | /**
30 | * Listens for connections and yields connected client sockets.
31 | *
32 | */
33 | public listen() {
34 | if (this.serverOptions.port === 0) {
35 | throw new Error("Server socket not allowed to listen to port 0.");
36 | }
37 |
38 | this.serverListen();
39 | }
40 |
41 | /**
42 | * Close listener and optionally (default) also all accepted socket clients.
43 | * @param closeClients if set to true (default) then also close all accepted sockets,
44 | * if set to false then leave accepted client sockets open.
45 | */
46 | public close(closeClients: boolean = true) {
47 | if (this._isClosed) {
48 | return;
49 | }
50 | this.serverClose();
51 | if (closeClients) {
52 | this.clients.forEach( client => client.close() );
53 | this.clients = [];
54 | }
55 | }
56 |
57 | /**
58 | * Event handler triggered when client has connected.
59 | *
60 | * A Client object is passed as argument to fn() of the instance type this.SocketClass.
61 | *
62 | * @param {Function} fn callback
63 | */
64 | public onConnection(fn: SocketAcceptCallback) {
65 | this.on("connection", fn);
66 | }
67 |
68 | /**
69 | * Event handler triggered when a server error occurs.
70 | *
71 | * An error string is passed as argument to fn().
72 | *
73 | * @param {Function} fn callback
74 | */
75 | public onError(fn: SocketErrorCallback) {
76 | this.on("error", fn);
77 | }
78 |
79 | /**
80 | * Event handler triggered when server has closed together with all its client sockets.
81 | *
82 | * @param {Function} fn callback
83 | */
84 | public onClose(fn: SocketCloseCallback) {
85 | this.on("close", fn);
86 | }
87 |
88 | public isClosed(): boolean {
89 | return this._isClosed;
90 | }
91 |
92 | /**
93 | * Create the server socket.
94 | */
95 | protected serverCreate() {
96 | throw new Error("Not implemented.");
97 | }
98 |
99 | /**
100 | * Initiate the server listener.
101 | */
102 | protected serverListen() {
103 | throw new Error("Not implemented.");
104 | }
105 |
106 | /**
107 | * Close the server.
108 | * Override as necessary.
109 | */
110 | protected serverClose() {
111 | throw new Error("Not implemented.");
112 | }
113 |
114 | /**
115 | * Internal error event implementation.
116 | *
117 | * @param {Error} err
118 | */
119 | protected serverError = (message: string) => {
120 | const errorEvent: Parameters = [message];
121 |
122 | this.triggerEvent("error", ...errorEvent);
123 | }
124 |
125 | /**
126 | * Internal close event implementation.
127 | */
128 | protected serverClosed = () => {
129 | this._isClosed = true;
130 | this.triggerEvent("close");
131 | }
132 |
133 | /**
134 | * Performs all operations involved in registering a new client connection.
135 | *
136 | * @param {ClientInterface} client
137 | */
138 | protected addClient(client: ClientInterface) {
139 | this.clients.push(client);
140 | client.onClose( () => { this.removeClient(client) } );
141 |
142 | const socketAcceptedEvent: Parameters = [client];
143 |
144 | this.triggerEvent("connection", ...socketAcceptedEvent);
145 | }
146 |
147 | /**
148 | * Performs all operations involved in removing an existing client registration.
149 | *
150 | * @param {ClientInterface} client
151 | */
152 | protected removeClient(client: ClientInterface) {
153 | const index = this.clients.indexOf(client);
154 | if (index > -1) {
155 | this.clients.splice(index, 1)
156 | }
157 | }
158 |
159 | /**
160 | * Internal event implementation.
161 | *
162 | * @param {string} event
163 | * @param {Function} fn
164 | */
165 | public on(event: string, fn: (data: any) => void) {
166 | const fns = this.eventHandlers[event] || [];
167 | this.eventHandlers[event] = fns;
168 | fns.push(fn);
169 | }
170 |
171 | /**
172 | * Internal event trigger implementation.
173 | *
174 | * @param {string} event
175 | * @param {any} data
176 | */
177 | protected triggerEvent(event: string, data?: any) {
178 | const fns = this.eventHandlers[event] || [];
179 | fns.forEach( fn => {
180 | fn(data);
181 | });
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/test/connection.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestSuite, Test } from "testyts";
2 | import {WSClient} from "../index";
3 | import {WSServer, ClientInterface} from "../index";
4 |
5 | const assert = require("assert");
6 |
7 | @TestSuite()
8 | export class Connection {
9 |
10 | @Test()
11 | public async clientserver() {
12 | await new Promise(resolve => {
13 | const serverOptions = {
14 | host: "localhost",
15 | port: 8181
16 | };
17 | const server = new WSServer(serverOptions);
18 | server.listen();
19 |
20 | server.onConnection( (client: ClientInterface) => {
21 | client.onData( (data: Buffer | string) => {
22 | assert(data.toString() == "hello");
23 | client.send("received!");
24 | });
25 | client.onClose( () => {
26 | server.close();
27 | resolve();
28 | });
29 | });
30 |
31 | const clientOptions = {
32 | host: "localhost",
33 | port: 8181
34 | };
35 | const client = new WSClient(clientOptions);
36 | client.connect();
37 |
38 | client.onConnect( () => {
39 | client.onData( (data: Buffer | string) => {
40 | assert(data.toString() == "received!");
41 | client.close();
42 | });
43 | client.onClose( () => {
44 | });
45 | client.send("hello");
46 | });
47 | });
48 | }
49 |
50 | @Test()
51 | public async clientserver_ipv4() {
52 | await new Promise(resolve => {
53 | const serverOptions = {
54 | host: "127.0.0.1",
55 | port: 8181
56 | };
57 | const server = new WSServer(serverOptions);
58 | server.listen();
59 |
60 | server.onConnection( (client: ClientInterface) => {
61 | client.onData( (data: Buffer | string) => {
62 | assert(data.toString() == "hello");
63 | client.send("received!");
64 | });
65 | client.onClose( () => {
66 | server.close();
67 | resolve();
68 | });
69 | });
70 |
71 | const clientOptions = {
72 | host: "127.0.0.1",
73 | port: 8181
74 | };
75 | const client = new WSClient(clientOptions);
76 | client.connect();
77 |
78 | client.onConnect( () => {
79 | client.onData( (data: Buffer | string) => {
80 | assert(data.toString() == "received!");
81 | client.close();
82 | });
83 | client.onClose( () => {
84 | });
85 | client.send("hello");
86 | });
87 | });
88 | }
89 |
90 | @Test()
91 | public async clientserver_ipv6_short() {
92 | await new Promise(resolve => {
93 | const serverOptions = {
94 | host: "::1",
95 | port: 8181
96 | };
97 | const server = new WSServer(serverOptions);
98 | server.listen();
99 |
100 | server.onConnection( (client: ClientInterface) => {
101 | client.onData( (data: Buffer | string) => {
102 | assert(data.toString() == "hello");
103 | client.send("received!");
104 | });
105 | client.onClose( () => {
106 | server.close();
107 | resolve();
108 | });
109 | });
110 |
111 | const clientOptions = {
112 | host: "::1",
113 | port: 8181
114 | };
115 | const client = new WSClient(clientOptions);
116 | client.connect();
117 |
118 | client.onConnect( () => {
119 | client.onData( (data: Buffer | string) => {
120 | assert(data.toString() == "received!");
121 | client.close();
122 | });
123 | client.onClose( () => {
124 | });
125 | client.send("hello");
126 | });
127 | });
128 | }
129 |
130 | @Test()
131 | public async clientserver_ipv6_long() {
132 | await new Promise(resolve => {
133 | const serverOptions = {
134 | host: "0:0:0:0:0:0:0:1",
135 | port: 8181
136 | };
137 | const server = new WSServer(serverOptions);
138 | server.listen();
139 |
140 | server.onConnection( (client: ClientInterface) => {
141 | client.onData( (data: Buffer | string) => {
142 | assert(data.toString() == "hello");
143 | client.send("received!");
144 | });
145 | client.onClose( () => {
146 | server.close();
147 | resolve();
148 | });
149 | });
150 |
151 | const clientOptions = {
152 | host: "0:0:0:0:0:0:0:1",
153 | port: 8181
154 | };
155 | const client = new WSClient(clientOptions);
156 | client.connect();
157 |
158 | client.onConnect( () => {
159 | client.onData( (data: Buffer | string) => {
160 | assert(data.toString() == "received!");
161 | client.close();
162 | });
163 | client.onClose( () => {
164 | });
165 | client.send("hello");
166 | });
167 | });
168 | }
169 |
170 | @Test()
171 | public async clientserver_missing_host() {
172 | await new Promise(resolve => {
173 | const serverOptions = {
174 | port: 8182
175 | };
176 | const server = new WSServer(serverOptions);
177 | server.listen();
178 |
179 | server.onConnection( (client: ClientInterface) => {
180 | client.onData( (data: Buffer | string) => {
181 | assert(data.toString() == "hello");
182 | server.close();
183 | resolve();
184 | });
185 | });
186 |
187 | const clientOptions = {
188 | port: 8182
189 | };
190 | const client = new WSClient(clientOptions);
191 | client.connect();
192 |
193 | client.onConnect( () => {
194 | client.onData( (data: Buffer | string) => {
195 | client.close();
196 | });
197 | client.send("hello");
198 | });
199 | });
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/test/connectionTCP.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestSuite, Test } from "testyts";
2 | import {TCPClient} from "../index";
3 | import {TCPServer, ClientInterface} from "../index";
4 |
5 | const assert = require("assert");
6 |
7 | @TestSuite()
8 | export class ConnectionTCP {
9 |
10 | @Test()
11 | public async clientserver() {
12 | await new Promise(resolve => {
13 | const serverOptions = {
14 | host: "localhost",
15 | port: 8181
16 | };
17 | const server = new TCPServer(serverOptions);
18 | server.listen();
19 |
20 | server.onConnection( (client: ClientInterface) => {
21 | client.onData( (data: Buffer | string) => {
22 | assert(data.toString() == "hello");
23 | client.send("received!");
24 | });
25 | client.onClose( () => {
26 | server.close();
27 | resolve();
28 | });
29 | });
30 |
31 | const clientOptions = {
32 | host: "localhost",
33 | port: 8181
34 | };
35 | const client = new TCPClient(clientOptions);
36 | client.connect();
37 |
38 | client.onConnect( () => {
39 | client.onData( (data: Buffer | string) => {
40 | assert(data.toString() == "received!");
41 | client.close();
42 | });
43 | client.onClose( () => {
44 | });
45 | client.send("hello");
46 | });
47 | });
48 | }
49 |
50 | @Test()
51 | public async clientserver_ipv4() {
52 | await new Promise(resolve => {
53 | const serverOptions = {
54 | host: "127.0.0.1",
55 | port: 8181
56 | };
57 | const server = new TCPServer(serverOptions);
58 | server.listen();
59 |
60 | server.onConnection( (client: ClientInterface) => {
61 | client.onData( (data: Buffer | string) => {
62 | assert(data.toString() == "hello");
63 | client.send("received!");
64 | });
65 | client.onClose( () => {
66 | server.close();
67 | resolve();
68 | });
69 | });
70 |
71 | const clientOptions = {
72 | host: "127.0.0.1",
73 | port: 8181
74 | };
75 | const client = new TCPClient(clientOptions);
76 | client.connect();
77 |
78 | client.onConnect( () => {
79 | client.onData( (data: Buffer | string) => {
80 | assert(data.toString() == "received!");
81 | client.close();
82 | });
83 | client.onClose( () => {
84 | });
85 | client.send("hello");
86 | });
87 | });
88 | }
89 |
90 | @Test()
91 | public async clientserver_ipv6_short() {
92 | await new Promise(resolve => {
93 | const serverOptions = {
94 | host: "::1",
95 | port: 8181
96 | };
97 | const server = new TCPServer(serverOptions);
98 | server.listen();
99 |
100 | server.onConnection( (client: ClientInterface) => {
101 | client.onData( (data: Buffer | string) => {
102 | assert(data.toString() == "hello");
103 | client.send("received!");
104 | });
105 | client.onClose( () => {
106 | server.close();
107 | resolve();
108 | });
109 | });
110 |
111 | const clientOptions = {
112 | host: "::1",
113 | port: 8181
114 | };
115 | const client = new TCPClient(clientOptions);
116 | client.connect();
117 |
118 | client.onConnect( () => {
119 | client.onData( (data: Buffer | string) => {
120 | assert(data.toString() == "received!");
121 | client.close();
122 | });
123 | client.onClose( () => {
124 | });
125 | client.send("hello");
126 | });
127 | });
128 | }
129 |
130 | @Test()
131 | public async clientserver_ipv6_long() {
132 | await new Promise(resolve => {
133 | const serverOptions = {
134 | host: "0:0:0:0:0:0:0:1",
135 | port: 8181
136 | };
137 | const server = new TCPServer(serverOptions);
138 | server.listen();
139 |
140 | server.onConnection( (client: ClientInterface) => {
141 | client.onData( (data: Buffer | string) => {
142 | assert(data.toString() == "hello");
143 | client.send("received!");
144 | });
145 | client.onClose( () => {
146 | server.close();
147 | resolve();
148 | });
149 | });
150 |
151 | const clientOptions = {
152 | host: "0:0:0:0:0:0:0:1",
153 | port: 8181
154 | };
155 | const client = new TCPClient(clientOptions);
156 | client.connect();
157 |
158 | client.onConnect( () => {
159 | client.onData( (data: Buffer | string) => {
160 | assert(data.toString() == "received!");
161 | client.close();
162 | });
163 | client.onClose( () => {
164 | });
165 | client.send("hello");
166 | });
167 | });
168 | }
169 |
170 | @Test()
171 | public async clientserver_missing_host() {
172 | await new Promise(resolve => {
173 | const serverOptions = {
174 | port: 8182
175 | };
176 | const server = new TCPServer(serverOptions);
177 | server.listen();
178 |
179 | server.onConnection( (client: ClientInterface) => {
180 | client.onData( (data: Buffer | string) => {
181 | assert(data.toString() == "hello");
182 | server.close();
183 | resolve();
184 | });
185 | });
186 |
187 | const clientOptions = {
188 | port: 8182
189 | };
190 | const client = new TCPClient(clientOptions);
191 | client.connect();
192 |
193 | client.onConnect( () => {
194 | client.onData( (data: Buffer | string) => {
195 | client.close();
196 | });
197 | client.send("hello");
198 | });
199 | });
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/test/TCPServer.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestSuite, Test } from "testyts";
2 | import {TCPClient, TCPServer} from "../index";
3 |
4 | const assert = require("assert");
5 | const net = require("net");
6 | const tls = require("tls");
7 |
8 | @TestSuite()
9 | export class TCPServerConstructor {
10 | @Test()
11 | public successful_call_with_cert() {
12 | assert.doesNotThrow(() => {
13 | tls.createServer = function() {
14 | return {
15 | "on": function(name: string, fn: Function) {
16 | assert(name == "secureConnection" || name == "error" || name == "close" || name == "tlsClientError");
17 | assert(typeof fn == "function");
18 | }
19 | }
20 | }
21 | const server = new TCPServer({
22 | "host": "host.com",
23 | "port": 99,
24 | "rejectUnauthorized": undefined,
25 | "cert": "valid-certificate"
26 | });
27 | //@ts-ignore: protected data
28 | assert(server.server);
29 | });
30 | }
31 |
32 | @Test()
33 | public successful_call_without_TLS() {
34 | assert.doesNotThrow(() => {
35 | net.createServer = function() {
36 | return {
37 | "on": function(name: string, fn: Function) {
38 | assert(name == "connection" || name == "error" || name == "close" || name == "tlsClientError");
39 | assert(typeof fn == "function");
40 | }
41 | };
42 | };
43 | const server = new TCPServer({
44 | "host": "host.com",
45 | "port": 99,
46 | });
47 | //@ts-ignore: protected data
48 | assert(server.server);
49 | });
50 | }
51 |
52 | @Test()
53 | public calls_serverCreate() {
54 | assert.doesNotThrow(() => {
55 | let flag = false;
56 | class TestServer extends TCPServer {
57 | serverCreate() {
58 | flag = true;
59 | }
60 | }
61 | const server = new TestServer({
62 | "host": "host.com",
63 | "port": 99,
64 | });
65 | //@ts-ignore: state is expected to be changed by custom serverCreate
66 | assert(flag == true);
67 | });
68 | }
69 |
70 | }
71 |
72 | @TestSuite()
73 | export class TCPServerCreate {
74 | @Test()
75 | public successful_call_with_USE_TLS() {
76 | assert.doesNotThrow(() => {
77 | tls.createServer = function() {
78 | return {
79 | "on": function(name: string, fn: Function) {
80 | assert(name == "secureConnection" || name == "error" || name == "close" || name == "tlsClientError");
81 | assert(typeof fn == "function");
82 | }
83 | }
84 | };
85 | const server = new TCPServer({
86 | "host": "host.com",
87 | "port": 99,
88 | "rejectUnauthorized": undefined,
89 | "cert": "valid-certificate"
90 | });
91 | //@ts-ignore: protected data
92 | assert(server.server);
93 | });
94 | }
95 |
96 | @Test()
97 | public successful_call_without_USE_TLS() {
98 | assert.doesNotThrow(() => {
99 | net.createServer = function() {
100 | return {
101 | "on": function(name: string, fn: Function) {
102 | assert(name == "connection" || name == "error" || name == "close" || name == "tlsClientError");
103 | assert(typeof fn == "function");
104 | }
105 | };
106 | };
107 | const server = new TCPServer({
108 | "host": "host.com",
109 | "port": 99,
110 | "rejectUnauthorized": undefined
111 | });
112 | //@ts-ignore: protected data
113 | assert(server.server);
114 | });
115 | }
116 | }
117 |
118 | @TestSuite()
119 | export class TCPServerListen {
120 | @Test()
121 | public overwritten_server_after_object_creation() {
122 | assert.doesNotThrow(() => {
123 | tls.createServer = function() {
124 | return {
125 | "on": function(name: string, fn: Function) {
126 | assert(name == "secureConnection" || name == "error" || name == "close" || name == "tlsClientError");
127 | assert(typeof fn == "function");
128 | }
129 | };
130 | };
131 | const server = new TCPServer({
132 | "host": "host.com",
133 | "port": 99,
134 | "rejectUnauthorized": undefined,
135 | "cert": "valid-certificate"
136 | });
137 | //@ts-ignore: protected data
138 | server.server = undefined;
139 | //@ts-ignore: protected method
140 | server.serverListen();
141 | });
142 | }
143 |
144 | @Test()
145 | public successful_call() {
146 | assert.doesNotThrow(() => {
147 | tls.createServer = function() {
148 | return {
149 | "on": function(name: string, fn: Function) {
150 | assert(name == "secureConnection" || name == "error" || name == "close" || name == "tlsClientError");
151 | assert(typeof fn == "function");
152 | },
153 | "listen": function(options: any) {
154 | assert(options.host == "host.com");
155 | assert(options.port == 99);
156 | assert(!options.ipv6Only);
157 | }
158 |
159 | };
160 | };
161 | const server = new TCPServer({
162 | "host": "host.com",
163 | "port": 99,
164 | "rejectUnauthorized": undefined,
165 | "cert": "valid-certificate"
166 | });
167 | //@ts-ignore: protected method
168 | server.serverListen();
169 | });
170 | }
171 | }
172 |
173 | @TestSuite()
174 | export class TCPServerClose {
175 | @Test()
176 | public successful_call() {
177 | assert.doesNotThrow(() => {
178 | const server = new TCPServer({
179 | "host": "host.com",
180 | "port": 99,
181 | "rejectUnauthorized": undefined,
182 | "cert": "valid-certificate"
183 | });
184 | let flag = false;
185 | //@ts-ignore: custom signature
186 | server.server!.close = function() {
187 | flag = true;
188 | }
189 | assert(flag == false);
190 | //@ts-ignore: protected method
191 | server.serverClose();
192 | //@ts-ignore: expected to be mutated by custom close procedure
193 | assert(flag == true);
194 | });
195 | }
196 | }
197 |
198 | @TestSuite()
199 | export class TCPServerClientConnected {
200 | @Test()
201 | public successful_call() {
202 | assert.doesNotThrow(() => {
203 | const server = new TCPServer({
204 | "host": "host.com",
205 | "port": 99,
206 | "rejectUnauthorized": undefined,
207 | "cert": "valid-certificate"
208 | });
209 | let flag = false;
210 | //@ts-ignore: custom signature
211 | server.addClient = function(client) {
212 | assert(client);
213 | flag = true;
214 | }
215 | assert(flag == false);
216 | //@ts-ignore: protected method
217 | server.clientConnected();
218 | //@ts-ignore: expected to be mutated by custom procedure
219 | assert(flag == true);
220 | });
221 | }
222 | }
223 |
224 | @TestSuite()
225 | export class TCPServerError {
226 | @Test()
227 | public successful_call() {
228 | assert.doesNotThrow(() => {
229 | const server = new TCPServer({
230 | "host": "host.com",
231 | "port": 99,
232 | "rejectUnauthorized": undefined,
233 | "cert": "valid-certificate"
234 | });
235 | let flag = false;
236 | //@ts-ignore: custom signature
237 | server.serverError = function(buffer) {
238 | assert(buffer.toString() == "Error Message Here");
239 | flag = true;
240 | }
241 | assert(flag == false);
242 | //@ts-ignore: protected method
243 | server.error(new Error("Error Message Here"));
244 | //@ts-ignore: expected to be mutated by custom procedure
245 | assert(flag == true);
246 | });
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/src/WSClient.ts:
--------------------------------------------------------------------------------
1 | // Add browser/browserify/parcel dependency
2 | // @ts-ignore
3 | if(typeof "process" === "undefined" && !process && !process.versions && !process.versions.node) {
4 | require("regenerator-runtime/runtime");
5 | }
6 |
7 | import {Client} from "./Client";
8 | import {ClientOptions} from "./types";
9 | import * as ws from "ws";
10 |
11 | let isBrowser: boolean;
12 | let WebSocketClass: any = null;
13 | //@ts-ignore
14 | if (typeof WebSocket !== "undefined") {
15 | isBrowser = true;
16 | //@ts-ignore
17 | WebSocketClass = WebSocket;
18 | //@ts-ignore
19 | } else if (typeof MozWebSocket !== "undefined") {
20 | isBrowser = true;
21 | //@ts-ignore
22 | WebSocketClass = MozWebSocket;
23 | //@ts-ignore
24 | } else if (typeof window !== "undefined") {
25 | isBrowser = true;
26 | //@ts-ignore
27 | WebSocketClass = window.WebSocket || window.MozWebSocket;
28 | } else {
29 | isBrowser = false;
30 | // @ts-ignore
31 | WebSocketClass = ws.WebSocket;
32 | }
33 |
34 | /**
35 | * WebSocket client implementation.
36 | */
37 | export class WSClient extends Client
38 | {
39 | protected socket?: ws;
40 |
41 | constructor(clientOptions?: ClientOptions, socket?: ws) {
42 | super(clientOptions);
43 | this.socket = socket;
44 |
45 | if (this.socket) {
46 | if (isBrowser) {
47 | this.socket.binaryType = "arraybuffer";
48 | }
49 |
50 | this.socketHook();
51 | }
52 | }
53 |
54 | public getSocket(): ws {
55 | if (!this.socket) {
56 | throw new Error("Socket not initiated");
57 | }
58 |
59 | return this.socket;
60 | }
61 |
62 | public isWebSocket(): boolean {
63 | return true;
64 | }
65 |
66 | /**
67 | * Note this does always return undefined in browser.
68 | * @return {string | undefined} local IP address
69 | */
70 | public getLocalAddress(): string | undefined {
71 | //@ts-ignore
72 | if (this.socket?._socket && typeof this.socket._socket.localAddress === "string") {
73 | //@ts-ignore
74 | return this.socket._socket.localAddress;
75 | }
76 | }
77 |
78 | /**
79 | * Note this does always return undefined in browser.
80 | * @return {string | undefined} remote IP address
81 | */
82 | public getRemoteAddress(): string | undefined {
83 | //@ts-ignore
84 | if (this.socket?._socket && typeof this.socket._socket.remoteAddress === "string") {
85 | //@ts-ignore
86 | return this.socket._socket.remoteAddress;
87 | }
88 | }
89 |
90 | /**
91 | * Note this does always return undefined in browser.
92 | * @return {number | undefined} remote port
93 | */
94 | public getRemotePort(): number | undefined {
95 | //@ts-ignore
96 | if (this.socket?._socket && typeof this.socket._socket.remotePort === "number") {
97 | //@ts-ignore
98 | return this.socket._socket.remotePort;
99 | }
100 | }
101 |
102 | /**
103 | * Note this does always return undefined in browser.
104 | * @return {number | undefined} local port
105 | */
106 | public getLocalPort(): number | undefined {
107 | //@ts-ignore
108 | if (this.socket?._socket && typeof this.socket._socket.localPort === "number") {
109 | //@ts-ignore
110 | return this.socket._socket.localPort;
111 | }
112 | }
113 |
114 | /**
115 | * Specifies how the socket gets initialized and created, then establishes a connection.
116 | */
117 | protected socketConnect() {
118 | if (this.socket) {
119 | throw new Error("Socket already created");
120 | }
121 |
122 | if (!this.clientOptions) {
123 | throw new Error("clientOptions is required to create socket");
124 | }
125 |
126 | let host = this.clientOptions.host ? this.clientOptions.host : "localhost";
127 |
128 | //
129 | // The following browser-friendly Node.js net module isIPv6 procedure was inspired by the net-browserify package.
130 | //
131 | // References:
132 | // - https://www.npmjs.com/package/net-browserify
133 | // - https://github.com/emersion/net-browserify
134 | const isIPv6 = function(input: string) {
135 | return /^(([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))$/.test(input);
136 | };
137 | // Ensure IPv6 host gets surrounded with brackets for later address formation
138 | if(isIPv6(host)) {
139 | host = `[${host}]`;
140 | }
141 |
142 | const USE_TLS = this.clientOptions.secure ? true: false;
143 |
144 | let address;
145 | if(USE_TLS) {
146 | address = `wss://${host}:${this.clientOptions.port}`;
147 | }
148 | else {
149 | address = `ws://${host}:${this.clientOptions.port}`;
150 | }
151 |
152 | if(isBrowser) {
153 | this.socket = new WebSocketClass(address);
154 | // Make sure binary type is set to ArrayBuffer instead of Blob
155 | if (this.socket) {
156 | this.socket.binaryType = "arraybuffer";
157 | }
158 | } else {
159 | this.socket = new WebSocketClass(address, {
160 | cert: this.clientOptions.cert,
161 | key: this.clientOptions.key,
162 | rejectUnauthorized: this.clientOptions.rejectUnauthorized,
163 | ca: this.clientOptions.ca,
164 | perMessageDeflate: false,
165 | maxPayload: 100 * 1024 * 1024,
166 | });
167 | }
168 |
169 | if (this.socket) {
170 | this.socket.onopen = this.socketConnected;
171 | }
172 | else {
173 | throw new Error("Could not create socket");
174 | }
175 | }
176 |
177 | /**
178 | * Specifies hooks to be called as part of the connect procedure.
179 | */
180 | protected socketHook() {
181 | if (!this.socket) {
182 | return;
183 | }
184 |
185 | this.socket.onmessage = (msg: any) => {
186 | let data = msg.data;
187 |
188 | // Under Browser settings, convert message data from ArrayBuffer to Buffer.
189 | if (isBrowser) {
190 | if (typeof(data) !== "string") {
191 | const bytes = new Uint8Array(data);
192 | data = Buffer.from(bytes);
193 | }
194 | }
195 |
196 | this.socketData(data);
197 | };
198 | this.socket.onerror = this.error; // Error connecting
199 | this.socket.onclose = (closeEvent) => this.socketClosed(closeEvent && closeEvent.code === 1000); // Socket closed
200 | }
201 |
202 | protected unhookError() {
203 | if (this.socket) {
204 | this.socket.onerror = null;
205 | }
206 | }
207 |
208 | /**
209 | * Defines how data gets written to the socket.
210 | * @param {data} buffer or string - data to be sent
211 | * strings are sent as UTF-8 text, Buffers as binary.
212 | * @throws if socket not instantiated.
213 | */
214 | protected socketSend(data: Buffer | string) {
215 | if (!this.socket) {
216 | throw new Error("Socket not instantiated");
217 | }
218 |
219 | if (this.isTextMode()) {
220 | if (typeof(data) !== "string") {
221 | data = data.toString();
222 | }
223 |
224 | this.socket.send(data, {binary: false, compress: false});
225 | }
226 | else {
227 | if (!Buffer.isBuffer(data)) {
228 | data = Buffer.from(data);
229 | }
230 |
231 | this.socket.send(data, {binary: true, compress: false});
232 | }
233 | }
234 |
235 | /**
236 | * Defines the steps to be performed during close.
237 | */
238 | protected socketClose() {
239 | if (this.socket) {
240 | this.socket.close();
241 | }
242 | }
243 |
244 | protected error = (error: ws.ErrorEvent) => {
245 | this.socketError(error.message || error.error || "WebSocket could not connect");
246 | };
247 | }
248 |
--------------------------------------------------------------------------------
/test/WSClient.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestSuite, Test } from "testyts";
2 | import {WSClient} from "../index";
3 |
4 | const assert = require("assert");
5 | const net = require("net");
6 | const tls = require("tls");
7 | const ws = require("ws");
8 |
9 | @TestSuite()
10 | export class WSClientSocketConstructor {
11 | @Test()
12 | public successful_call_setting_USE_TLS() {
13 | assert.doesNotThrow(() => {
14 | const client = new WSClient({
15 | "host": "host.com",
16 | "port": 99,
17 | "secure": true,
18 | });
19 | //@ts-ignore: protected data
20 | assert(client.clientOptions!.host == "host.com");
21 | //@ts-ignore: protected data
22 | assert(client.clientOptions!.port == 99);
23 | //@ts-ignore: protected data
24 | assert(client.clientOptions!.secure == true);
25 |
26 | //@ts-ignore: protected data
27 | assert(client.socket == null);
28 | });
29 | }
30 |
31 | @Test()
32 | public successful_call_without_TLS() {
33 | assert.doesNotThrow(() => {
34 | assert.doesNotThrow(() => {
35 | const client = new WSClient({
36 | "host": "host.com",
37 | "port": 99,
38 | "secure": false,
39 | });
40 | //@ts-ignore: protected data
41 | assert(client.clientOptions!.host == "host.com");
42 | //@ts-ignore: protected data
43 | assert(client.clientOptions!.port == 99);
44 | //@ts-ignore: protected data
45 | assert(client.clientOptions!.secure == false);
46 |
47 | //@ts-ignore: protected data
48 | assert(client.socket == null);
49 | });
50 | });
51 | }
52 |
53 | @Test()
54 | public successful_call_with_existing_socket() {
55 | assert.doesNotThrow(() => {
56 | let flag = false;
57 | class TestClient extends WSClient {
58 | socketHook() {
59 | flag = true;
60 | }
61 | }
62 | assert(flag == false);
63 | //@ts-ignore incomplete socket definition
64 | let socket: ws.WebSocket = {};
65 | //@ts-ignore incomplete socket definition
66 | const client = new TestClient({
67 | "host": "host.com",
68 | "port": 99,
69 | "secure": false,
70 | }, /*@ts-ignore*/ socket);
71 | assert(flag == true);
72 | });
73 | }
74 | }
75 |
76 | @TestSuite()
77 | export class WSClientSocketHook {
78 | @Test()
79 | public successful_call() {
80 | assert.doesNotThrow(() => {
81 | const client = new WSClient({
82 | "host": "host.com",
83 | "port": 99,
84 | "secure": false,
85 | });
86 | //@ts-ignore: dummy
87 | client.socketConnect = function() {
88 | //@ts-ignore: incomplete implementation
89 | client.socket = {};
90 | };
91 |
92 | //@ts-ignore: validate flow
93 | client.socketConnect();
94 | //@ts-ignore: protected data
95 | assert(!client.socket!.onmessage);
96 | //@ts-ignore: protected data
97 | assert(!client.socket!.onerror);
98 | //@ts-ignore: protected data
99 | assert(!client.socket!.onclose);
100 | //@ts-ignore: protected method
101 | client.socketHook();
102 | //@ts-ignore: protected data
103 | assert(client.socket!.onmessage);
104 | //@ts-ignore: protected data
105 | assert(client.socket!.onerror);
106 | //@ts-ignore: protected data
107 | assert(client.socket!.onclose);
108 | });
109 | }
110 |
111 | @Test()
112 | public missing_socket_noop() {
113 | assert.doesNotThrow(() => {
114 | const client = new WSClient({
115 | "host": "host.com",
116 | "port": 99,
117 | "secure": false,
118 | });
119 | //@ts-ignore: protected data
120 | client.socket = undefined;
121 | //@ts-ignore: protected method
122 | client.socketHook();
123 | });
124 | }
125 | }
126 |
127 | @TestSuite()
128 | export class WSClientSocketConnect {
129 | @Test()
130 | public socket_already_created() {
131 | assert.throws(() => {
132 | //@ts-ignore incomplete socket definition
133 | let socket: ws.WebSocket = {};
134 | //@ts-ignore incomplete socket definition
135 | const client = new WSClient({
136 | "host": "host.com",
137 | "port": 99,
138 | "secure": false,
139 | }, /*@ts-ignore*/ socket);
140 | assert(client.socketConnect());
141 | }, /Socket already created/);
142 | }
143 |
144 | @Test()
145 | public undefined_options() {
146 | assert.throws(() => {
147 | const client = new WSClient({
148 | "host": "host.com",
149 | "port": 99,
150 | "secure": false,
151 | });
152 | //@ts-ignore: protected data
153 | client.clientOptions = undefined;
154 | //@ts-ignore: protected method
155 | assert(client.socketConnect());
156 | }, /clientOptions is required to create socket/);
157 | }
158 | }
159 |
160 | @TestSuite()
161 | export class WSClientSocketSend {
162 | @Test()
163 | public successful_call_setting_USE_TLS() {
164 | assert.doesNotThrow(() => {
165 | const client = new WSClient({
166 | "host": "host.com",
167 | "port": 99,
168 | "secure": false,
169 | });
170 | //@ts-ignore: incomplete implementation
171 | client.socket = {
172 | "send": function(buffer: Buffer, options: any) {
173 | assert(buffer.toString() == "testdata123");
174 | assert(Buffer.isBuffer(buffer));
175 | assert(options.binary == true);
176 | assert(options.compress == false);
177 | }
178 | };
179 | //@ts-ignore: protected method
180 | client.socketSend(Buffer.from("testdata123"));
181 | });
182 | }
183 |
184 | @Test()
185 | public missing_socket_noop() {
186 | assert.doesNotThrow(() => {
187 | const client = new WSClient({
188 | "host": "host.com",
189 | "port": 99,
190 | "secure": false,
191 | });
192 | //@ts-ignore: protected data
193 | client.socket = undefined;
194 | //@ts-ignore: protected method
195 | assert.throws( () => client.socketSend(Buffer.from("testdata123")),
196 | /Socket not instantiated/ );
197 | });
198 | }
199 |
200 | }
201 |
202 | @TestSuite()
203 | export class WSClientSocketClose {
204 | @Test()
205 | public successful_call() {
206 | assert.doesNotThrow(() => {
207 | const client = new WSClient({
208 | "host": "host.com",
209 | "port": 99,
210 | "secure": false,
211 | });
212 | //@ts-ignore: protected method
213 | let hasClosed = false;
214 | //@ts-ignore: incomplete implementation
215 | client.socket = {
216 | "close": function() {
217 | hasClosed = true;
218 | }
219 | };
220 | assert(hasClosed == false);
221 | //@ts-ignore: protected method
222 | client.socketClose();
223 | //@ts-ignore: data expected to be changed by previously called inner functions
224 | assert(hasClosed == true);
225 | });
226 | }
227 |
228 | @Test()
229 | public missing_socket_noop() {
230 | assert.doesNotThrow(() => {
231 | const client = new WSClient({
232 | "host": "host.com",
233 | "port": 99,
234 | "secure": false,
235 | });
236 | //@ts-ignore: protected data
237 | client.socket = undefined;
238 | //@ts-ignore: protected method
239 | client.socketClose();
240 | });
241 | }
242 | }
243 |
244 | @TestSuite()
245 | export class WSClientError {
246 | @Test()
247 | public successful_call() {
248 | assert.doesNotThrow(() => {
249 | const client = new WSClient({
250 | "host": "host.com",
251 | "port": 99,
252 | "secure": false,
253 | });
254 | let flag = false;
255 | //@ts-ignore: protected method
256 | client.socketError = function(msg: string) {
257 | assert(msg == "test error");
258 | flag = true;
259 | };
260 | assert(flag == false);
261 | //@ts-ignore: protected method
262 | client.error(new Error("test error"));
263 | //@ts-ignore: flag changes inside custom callback
264 | assert(flag == true);
265 | });
266 | }
267 |
268 | @Test()
269 | public onerror_missing_error_message_succeeds() {
270 | assert.doesNotThrow(() => {
271 | const client = new WSClient({
272 | "host": "host.com",
273 | "port": 99,
274 | "secure": false,
275 | });
276 | let flag = false;
277 | //@ts-ignore: protected method
278 | client.socketError = function(msg: string) {
279 | assert(msg == "WebSocket could not connect");
280 | flag = true;
281 | };
282 | //@ts-ignore: ignore missing input check
283 | client.error(new Error());
284 | //@ts-ignore: flag changes inside custom callback
285 | assert(flag == true);
286 | });
287 | }
288 |
289 | public onerror_undefined_error_succeeds() {
290 | assert.doesNotThrow(() => {
291 | const client = new WSClient({
292 | "host": "host.com",
293 | "port": 99,
294 | "secure": false,
295 | });
296 | let flag = false;
297 | //@ts-ignore: protected method
298 | client.socketError = function(msg: string) {
299 | flag = true;
300 | };
301 | //@ts-ignore: ignore missing input check
302 | client.error();
303 | //@ts-ignore: flag changes inside custom callback
304 | assert(flag == true);
305 | });
306 | }
307 | }
308 |
--------------------------------------------------------------------------------
/test/WSServer.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestSuite, Test } from "testyts";
2 | import {WSServer, WSClient} from "../index";
3 |
4 | const assert = require("assert");
5 | const http = require("http");
6 | const https = require("https");
7 | const WebSocket = require("ws");
8 |
9 | @TestSuite()
10 | export class WSServerConstructor {
11 | @Test()
12 | public calls_serverCreate() {
13 | assert.doesNotThrow(() => {
14 | https.createServer = function() {
15 | return {
16 | "on": function(name: string, fn: Function) {
17 | assert(name == "secureConnection" || name == "error" || name == "close" || name == "tlsClientError");
18 | assert(typeof fn == "function");
19 | }
20 | };
21 | };
22 | let flag = false;
23 | class TestServer extends WSServer {
24 | serverCreate() {
25 | flag = true;
26 | }
27 | }
28 | assert(flag == false);
29 | const server = new TestServer({
30 | "host": "host.com",
31 | "port": 99,
32 | "cert": "valid-certificate"
33 | });
34 | //@ts-ignore: modified by custom serverCreate
35 | assert(flag == true);
36 | });
37 | }
38 |
39 |
40 | @Test()
41 | public successful_call_with_cert() {
42 | assert.doesNotThrow(() => {
43 | https.createServer = function() {
44 | return {
45 | "on": function(name: string, fn: Function) {
46 | assert(name == "secureConnection" || name == "error" || name == "close" || name == "tlsClientError");
47 | assert(typeof fn == "function");
48 | }
49 | };
50 | };
51 | const server = new WSServer({
52 | "host": "host.com",
53 | "port": 99,
54 | "cert": "valid-certificate"
55 | });
56 | //@ts-ignore: protected data
57 | assert(server.wsServer == null);
58 | });
59 | }
60 |
61 | @Test()
62 | public successful_call_without_TLS() {
63 | assert.doesNotThrow(() => {
64 | http.createServer = function() {
65 | return {
66 | "on": function(name: string, fn: Function) {
67 | assert(name == "connection" || name == "error" || name == "close" || name == "tlsClientError");
68 | assert(typeof fn == "function");
69 | }
70 | };
71 | };
72 | const server = new WSServer({
73 | "host": "host.com",
74 | "port": 99,
75 | });
76 | //@ts-ignore: protected data
77 | assert(server.wsServer == null);
78 | });
79 | }
80 | }
81 |
82 | @TestSuite()
83 | export class WSServerCreate {
84 | @Test()
85 | public successful_call_with_USE_TLS() {
86 | https.createServer = function() {
87 | return {
88 | "on": function(name: string, fn: Function) {
89 | assert(name == "secureConnection" || name == "error" || name == "close" || name == "tlsClientError");
90 | assert(typeof fn == "function");
91 | }
92 | };
93 | };
94 | const server = new WSServer({
95 | "host": "host.com",
96 | "port": 99,
97 | "cert": "valid-certificate"
98 | });
99 | //@ts-ignore: protected data
100 | assert(server.server);
101 | }
102 |
103 | @Test()
104 | public successful_call_without_USE_TLS() {
105 | assert.doesNotThrow(() => {
106 | https.createServer = function() {
107 | return {
108 | "on": function(name: string, fn: Function) {
109 | assert(name == "connection" || name == "error" || name == "close" || name == "tlsClientError");
110 | assert(typeof fn == "function");
111 | }
112 | };
113 | };
114 | const server = new WSServer({
115 | "host": "host.com",
116 | "port": 99,
117 | });
118 | //@ts-ignore: protected data
119 | assert(server.server);
120 | });
121 | }
122 |
123 | @Test()
124 | public undefined_return_from_createServer() {
125 | assert.doesNotThrow(() => {
126 | http.createServer = function() {
127 | return undefined;
128 | };
129 | const server = new WSServer({
130 | "host": "host.com",
131 | "port": 99,
132 | });
133 | //@ts-ignore: protected data
134 | assert(!server.server);
135 | });
136 | }
137 | }
138 |
139 | @TestSuite()
140 | export class WSServerListen {
141 | @Test()
142 | public overwritten_server_after_object_creation() {
143 | assert.throws(() => {
144 | https.createServer = function() {
145 | return {
146 | "on": function(name: string, fn: Function) {
147 | assert(name == "secureConnection" || name == "error" || name == "close" || name == "tlsClientError");
148 | assert(typeof fn == "function");
149 | }
150 | };
151 | }
152 | const server = new WSServer({
153 | "host": "host.com",
154 | "port": 99,
155 | "cert": "valid-certificate"
156 | });
157 | //@ts-ignore: protected data
158 | server.server = undefined;
159 | //@ts-ignore: protected method
160 | server.serverListen();
161 | });
162 | }
163 |
164 | @Test()
165 | public successful_call() {
166 | assert.doesNotThrow(() => {
167 | https.createServer = function() {
168 | return {
169 | "on": function(name: string, fn: Function) {
170 | assert(name == "connection" || name == "error" || name == "close" || name == "tlsClientError" || name == "listening" || name == "upgrade");
171 | assert(typeof fn == "function");
172 | },
173 | "listen": function(options: any) {
174 | assert(options.host == "host.com");
175 | assert(options.port == 99);
176 | assert(!options.ipv6Only);
177 | }
178 | };
179 | };
180 |
181 | WebSocket.Server = function(data: any) {
182 | assert(data!.path == "/");
183 | assert(data!.clientTracking == true);
184 | assert(data!.perMessageDeflate == false);
185 | assert(data!.maxPayload == 100*1024*1024);
186 | return {
187 | "on": function(name: string, fn: Function) {
188 | assert(name == "connection" || name == "error" || name == "close");
189 | assert(typeof fn == "function");
190 | }
191 | };
192 | };
193 |
194 | const server = new WSServer({
195 | "host": "host.com",
196 | "port": 99,
197 | "cert": "valid-certificate"
198 | });
199 | //@ts-ignore: protected data
200 | assert(!server.wsServer);
201 | //@ts-ignore: protected method
202 | server.serverListen();
203 | //@ts-ignore: protected data
204 | assert(server.wsServer);
205 | });
206 | }
207 | }
208 |
209 | @TestSuite()
210 | export class WSServerClose {
211 | @Test()
212 | public successful_call() {
213 | assert.doesNotThrow(() => {
214 | https.createServer = function() {
215 | return {
216 | "on": function(name: string, fn: Function) {
217 | },
218 | "listen": function(options: any) {
219 | }
220 | };
221 | };
222 | WebSocket.Server = function() {
223 | return {
224 | "on": function(name: string, fn: Function) {
225 | }
226 | };
227 | };
228 | const server = new WSServer({
229 | "host": "host.com",
230 | "port": 99,
231 | "cert": "valid-certificate"
232 | });
233 | //@ts-ignore: protected method
234 | server.serverListen();
235 |
236 | let callCount = 0;
237 | //@ts-ignore: different signature is not relevant
238 | server.server!.close = function() {
239 | callCount++;
240 | }
241 | //@ts-ignore: different signature is not relevant
242 | server.wsServer!.close = function() {
243 | callCount++;
244 | }
245 | assert(callCount == 0);
246 | //@ts-ignore: protected method
247 | server.serverClose();
248 | assert(callCount == 2);
249 | });
250 | }
251 | }
252 |
253 | @TestSuite()
254 | export class WSServerClientConnected {
255 | @Test()
256 | public pass_socket_to_clientConnected() {
257 | assert.doesNotThrow(() => {
258 | https.createServer = function() {
259 | return {
260 | "on": function(name: string, fn: Function) {
261 | assert(name == "connection" || name == "error" || name == "close" || name == "tlsClientError");
262 | assert(typeof fn == "function");
263 | }
264 | };
265 | };
266 | const server = new WSServer({
267 | "host": "host.com",
268 | "port": 99,
269 | });
270 | //@ts-ignore: protected data
271 | assert(server.clients.length == 0);
272 | //@ts-ignore: protected method
273 | server.clientConnected(server);
274 | //@ts-ignore: protected data
275 | assert(server.clients.length == 1);
276 | });
277 | }
278 | }
279 | @TestSuite()
280 | export class WSServerError {
281 | @Test()
282 | public successful_call() {
283 | assert.doesNotThrow(() => {
284 | https.createServer = function() {
285 | return {
286 | "on": function(name: string, fn: Function) {
287 | assert(name == "connection" || name == "error" || name == "close" || name == "tlsClientError");
288 | assert(typeof fn == "function");
289 | }
290 | };
291 | };
292 | const server = new WSServer({
293 | "host": "host.com",
294 | "port": 99,
295 | });
296 | let flag = false;
297 | //@ts-ignore: protected method
298 | server.serverError = function(buffer: Buffer) {
299 | assert(buffer.toString() == "test error");
300 | flag = true;
301 | };
302 | assert(flag == false);
303 | //@ts-ignore: protected method
304 | server.error(new Error("test error"));
305 | //@ts-ignore: flag changes inside custom callback
306 | assert(flag == true);
307 | });
308 | }
309 | }
310 |
--------------------------------------------------------------------------------
/src/Client.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ClientOptions,
3 | SocketErrorCallback,
4 | SocketDataCallback,
5 | SocketConnectCallback,
6 | SocketCloseCallback,
7 | ClientInterface,
8 | } from "./types";
9 |
10 | /**
11 | * Class for wrapping a socket (TCP, Websocket or Virtual) under a common interface.
12 | *
13 | * Socket specific functions need to be overridden/implemented in dervived classes.
14 | */
15 | export abstract class Client implements ClientInterface
16 | {
17 | protected clientOptions?: ClientOptions;
18 | protected eventHandlers: {[key: string]: ((data: any) => void)[]} = {
19 | data: [],
20 | error: [],
21 | connect: [],
22 | close: [],
23 | };
24 | protected _isClosed: boolean = false;
25 | protected bufferedData: Array<(Buffer | string)> = [];
26 | protected unreadData: Array<(Buffer | string)> = [];
27 |
28 | constructor(clientOptions?: ClientOptions) {
29 | this.clientOptions = clientOptions;
30 | }
31 |
32 | /**
33 | * This should be called on (wrapped) clients before using.
34 | * For regular TCP and WebSocket clients this function does nothing.
35 | */
36 | public async init() {
37 | // Do nothing, but allows wrapping sockets to do something.
38 | }
39 |
40 | public getSocket(): any {
41 | throw new Error("Function not implemented.");
42 | }
43 |
44 | public isWebSocket(): boolean {
45 | return false;
46 | }
47 |
48 | /**
49 | * @returns true if set to text mode, false if binary mode (default).
50 | */
51 | public isTextMode(): boolean {
52 | return this.clientOptions?.textMode ?? false;
53 | }
54 |
55 | /**
56 | * Connect to server.
57 | *
58 | */
59 | public connect() {
60 | this.socketConnect();
61 | this.socketHook();
62 | }
63 |
64 | /**
65 | * Send buffer on socket.
66 | *
67 | * @param {data} data to be sent
68 | * For TCP sockets strings are always converted to Buffers before sending.
69 | *
70 | * For WebSockets in binary mode strings are converted to Buffers before sending.
71 | *
72 | * For WebSockets in binary mode Buffers are sent as they are in binary mode on the WebSocket.
73 | *
74 | * For WebSockets in text mode strings are sent as they are in text mode on the WebSocket.
75 | *
76 | * For WebSockets in text mode Buffers are converted into strings and sent in text mode
77 | * on the WebSocket.
78 | *
79 | * @throws An error will be thrown when buffer data type is incompatible.
80 | */
81 | public send(data: Buffer | string) {
82 | if (this._isClosed) {
83 | return;
84 | }
85 |
86 | this.socketSend(data);
87 | }
88 |
89 | /**
90 | * Close socket.
91 | */
92 | public close() {
93 | if (this._isClosed) {
94 | return;
95 | }
96 |
97 | this.socketClose();
98 | }
99 |
100 | public isClosed(): boolean {
101 | return this._isClosed;
102 | }
103 |
104 | /**
105 | * User hook for socket connection error.
106 | *
107 | * @param {Function} fn - on error callback. Function is passed a string with the error message.
108 | *
109 | */
110 | public onError(fn: SocketErrorCallback) {
111 | this.on("error", fn);
112 | }
113 |
114 | /**
115 | * Unhook handler for socket errors.
116 | *
117 | * @param {Function} fn - remove existing error callback
118 | *
119 | */
120 | public offError(fn: SocketErrorCallback) {
121 | this.off("error", fn);
122 | }
123 |
124 | /**
125 | * User hook for incoming data.
126 | *
127 | * For sockets in binary mode data in the callback is always Buffer.
128 | * For sockets in text mode data in the callback is always string.
129 | *
130 | * @param {Function} fn - on data callback. Function is passed a Buffer object.
131 | */
132 | public onData(fn: SocketDataCallback) {
133 | this.on("data", fn);
134 | }
135 |
136 | /**
137 | * Unhook handler for incoming data.
138 | *
139 | * @param {Function} fn - remove data callback.
140 | *
141 | */
142 | public offData(fn: SocketDataCallback) {
143 | this.off("data", fn);
144 | }
145 |
146 | /**
147 | * User hook for connection event.
148 | *
149 | * @param {Function} fn - on connect callback.
150 | *
151 | */
152 | public onConnect(fn: SocketConnectCallback) {
153 | this.on("connect", fn);
154 | }
155 |
156 | /**
157 | * Unhook handler for connection event.
158 | *
159 | * @param {Function} fn - remove connect callback.
160 | *
161 | */
162 | public offConnect(fn: SocketConnectCallback) {
163 | this.off("connect", fn);
164 | }
165 |
166 | /**
167 | * User hook for close event.
168 | *
169 | * @param {Function} fn - on close callback.
170 | *
171 | */
172 | public onClose(fn: SocketCloseCallback) {
173 | this.on("close", fn);
174 | }
175 |
176 | /**
177 | * Unhook handler for close event.
178 | *
179 | * @param {Function} fn - remove close callback.
180 | *
181 | */
182 | public offClose(fn: SocketCloseCallback) {
183 | this.off("close", fn);
184 | }
185 |
186 | public getLocalAddress(): string | undefined {
187 | // Override in implementation if applicable
188 | return undefined;
189 | }
190 |
191 | public getRemoteAddress(): string | undefined {
192 | // Override in implementation if applicable
193 | return undefined;
194 | }
195 |
196 | public getRemotePort(): number | undefined {
197 | // Override in implementation if applicable
198 | return undefined;
199 | }
200 |
201 | public getLocalPort(): number | undefined {
202 | // Override in implementation if applicable
203 | return undefined;
204 | }
205 |
206 | /**
207 | * Unread data by putting it back into the event queue.
208 | * This will not trigger an onData event but the data will be
209 | * buffered until the next onData event is triggered.
210 | * @param {Buffer} data
211 | */
212 | public unRead(data: Buffer | string) {
213 | if (this.isTextMode()) {
214 | if (typeof(data) !== "string") {
215 | throw new Error("unRead expecting string in text mode");
216 | }
217 | }
218 | else {
219 | if (!Buffer.isBuffer(data)) {
220 | throw new Error("unRead expecting Buffer in binary mode");
221 | }
222 | }
223 |
224 | if (data.length === 0) {
225 | return;
226 | }
227 |
228 | this.unreadData.push(data);
229 | }
230 |
231 | /**
232 | * Create the socket object and initiate a connection.
233 | * This only done for initiating client sockets.
234 | * A server listener socket client is already connected and must be passed in the constructor.
235 | */
236 | protected socketConnect() {
237 | throw new Error("Function not implemented.");
238 | }
239 |
240 | /**
241 | * Hook events on the socket.
242 | */
243 | protected socketHook() {
244 | throw new Error("Function not implemented.");
245 | }
246 |
247 | /**
248 | * Send the given buffer on socket.
249 | * Socket specific implementation.
250 | */
251 | protected socketSend(data: Buffer | string) { // eslint-disable-line @typescript-eslint/no-unused-vars
252 | throw new Error("Function not implemented.");
253 | }
254 |
255 | /**
256 | * Socket-specific close procedure.
257 | */
258 | protected socketClose() {
259 | throw new Error("Function not implemented.");
260 | }
261 |
262 | /**
263 | * Base close event procedure responsible for triggering the close event.
264 | */
265 | protected socketClosed = (hadError: boolean) => {
266 | this._isClosed = true;
267 |
268 | const closeEvent: Parameters = [hadError];
269 |
270 | this.triggerEvent("close", ...closeEvent);
271 | }
272 |
273 | /**
274 | * Base data event procedure responsible for triggering the data event.
275 | *
276 | * @param {Buffer} data - data buffer.
277 | *
278 | */
279 | protected socketData = (data: Buffer | string) => {
280 | if (this.isTextMode()) {
281 | if (Buffer.isBuffer(data)) {
282 | data = data.toString();
283 | }
284 | }
285 | else {
286 | if (typeof(data) === "string") {
287 | data = Buffer.from(data);
288 | }
289 | }
290 |
291 | const fns = this.eventHandlers["data"];
292 |
293 | if (fns.length > 0) {
294 | let data2: Buffer | string | undefined = undefined;
295 | if (this.unreadData.length > 0) {
296 | if (Buffer.isBuffer(data)) {
297 | data2 = Buffer.concat((this.unreadData as Buffer[]));
298 | }
299 | else {
300 | data2 = (this.unreadData as string[]).join("");
301 | }
302 | this.unreadData.length = 0;
303 | }
304 |
305 | this.bufferedData.push(data);
306 |
307 | if (Buffer.isBuffer(data)) {
308 | data2 = Buffer.concat([(data2 ?? Buffer.alloc(0)) as Buffer, ...(this.bufferedData as Buffer[])]);
309 | }
310 | else {
311 | data2 = (data2 ?? "") + (this.bufferedData as string[]).join("");
312 | }
313 | this.bufferedData.length = 0;
314 |
315 | fns.forEach( (fn) => {
316 | fn(data2);
317 | });
318 | }
319 | else {
320 | const bufferIncomingData = this.clientOptions?.bufferData === undefined ? true :
321 | this.clientOptions.bufferData;
322 |
323 | if (bufferIncomingData) {
324 | this.bufferedData.push(data);
325 | }
326 | }
327 | }
328 |
329 | /**
330 | * Base connect event procedure responsible for triggering the connect event.
331 | */
332 | protected socketConnected = () => {
333 | this.unhookError();
334 |
335 | this.triggerEvent("connect");
336 | }
337 |
338 | /**
339 | * The error handler should be unhooked after connection to not confuse
340 | * abrupt close with connect error.
341 | */
342 | protected unhookError() {
343 | throw new Error("Function not implemented");
344 | }
345 |
346 | /**
347 | * Base error event procedure responsible for triggering the error event.
348 | *
349 | * @param {Buffer} data - error message.
350 | *
351 | */
352 | protected socketError = (message: string) => {
353 | const errorEvent: Parameters = [message];
354 |
355 | this.triggerEvent("error", ...errorEvent);
356 | }
357 |
358 | /**
359 | * Base "off" event procedure responsible for removing a callback from the list of event handlers.
360 | *
361 | * @param {string} event - event name.
362 | * @param {Function} fn - callback.
363 | *
364 | */
365 | protected off(event: string, fn: (data: any) => void) {
366 | const fns = this.eventHandlers[event];
367 | const index = fns?.indexOf(fn);
368 | if (index !== undefined && index > -1) {
369 | fns.splice(index, 1);
370 | }
371 | }
372 |
373 | /**
374 | * Base "on" event procedure responsible for adding a callback to the list of event handlers.
375 | *
376 | * @param {string} event - event name.
377 | * @param {Function} fn - callback.
378 | *
379 | */
380 | protected on(event: string, fn: (data: any) => void) {
381 | this.eventHandlers[event]?.push(fn);
382 |
383 | if (event === "data") {
384 | if (this.bufferedData.length > 0 || this.unreadData.length > 0) {
385 | this.socketData(Buffer.alloc(0));
386 | }
387 | }
388 | }
389 |
390 | /**
391 | * Trigger event calls the appropriate handler based on the event name.
392 | *
393 | * @param {string} event - event name.
394 | * @param {any} data - event data passed on to event handler.
395 | */
396 | protected triggerEvent(event: string, data?: any) {
397 | this.eventHandlers[event]?.forEach( (fn) => {
398 | fn(data);
399 | });
400 | }
401 | }
402 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export const SOCKET_WEBSOCKET = "WebSocket";
2 | export const SOCKET_TCP = "TCP";
3 |
4 | /**
5 | * Event emitted on client socket connect error.
6 | * @param message potential error message
7 | */
8 | export type SocketErrorCallback = (message: string) => void;
9 |
10 | /**
11 | * Event emitted on incoming binary or text data on client socket.
12 | * @param data incoming data as Buffer or text (depending on socket configuration)
13 | */
14 | export type SocketDataCallback = (data: Buffer | string) => void;
15 |
16 | /**
17 | * Event emitted on client socket connect.
18 | */
19 | export type SocketConnectCallback = () => void;
20 |
21 | /**
22 | * Event emitted on client socket close.
23 | * @param hadError set to true if the socket was closed due to an error.
24 | */
25 | export type SocketCloseCallback = (hadError: boolean) => void;
26 |
27 | /**
28 | * Event emitted on server socket accepted.
29 | * @param client the newly accepted and created client socket
30 | */
31 | export type SocketAcceptCallback = (client: ClientInterface) => void;
32 |
33 | export const EVENT_SOCKETFACTORY_ERROR = "ERROR";
34 | export const EVENT_SOCKETFACTORY_CLOSE = "CLOSE";
35 | export const EVENT_SOCKETFACTORY_CONNECT = "CONNECT";
36 | export const EVENT_SOCKETFACTORY_CLIENT_INIT_ERROR = "CLIENT_INIT_ERROR";
37 | export const EVENT_SOCKETFACTORY_CLIENT_CONNECT_ERROR = "CLIENT_CONNECT_ERROR";
38 | export const EVENT_SOCKETFACTORY_CLIENT_IP_REFUSE = "CLIENT_IP_REFUSE";
39 | export const EVENT_SOCKETFACTORY_SERVER_INIT_ERROR = "SERVER_INIT_ERROR";
40 | export const EVENT_SOCKETFACTORY_SERVER_LISTEN_ERROR = "SERVER_LISTEN_ERROR";
41 |
42 | export const DETAIL_SOCKETFACTORY_CLIENT_REFUSE_ERROR_IP_DENIED = "IP_DENIED";
43 | export const DETAIL_SOCKETFACTORY_CLIENT_REFUSE_ERROR_IP_NOT_ALLOWED = "IP_NOT_ALLOWED";
44 | export const DETAIL_SOCKETFACTORY_CLIENT_REFUSE_ERROR_IP_OVERFLOW = "IP_OVERFLOW";
45 |
46 | /** Event emitted when client socket cannot be initaited, likely due to misconfiguration. */
47 | export type SocketFactoryClientInitErrorCallback = (error: Error) => void;
48 |
49 | /** Event emitted when client socket cannot connect to server. */
50 | export type SocketFactoryClientConnectErrorCallback = (error: Error) => void;
51 |
52 | /** Event emitted when client socket connected. */
53 | export type SocketFactoryConnectCallback = (client: ClientInterface, isServer: boolean) => void;
54 |
55 | /** Event emitted when either client or server accepted socket is closed. */
56 | export type SocketFactoryCloseCallback = (client: ClientInterface, isServer: boolean, hadError: boolean) => void;
57 |
58 | /** Event emitted when server socket cannot be created. */
59 | export type SocketFactoryServerInitErrorCallback = (error: Error) => void;
60 |
61 | /** Event emitted when server socket cannot bind and listen, could be that port is taken. */
62 | export type SocketFactoryServerListenErrorCallback = (error: Error) => void;
63 |
64 | export type SocketFactoryClientIPRefuseDetail =
65 | typeof DETAIL_SOCKETFACTORY_CLIENT_REFUSE_ERROR_IP_DENIED |
66 | typeof DETAIL_SOCKETFACTORY_CLIENT_REFUSE_ERROR_IP_NOT_ALLOWED |
67 | typeof DETAIL_SOCKETFACTORY_CLIENT_REFUSE_ERROR_IP_OVERFLOW;
68 |
69 | /**
70 | * Event emitted when server actively refused the client's IP address for a specific reason.
71 | * @param detail the reason why the IP address got refused to connect
72 | * @param ipAddress the textual IP address getting refused
73 | */
74 | export type SocketFactoryClientIPRefuseCallback = (detail: SocketFactoryClientIPRefuseDetail, ipAddress: string) => void;
75 |
76 | export type SocketFactoryErrorCallbackNames =
77 | typeof EVENT_SOCKETFACTORY_CLIENT_INIT_ERROR |
78 | typeof EVENT_SOCKETFACTORY_CLIENT_CONNECT_ERROR |
79 | typeof EVENT_SOCKETFACTORY_SERVER_INIT_ERROR |
80 | typeof EVENT_SOCKETFACTORY_SERVER_LISTEN_ERROR;
81 |
82 | /**
83 | * The error event is always emitted in addition to every specific error event in the SocketFactory.
84 | * It works as a catch-all error event handler.
85 | *
86 | * @param name is the name of the specific error event which was emitted
87 | * @param error is the original event error argument
88 | */
89 | export type SocketFactoryErrorCallback =
90 | (name: SocketFactoryErrorCallbackNames, error: Error) => void;
91 |
92 | export interface ClientInterface {
93 | init(): Promise;
94 | connect(): void;
95 | send(data: Buffer | string): void;
96 | close(): void;
97 | isClosed(): boolean;
98 | getSocket(): any;
99 | isWebSocket(): boolean;
100 | isTextMode(): boolean;
101 | onError(fn: SocketErrorCallback): void;
102 | offError(fn: SocketErrorCallback): void;
103 | onData(fn: SocketDataCallback): void;
104 | offData(fn: SocketDataCallback): void;
105 | onConnect(fn: SocketConnectCallback): void;
106 | offConnect(fn: SocketConnectCallback): void;
107 | onClose(fn: SocketCloseCallback): void;
108 | offClose(fn: SocketCloseCallback): void;
109 | getLocalAddress(): string | undefined;
110 | getRemoteAddress(): string | undefined;
111 | getRemotePort(): number | undefined;
112 | getLocalPort(): number | undefined;
113 | unRead(data: Buffer | string): void;
114 | }
115 |
116 | export interface SocketFactoryInterface {
117 | init(): void;
118 | getSocketFactoryConfig(): SocketFactoryConfig;
119 | close(): void;
120 | shutdown(): void;
121 | isClosed(): boolean;
122 | isShutdown(): boolean;
123 | getStats(): SocketFactoryStats;
124 |
125 | /** Catch-all error handler for SocketFactory. */
126 | onSocketFactoryError(callback: SocketFactoryErrorCallback): void;
127 |
128 | onServerInitError(callback: SocketFactoryServerInitErrorCallback): void;
129 | onServerListenError(callback: SocketFactoryServerListenErrorCallback): void;
130 | onClientInitError(callback: SocketFactoryClientInitErrorCallback): void;
131 | onConnectError(callback: SocketFactoryClientConnectErrorCallback): void;
132 | onConnect(callback: SocketFactoryConnectCallback): void;
133 | onClose(callback: SocketFactoryCloseCallback): void;
134 |
135 | /** Event called when connecting peer has been refused. */
136 | onClientIPRefuse(callback: SocketFactoryClientIPRefuseCallback): void;
137 | }
138 |
139 | export interface WrappedClientInterface extends ClientInterface {
140 | getClient(): ClientInterface;
141 | }
142 |
143 | export type ClientOptions = {
144 | /**
145 | * RFC6066 states that this should not be an IP address but a name when using TLS.
146 | * Default is "localhost".
147 | */
148 | host?: string,
149 |
150 | /**
151 | * TCP port number to connect to.
152 | */
153 | port: number,
154 |
155 | /**
156 | * Set to true (default) to buffer incoming data until an onData is hooked,
157 | * then the buffered data will be fed to that onData handler.
158 | * This can be useful when needing to pass the socket to some other part
159 | * of the code. The passing code does a socket.offData(...) and incoming
160 | * data gets buffered until the new owner does socket.onData(...).
161 | */
162 | bufferData?: boolean,
163 |
164 | /**
165 | * Set to true to have the socket in text mode (default is false).
166 | *
167 | * If set and data is received on TCP socket that data is translated into text and emitted
168 | * as string on onData() handler.
169 | *
170 | * If set and binary data is received on WebSocket that data translated into text and emitted
171 | * as string on onData() handler.
172 | *
173 | * If set and textual data is received on WebSocket that data is not transformed but emitted
174 | * as it is as string on onData() handler.
175 | *
176 | *
177 | * When not in text mode, data received on TCP socket is emitted as Buffer on the
178 | * onData() handler.
179 | *
180 | * When not in text mode and data received on WebSocket is binary, the data is emitted as Buffer
181 | * on the onData() handler.
182 | *
183 | * When not in text mode and data received on WebSocket is text, the data is transformed into
184 | * Buffer and emitted on the onData() handler.
185 | */
186 | textMode?: boolean,
187 |
188 | /**
189 | * Set to true to connect over TLS.
190 | */
191 | secure?: boolean,
192 |
193 | /**
194 | * If true (default) the client will reject any server certificate which is
195 | * not approved by the trusted or the supplied list of CAs in the ca property.
196 | */
197 | rejectUnauthorized?: boolean,
198 |
199 | /**
200 | * The client certificate needed if the server requires it.
201 | * Cert chains in PEM format.
202 | * Required if server is requiring it.
203 | */
204 | cert?: string | string[] | Buffer | Buffer[],
205 |
206 | /**
207 | * Client private key in PEM format.
208 | * Required if cert is set.
209 | * Note that the type string[] is not supported.
210 | */
211 | key?: string | Buffer | Buffer[],
212 |
213 | /**
214 | * Optionally override the trusted CAs, useful for trusting
215 | * server self signed certificates.
216 | */
217 | ca?: string | string[] | Buffer | Buffer[],
218 | };
219 |
220 | export type ServerOptions = {
221 | /**
222 | * Host to bind to.
223 | * RFC6066 states that this should not be an IP address but a name when using TLS.
224 | */
225 | host?: string,
226 |
227 | /**
228 | * TCP port number to listen to.
229 | * Listening to port 0 is not allowed.
230 | * Required.
231 | */
232 | port: number,
233 |
234 | /**
235 | * Set to true (default) to buffer incoming data until an onData is hooked,
236 | * then the buffered data will be fed to that onData handler.
237 | * This can be useful when needing to pass the socket to some other part
238 | * of the code. The passing code does a socket.offData(...) and incoming
239 | * data gets buffered until the new owner does socket.onData(...).
240 | * This parameter applies to the client sockets accepted on new connections.
241 | */
242 | bufferData?: boolean,
243 |
244 | /**
245 | * If set then set it for all accepted sockets.
246 | * See ClientOptions.textMode for details.
247 | */
248 | textMode?: boolean,
249 |
250 | /**
251 | * Set to true to only listen to IPv6 addresses.
252 | */
253 | ipv6Only?: boolean,
254 |
255 | /*
256 | * If set to true the server will request a client certificate and attempt
257 | * to verify that certificate (see also rejectUnauthorized).
258 | * Default is false.
259 | */
260 | requestCert?: boolean,
261 |
262 | /**
263 | * If true (default) the server will reject any client certificate which is
264 | * not approved by the trusted or the supplied list of CAs in the ca property.
265 | * This option only has effect if requestCert is set to true.
266 | */
267 | rejectUnauthorized?: boolean,
268 |
269 | /**
270 | * The server certificate.
271 | * Cert chains in PEM format, one per server private key provided.
272 | * Required if wanting to use TLS.
273 | */
274 | cert?: string | string[] | Buffer | Buffer[],
275 |
276 | /**
277 | * Server private keys in PEM format.
278 | * Required if cert is set.
279 | * Note that the type string[] is not supported.
280 | */
281 | key?: string | Buffer | Buffer[],
282 |
283 | /**
284 | * Optionally override the trusted CAs, useful for trusting
285 | * client self signed certificates.
286 | */
287 | ca?: string | string[] | Buffer | Buffer[],
288 | };
289 |
290 | export type SocketFactoryStats = {
291 | counters: {[key: string]: {counter: number}},
292 | };
293 |
294 | export type SocketFactoryConfig = {
295 | /**
296 | * This must be set if factory is to connect as a client.
297 | * Both client and server can bet set together.
298 | */
299 | client?: {
300 | socketType: typeof SOCKET_WEBSOCKET | typeof SOCKET_TCP,
301 |
302 | clientOptions: ClientOptions,
303 |
304 | /** If set greater than 0 wait as many seconds to reconnect a closed client. */
305 | reconnectDelay?: number,
306 | },
307 |
308 | /**
309 | * This must be set if factory is to connect as a server.
310 | * Both client and server can bet set together.
311 | */
312 | server?: {
313 | socketType: typeof SOCKET_WEBSOCKET | typeof SOCKET_TCP,
314 |
315 | serverOptions: ServerOptions,
316 |
317 | /** If client IP is in the array then the connection will be disallowed and disconnected. Lowercase strings are required. */
318 | deniedIPs: string[],
319 |
320 | /** If set then the client IP must be within the array to be allowed to connect. Lowercase strings are required. */
321 | allowedIPs?: string[],
322 | },
323 |
324 | /** Total allowed number of socket connections for the factory. */
325 | maxConnections?: number,
326 |
327 | /**
328 | * Max connections per remote IP for the factory.
329 | * Note that when sharing this state between server and client sockets
330 | * it is important that the client `host` field is the remote IP address,
331 | * the same IP address as read from the socket when server is accepting it.
332 | * If the client is using a hostname as `host` then the shared counting will not be correct,
333 | * since hostname and clientIP will not match.
334 | */
335 | maxConnectionsPerIp?: number,
336 | };
337 |
--------------------------------------------------------------------------------
/test/connectionTLS.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestSuite, Test } from "testyts";
2 | import {WSClient} from "../index";
3 | import {WSServer, ClientInterface} from "../index";
4 |
5 | const assert = require("assert");
6 | const fs = require("fs");
7 |
8 | @TestSuite()
9 | export class ConnectionTLS {
10 |
11 | @Test()
12 | public async clientserver_server_reject_unauthorized() {
13 | await new Promise(resolve => {
14 | const serverOptions = {
15 | host: "localhost",
16 | port: 8181,
17 | requestCert: true,
18 | rejectUnauthorized: true,
19 | cert: fs.readFileSync("./test/cert/localhost.cert"),
20 | key: fs.readFileSync("./test/cert/localhost.key"),
21 | ca: fs.readFileSync("./test/cert/testCA.pem")
22 | };
23 | const server = new WSServer(serverOptions);
24 | server.listen();
25 |
26 | server.onConnection( (client: ClientInterface) => {
27 | client.onData( (data: Buffer | string) => {
28 | assert(data.toString() == "hello");
29 | client.send("received!");
30 | });
31 | client.onClose( () => {
32 | server.close();
33 | resolve();
34 | });
35 | });
36 |
37 | const clientOptions = {
38 | host: "localhost",
39 | port: 8181,
40 | secure: true,
41 | rejectUnauthorized: false,
42 | cert: fs.readFileSync("./test/cert/testClient.cert"),
43 | key: fs.readFileSync("./test/cert/testClient.key"),
44 | ca: fs.readFileSync("./test/cert/testCA.pem")
45 | };
46 | const client = new WSClient(clientOptions);
47 | client.connect();
48 |
49 | client.onConnect( () => {
50 | client.onData( (data: Buffer | string) => {
51 | assert(data.toString() == "received!");
52 | client.close();
53 | });
54 | client.onClose( () => {
55 | });
56 | client.send("hello");
57 | });
58 | });
59 | }
60 |
61 | @Test()
62 | public async clientserver_client_reject_unauthorized() {
63 | await new Promise(resolve => {
64 | const serverOptions = {
65 | host: "localhost",
66 | port: 8181,
67 | requestCert: true,
68 | rejectUnauthorized: false,
69 | cert: fs.readFileSync("./test/cert/localhost.cert"),
70 | key: fs.readFileSync("./test/cert/localhost.key"),
71 | ca: fs.readFileSync("./test/cert/testCA.pem")
72 | };
73 | const server = new WSServer(serverOptions);
74 | server.listen();
75 |
76 | server.onConnection( (client: ClientInterface) => {
77 | client.onData( (data: Buffer | string) => {
78 | assert(data.toString() == "hello");
79 | client.send("received!");
80 | });
81 | client.onClose( () => {
82 | server.close();
83 | resolve();
84 | });
85 | });
86 |
87 | const clientOptions = {
88 | host: "localhost",
89 | port: 8181,
90 | secure: true,
91 | rejectUnauthorized: true,
92 | cert: fs.readFileSync("./test/cert/testClient.cert"),
93 | key: fs.readFileSync("./test/cert/testClient.key"),
94 | ca: fs.readFileSync("./test/cert/testCA.pem")
95 | };
96 | const client = new WSClient(clientOptions);
97 | client.connect();
98 |
99 | client.onConnect( () => {
100 | client.onData( (data: Buffer | string) => {
101 | assert(data.toString() == "received!");
102 | client.close();
103 | });
104 | client.onClose( () => {
105 | });
106 | client.send("hello");
107 | });
108 | });
109 | }
110 |
111 | @Test()
112 | public async clientserver_reject_unauthorized() {
113 | await new Promise(resolve => {
114 | const serverOptions = {
115 | host: "localhost",
116 | port: 8181,
117 | requestCert: true,
118 | rejectUnauthorized: true,
119 | cert: fs.readFileSync("./test/cert/localhost.cert"),
120 | key: fs.readFileSync("./test/cert/localhost.key"),
121 | ca: fs.readFileSync("./test/cert/testCA.pem")
122 | };
123 | const server = new WSServer(serverOptions);
124 | server.listen();
125 |
126 | server.onConnection( (client: ClientInterface) => {
127 | client.onData( (data: Buffer | string) => {
128 | assert(data.toString() == "hello");
129 | client.send("received!");
130 | });
131 | client.onClose( () => {
132 | server.close();
133 | resolve();
134 | });
135 | });
136 |
137 | const clientOptions = {
138 | host: "localhost",
139 | port: 8181,
140 | secure: true,
141 | rejectUnauthorized: true,
142 | cert: fs.readFileSync("./test/cert/testClient.cert"),
143 | key: fs.readFileSync("./test/cert/testClient.key"),
144 | ca: fs.readFileSync("./test/cert/testCA.pem")
145 | };
146 | const client = new WSClient(clientOptions);
147 | client.connect();
148 |
149 | client.onConnect( () => {
150 | client.onData( (data: Buffer | string) => {
151 | assert(data.toString() == "received!");
152 | client.close();
153 | });
154 | client.onClose( () => {
155 | });
156 | client.send("hello");
157 | });
158 | });
159 | }
160 |
161 | @Test()
162 | public async clientserver_allow_unauthorized() {
163 | await new Promise(resolve => {
164 | const serverOptions = {
165 | host: "localhost",
166 | port: 8181,
167 | requestCert: true,
168 | rejectUnauthorized: false,
169 | cert: fs.readFileSync("./test/cert/localhost.cert"),
170 | key: fs.readFileSync("./test/cert/localhost.key")
171 | };
172 | const server = new WSServer(serverOptions);
173 | server.listen();
174 |
175 | server.onConnection( (client: ClientInterface) => {
176 | client.onData( (data: Buffer | string) => {
177 | assert(data.toString() == "hello");
178 | client.send("received!");
179 | });
180 | client.onClose( () => {
181 | server.close();
182 | resolve();
183 | });
184 | });
185 |
186 | const clientOptions = {
187 | host: "localhost",
188 | port: 8181,
189 | secure: true,
190 | rejectUnauthorized: false,
191 | cert: fs.readFileSync("./test/cert/testClient.cert"),
192 | key: fs.readFileSync("./test/cert/testClient.key")
193 | };
194 | const client = new WSClient(clientOptions);
195 | client.connect();
196 |
197 | client.onConnect( () => {
198 | client.onData( (data: Buffer | string) => {
199 | assert(data.toString() == "received!");
200 | client.close();
201 | });
202 | client.onClose( () => {
203 | });
204 | client.send("hello");
205 | });
206 | });
207 | }
208 |
209 | @Test()
210 | public async clientserver_ipv4() {
211 | await new Promise(resolve => {
212 | const serverOptions = {
213 | host: "127.0.0.1",
214 | port: 8181,
215 | requestCert: true,
216 | rejectUnauthorized: false,
217 | cert: fs.readFileSync("./test/cert/localhost.cert"),
218 | key: fs.readFileSync("./test/cert/localhost.key")
219 | };
220 | const server = new WSServer(serverOptions);
221 | server.listen();
222 |
223 | server.onConnection( (client: ClientInterface) => {
224 | client.onData( (data: Buffer | string) => {
225 | assert(data.toString() == "hello");
226 | client.send("received!");
227 | });
228 | client.onClose( () => {
229 | server.close();
230 | resolve();
231 | });
232 | });
233 |
234 | const clientOptions = {
235 | host: "127.0.0.1",
236 | port: 8181,
237 | secure: true,
238 | rejectUnauthorized: false,
239 | cert: fs.readFileSync("./test/cert/testClient.cert"),
240 | key: fs.readFileSync("./test/cert/testClient.key")
241 | };
242 | const client = new WSClient(clientOptions);
243 | client.connect();
244 |
245 | client.onConnect( () => {
246 | client.onData( (data: Buffer | string) => {
247 | assert(data.toString() == "received!");
248 | client.close();
249 | });
250 | client.onClose( () => {
251 | });
252 | client.send("hello");
253 | });
254 | });
255 | }
256 |
257 | @Test()
258 | public async clientserver_ipv6_short() {
259 | await new Promise(resolve => {
260 | const serverOptions = {
261 | host: "::1",
262 | port: 8181,
263 | requestCert: true,
264 | rejectUnauthorized: false,
265 | cert: fs.readFileSync("./test/cert/localhost.cert"),
266 | key: fs.readFileSync("./test/cert/localhost.key")
267 | };
268 | const server = new WSServer(serverOptions);
269 | server.listen();
270 |
271 | server.onConnection( (client: ClientInterface) => {
272 | client.onData( (data: Buffer | string) => {
273 | assert(data.toString() == "hello");
274 | client.send("received!");
275 | });
276 | client.onClose( () => {
277 | server.close();
278 | resolve();
279 | });
280 | });
281 |
282 | const clientOptions = {
283 | host: "::1",
284 | port: 8181,
285 | secure: true,
286 | rejectUnauthorized: false,
287 | cert: fs.readFileSync("./test/cert/testClient.cert"),
288 | key: fs.readFileSync("./test/cert/testClient.key")
289 | };
290 | const client = new WSClient(clientOptions);
291 | client.connect();
292 |
293 | client.onConnect( () => {
294 | client.onData( (data: Buffer | string) => {
295 | assert(data.toString() == "received!");
296 | client.close();
297 | });
298 | client.onClose( () => {
299 | });
300 | client.send("hello");
301 | });
302 | });
303 | }
304 |
305 | @Test()
306 | public async clientserver_ipv6_long() {
307 | await new Promise(resolve => {
308 | const serverOptions = {
309 | host: "0:0:0:0:0:0:0:1",
310 | port: 8181,
311 | requestCert: true,
312 | rejectUnauthorized: false,
313 | cert: fs.readFileSync("./test/cert/localhost.cert"),
314 | key: fs.readFileSync("./test/cert/localhost.key")
315 | };
316 | const server = new WSServer(serverOptions);
317 | server.listen();
318 |
319 | server.onConnection( (client: ClientInterface) => {
320 | client.onData( (data: Buffer | string) => {
321 | assert(data.toString() == "hello");
322 | client.send("received!");
323 | });
324 | client.onClose( () => {
325 | server.close();
326 | resolve();
327 | });
328 | });
329 |
330 | const clientOptions = {
331 | host: "0:0:0:0:0:0:0:1",
332 | port: 8181,
333 | secure: true,
334 | rejectUnauthorized: false,
335 | cert: fs.readFileSync("./test/cert/testClient.cert"),
336 | key: fs.readFileSync("./test/cert/testClient.key")
337 | };
338 | const client = new WSClient(clientOptions);
339 | client.connect();
340 |
341 | client.onConnect( () => {
342 | client.onData( (data: Buffer | string) => {
343 | assert(data.toString() == "received!");
344 | client.close();
345 | });
346 | client.onClose( () => {
347 | });
348 | client.send("hello");
349 | });
350 | });
351 | }
352 |
353 | @Test()
354 | public async clientserver_missing_host() {
355 | await new Promise(resolve => {
356 | const serverOptions = {
357 | port: 8182,
358 | requestCert: true,
359 | rejectUnauthorized: false,
360 | cert: fs.readFileSync("./test/cert/localhost.cert"),
361 | key: fs.readFileSync("./test/cert/localhost.key")
362 | };
363 | const server = new WSServer(serverOptions);
364 | server.listen();
365 |
366 | server.onConnection( (client: ClientInterface) => {
367 | client.onData( (data: Buffer | string) => {
368 | assert(data.toString() == "hello");
369 | server.close();
370 | resolve();
371 | });
372 | });
373 |
374 | const clientOptions = {
375 | port: 8182,
376 | secure: true,
377 | rejectUnauthorized: false,
378 | cert: fs.readFileSync("./test/cert/testClient.cert"),
379 | key: fs.readFileSync("./test/cert/testClient.key")
380 | };
381 | const client = new WSClient(clientOptions);
382 | client.connect();
383 |
384 | client.onConnect( () => {
385 | client.onData( (data: Buffer | string) => {
386 | client.close();
387 | });
388 | client.send("hello");
389 | });
390 | });
391 | }
392 | }
393 |
--------------------------------------------------------------------------------
/test/connectionTCPTLS.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestSuite, Test } from "testyts";
2 | import {TCPClient} from "../index";
3 | import {TCPServer, ClientInterface} from "../index";
4 |
5 | const assert = require("assert");
6 | const fs = require("fs");
7 |
8 | @TestSuite()
9 | export class ConnectionTCPTLS {
10 |
11 | @Test()
12 | public async clientserver_server_reject_unauthorized() {
13 | await new Promise(resolve => {
14 | const serverOptions = {
15 | host: "localhost",
16 | port: 8181,
17 | requestCert: true,
18 | rejectUnauthorized: true,
19 | cert: fs.readFileSync("./test/cert/localhost.cert"),
20 | key: fs.readFileSync("./test/cert/localhost.key"),
21 | ca: fs.readFileSync("./test/cert/testCA.cert")
22 | };
23 | const server = new TCPServer(serverOptions);
24 | server.listen();
25 |
26 | server.onConnection( (client: ClientInterface) => {
27 | client.onData( (data: Buffer | string) => {
28 | assert(data.toString() == "hello");
29 | client.send("received!");
30 | });
31 | client.onClose( () => {
32 | server.close();
33 | resolve();
34 | });
35 | });
36 |
37 | const clientOptions = {
38 | host: "localhost",
39 | port: 8181,
40 | secure: true,
41 | rejectUnauthorized: false,
42 | cert: fs.readFileSync("./test/cert/testClient.cert"),
43 | key: fs.readFileSync("./test/cert/testClient.key"),
44 | ca: fs.readFileSync("./test/cert/testCA.cert")
45 | };
46 | const client = new TCPClient(clientOptions);
47 | client.connect();
48 |
49 | client.onConnect( () => {
50 | client.onData( (data: Buffer | string) => {
51 | assert(data.toString() == "received!");
52 | client.close();
53 | });
54 | client.onClose( () => {
55 | });
56 | client.send("hello");
57 | });
58 | });
59 | }
60 |
61 | @Test()
62 | public async clientserver_client_reject_unauthorized() {
63 | await new Promise(resolve => {
64 | const serverOptions = {
65 | host: "localhost",
66 | port: 8181,
67 | requestCert: true,
68 | rejectUnauthorized: false,
69 | cert: fs.readFileSync("./test/cert/localhost.cert"),
70 | key: fs.readFileSync("./test/cert/localhost.key"),
71 | ca: fs.readFileSync("./test/cert/testCA.cert")
72 | };
73 | const server = new TCPServer(serverOptions);
74 | server.listen();
75 |
76 | server.onConnection( (client: ClientInterface) => {
77 | client.onData( (data: Buffer | string) => {
78 | assert(data.toString() == "hello");
79 | client.send("received!");
80 | });
81 | client.onClose( () => {
82 | server.close();
83 | resolve();
84 | });
85 | });
86 |
87 | const clientOptions = {
88 | host: "localhost",
89 | port: 8181,
90 | secure: true,
91 | rejectUnauthorized: true,
92 | cert: fs.readFileSync("./test/cert/testClient.cert"),
93 | key: fs.readFileSync("./test/cert/testClient.key"),
94 | ca: fs.readFileSync("./test/cert/testCA.cert")
95 | };
96 | const client = new TCPClient(clientOptions);
97 | client.connect();
98 |
99 | client.onConnect( () => {
100 | client.onData( (data: Buffer | string) => {
101 | assert(data.toString() == "received!");
102 | client.close();
103 | });
104 | client.onClose( () => {
105 | });
106 | client.send("hello");
107 | });
108 | });
109 | }
110 |
111 | @Test()
112 | public async clientserver_reject_unauthorized() {
113 | await new Promise(resolve => {
114 | const serverOptions = {
115 | host: "localhost",
116 | port: 8181,
117 | requestCert: true,
118 | rejectUnauthorized: true,
119 | cert: fs.readFileSync("./test/cert/localhost.cert"),
120 | key: fs.readFileSync("./test/cert/localhost.key"),
121 | ca: fs.readFileSync("./test/cert/testCA.cert")
122 | };
123 | const server = new TCPServer(serverOptions);
124 | server.listen();
125 |
126 | server.onConnection( (client: ClientInterface) => {
127 | client.onData( (data: Buffer | string) => {
128 | assert(data.toString() == "hello");
129 | client.send("received!");
130 | });
131 | client.onClose( () => {
132 | server.close();
133 | resolve();
134 | });
135 | });
136 |
137 | const clientOptions = {
138 | host: "localhost",
139 | port: 8181,
140 | secure: true,
141 | rejectUnauthorized: true,
142 | cert: fs.readFileSync("./test/cert/testClient.cert"),
143 | key: fs.readFileSync("./test/cert/testClient.key"),
144 | ca: fs.readFileSync("./test/cert/testCA.cert")
145 | };
146 | const client = new TCPClient(clientOptions);
147 | client.connect();
148 |
149 | client.onConnect( () => {
150 | client.onData( (data: Buffer | string) => {
151 | assert(data.toString() == "received!");
152 | client.close();
153 | });
154 | client.onClose( () => {
155 | });
156 | client.send("hello");
157 | });
158 | });
159 | }
160 |
161 | @Test()
162 | public async clientserver() {
163 | await new Promise(resolve => {
164 | const serverOptions = {
165 | host: "localhost",
166 | port: 8181,
167 | requestCert: true,
168 | rejectUnauthorized: false,
169 | cert: fs.readFileSync("./test/cert/localhost.cert"),
170 | key: fs.readFileSync("./test/cert/localhost.key")
171 | };
172 | const server = new TCPServer(serverOptions);
173 | server.listen();
174 |
175 | server.onConnection( (client: ClientInterface) => {
176 | client.onData( (data: Buffer | string) => {
177 | assert(data.toString() == "hello");
178 | client.send("received!");
179 | });
180 | client.onClose( () => {
181 | server.close();
182 | resolve();
183 | });
184 | });
185 |
186 | const clientOptions = {
187 | host: "localhost",
188 | port: 8181,
189 | secure: true,
190 | rejectUnauthorized: false,
191 | cert: fs.readFileSync("./test/cert/testClient.cert"),
192 | key: fs.readFileSync("./test/cert/testClient.key")
193 | };
194 | const client = new TCPClient(clientOptions);
195 | client.connect();
196 |
197 | client.onConnect( () => {
198 | client.onData( (data: Buffer | string) => {
199 | assert(data.toString() == "received!");
200 | client.close();
201 | });
202 | client.onClose( () => {
203 | });
204 | client.send("hello");
205 | });
206 | });
207 | }
208 |
209 | @Test()
210 | public async clientserver_ipv4() {
211 | await new Promise(resolve => {
212 | const serverOptions = {
213 | host: "127.0.0.1",
214 | port: 8181,
215 | requestCert: true,
216 | rejectUnauthorized: false,
217 | cert: fs.readFileSync("./test/cert/localhost.cert"),
218 | key: fs.readFileSync("./test/cert/localhost.key")
219 | };
220 | const server = new TCPServer(serverOptions);
221 | server.listen();
222 |
223 | server.onConnection( (client: ClientInterface) => {
224 | client.onData( (data: Buffer | string) => {
225 | assert(data.toString() == "hello");
226 | client.send("received!");
227 | });
228 | client.onClose( () => {
229 | server.close();
230 | resolve();
231 | });
232 | });
233 |
234 | const clientOptions = {
235 | host: "127.0.0.1",
236 | port: 8181,
237 | secure: true,
238 | rejectUnauthorized: false,
239 | cert: fs.readFileSync("./test/cert/testClient.cert"),
240 | key: fs.readFileSync("./test/cert/testClient.key")
241 | };
242 | const client = new TCPClient(clientOptions);
243 | client.connect();
244 |
245 | client.onConnect( () => {
246 | client.onData( (data: Buffer | string) => {
247 | assert(data.toString() == "received!");
248 | client.close();
249 | });
250 | client.onClose( () => {
251 | });
252 | client.send("hello");
253 | });
254 | });
255 | }
256 |
257 | @Test()
258 | public async clientserver_ipv6_short() {
259 | await new Promise(resolve => {
260 | const serverOptions = {
261 | host: "::1",
262 | port: 8181,
263 | requestCert: true,
264 | rejectUnauthorized: false,
265 | cert: fs.readFileSync("./test/cert/localhost.cert"),
266 | key: fs.readFileSync("./test/cert/localhost.key")
267 | };
268 | const server = new TCPServer(serverOptions);
269 | server.listen();
270 |
271 | server.onConnection( (client: ClientInterface) => {
272 | client.onData( (data: Buffer | string) => {
273 | assert(data.toString() == "hello");
274 | client.send("received!");
275 | });
276 | client.onClose( () => {
277 | server.close();
278 | resolve();
279 | });
280 | });
281 |
282 | const clientOptions = {
283 | host: "::1",
284 | port: 8181,
285 | secure: true,
286 | rejectUnauthorized: false,
287 | cert: fs.readFileSync("./test/cert/testClient.cert"),
288 | key: fs.readFileSync("./test/cert/testClient.key")
289 | };
290 | const client = new TCPClient(clientOptions);
291 | client.connect();
292 |
293 | client.onConnect( () => {
294 | client.onData( (data: Buffer | string) => {
295 | assert(data.toString() == "received!");
296 | client.close();
297 | });
298 | client.onClose( () => {
299 | });
300 | client.send("hello");
301 | });
302 | });
303 | }
304 |
305 | @Test()
306 | public async clientserver_ipv6_long() {
307 | await new Promise(resolve => {
308 | const serverOptions = {
309 | host: "0:0:0:0:0:0:0:1",
310 | port: 8181,
311 | requestCert: true,
312 | rejectUnauthorized: false,
313 | cert: fs.readFileSync("./test/cert/localhost.cert"),
314 | key: fs.readFileSync("./test/cert/localhost.key")
315 | };
316 | const server = new TCPServer(serverOptions);
317 | server.listen();
318 |
319 | server.onConnection( (client: ClientInterface) => {
320 | client.onData( (data: Buffer | string) => {
321 | assert(data.toString() == "hello");
322 | client.send("received!");
323 | });
324 | client.onClose( () => {
325 | server.close();
326 | resolve();
327 | });
328 | });
329 |
330 | const clientOptions = {
331 | host: "0:0:0:0:0:0:0:1",
332 | port: 8181,
333 | secure: true,
334 | rejectUnauthorized: false,
335 | cert: fs.readFileSync("./test/cert/testClient.cert"),
336 | key: fs.readFileSync("./test/cert/testClient.key")
337 | };
338 | const client = new TCPClient(clientOptions);
339 | client.connect();
340 |
341 | client.onConnect( () => {
342 | client.onData( (data: Buffer | string) => {
343 | assert(data.toString() == "received!");
344 | client.close();
345 | });
346 | client.onClose( () => {
347 | });
348 | client.send("hello");
349 | });
350 | });
351 | }
352 |
353 | @Test()
354 | public async clientserver_missing_host() {
355 | await new Promise(resolve => {
356 | const serverOptions = {
357 | port: 8182,
358 | requestCert: true,
359 | rejectUnauthorized: false,
360 | cert: fs.readFileSync("./test/cert/localhost.cert"),
361 | key: fs.readFileSync("./test/cert/localhost.key")
362 | };
363 | const server = new TCPServer(serverOptions);
364 | server.listen();
365 |
366 | server.onConnection( (client: ClientInterface) => {
367 | client.onData( (data: Buffer | string) => {
368 | assert(data.toString() == "hello");
369 | server.close();
370 | resolve();
371 | });
372 | });
373 |
374 | const clientOptions = {
375 | port: 8182,
376 | secure: true,
377 | rejectUnauthorized: false,
378 | cert: fs.readFileSync("./test/cert/testClient.cert"),
379 | key: fs.readFileSync("./test/cert/testClient.key")
380 | };
381 | const client = new TCPClient(clientOptions);
382 | client.connect();
383 |
384 | client.onConnect( () => {
385 | client.onData( (data: Buffer | string) => {
386 | client.close();
387 | });
388 | client.send("hello");
389 | });
390 | });
391 | }
392 | }
393 |
--------------------------------------------------------------------------------
/test/TCPClient.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestSuite, Test } from "testyts";
2 | import {TCPClient} from "../index";
3 |
4 | const assert = require("assert");
5 | const net = require("net");
6 | const tls = require("tls");
7 |
8 | @TestSuite()
9 | export class TCPClientSocketConnect {
10 |
11 | @Test()
12 | public successful_call_setting_USE_TLS() {
13 | assert.doesNotThrow(() => {
14 | const client = new TCPClient({
15 | "host": "host.com",
16 | "port": 99,
17 | "secure": true,
18 | });
19 | //@ts-ignore: protected data
20 | assert(client.clientOptions!.host == "host.com");
21 | //@ts-ignore: protected data
22 | assert(client.clientOptions!.port == 99);
23 | //@ts-ignore: protected data
24 | assert(client.clientOptions!.secure == true);
25 | //@ts-ignore: protected data
26 | assert(client.clientOptions!.rejectUnauthorized == null);
27 |
28 | //@ts-ignore: protected data
29 | assert(client.socket == null);
30 | //@ts-ignore: protected data
31 | assert(client._isClosed == false);
32 |
33 | //@ts-ignore: overwrite read-only
34 | tls.connect = function() {
35 | return {
36 | "on": function(name: string, fn: Function) {
37 | assert(name == "secureConnect");
38 | assert(typeof fn == "function");
39 | }
40 | };
41 | };
42 | //@ts-ignore
43 | client.socketConnect();
44 | });
45 | }
46 |
47 | @Test()
48 | public successful_call_without_USE_TLS() {
49 | assert.doesNotThrow(() => {
50 | const client = new TCPClient({
51 | "host": "host.com",
52 | "port": 99,
53 | "secure": false,
54 | });
55 | //@ts-ignore: protected data
56 | assert(client.clientOptions!.host == "host.com");
57 | //@ts-ignore: protected data
58 | assert(client.clientOptions!.port == 99);
59 | //@ts-ignore: protected data
60 | assert(client.clientOptions!.secure == false);
61 | //@ts-ignore: protected data
62 | assert(client.clientOptions!.rejectUnauthorized == null);
63 |
64 | //@ts-ignore: protected data
65 | assert(client.socket == null);
66 | //@ts-ignore: protected data
67 | assert(client._isClosed == false);
68 |
69 | //@ts-ignore: overwrite read-only
70 | net.connect = function() {
71 | return {
72 | "on": function(name: string, fn: Function) {
73 | assert(name == "connect");
74 | assert(typeof fn == "function");
75 | }
76 | };
77 | };
78 | //@ts-ignore
79 | client.socketConnect();
80 | });
81 | }
82 |
83 | @Test()
84 | public successful_call_with_existing_socket() {
85 | assert.doesNotThrow(() => {
86 | //@ts-ignore: incomplete implementation
87 | let socket = new net.Socket();
88 | let flagOnEvent = false;
89 | //@ts-ignore: incomplete inheritance
90 | class TestClient extends TCPClient {
91 | triggerEvent(evt: string, data: any) {
92 | if(evt == "close") {
93 | flagOnEvent = true;
94 | }
95 | }
96 | }
97 | //@ts-ignore: incomplete socket specification
98 | const client = new TCPClient({
99 | "host": "host.com",
100 | "port": 99,
101 | "secure": false,
102 | }, /*@ts-ignore*/ socket);
103 |
104 | //@ts-ignore: protected data
105 | assert(client.socket != null);
106 |
107 | //@ts-ignore: protected method
108 | client.triggerEvent("close");
109 | });
110 | }
111 |
112 | @Test()
113 | public socket_already_created() {
114 | assert.throws(() => {
115 | //@ts-ignore: incomplete implementation
116 | let socket = new net.Socket();
117 | //@ts-ignore: incomplete socket specification
118 | const client = new TCPClient({
119 | "host": "host.com",
120 | "port": 99,
121 | "secure": false,
122 | }, /*@ts-ignore*/ socket);
123 |
124 | //@ts-ignore: protected data
125 | assert(client.socket != null);
126 | client.connect();
127 | }, /Socket already created/);
128 | }
129 |
130 | @Test()
131 | public missing_client_options() {
132 | assert.throws(() => {
133 | //@ts-ignore: incomplete socket specification
134 | const client = new TCPClient({
135 | "host": "host.com",
136 | "port": 99,
137 | "secure": false,
138 | });
139 |
140 | //@ts-ignore: protected data
141 | client.clientOptions = undefined;
142 | client.connect();
143 | }, /clientOptions is required to create socket/);
144 | }
145 |
146 |
147 | }
148 |
149 | @TestSuite()
150 | export class TCPClientSocketHook {
151 | @Test()
152 | public successful_call() {
153 | assert.doesNotThrow(() => {
154 | const client = new TCPClient({
155 | "host": "host.com",
156 | "port": 99,
157 | "secure": false,
158 | });
159 | let counter = 0;
160 | //@ts-ignore
161 | client.socket = {
162 | "on": function(name: string, fn: Function): any {
163 | counter++;
164 | assert(name == "data" || name == "error" || name == "close" );
165 | assert(typeof fn == "function");
166 | return null;
167 | }
168 | };
169 | assert(counter == 0);
170 | //@ts-ignore
171 | client.socketHook();
172 | assert(counter == 3);
173 | });
174 | }
175 | }
176 |
177 | @TestSuite()
178 | export class TCPClientSocketSend {
179 | @Test()
180 | public successful_call() {
181 | assert.doesNotThrow(() => {
182 | const client = new TCPClient({
183 | "host": "host.com",
184 | "port": 99,
185 | "secure": false,
186 | });
187 | //@ts-ignore: incomplete socket implementation
188 | client.socket = {
189 | "write": function(buffer: Buffer): boolean {
190 | assert(buffer.toString() == "testdata123");
191 | assert(Buffer.isBuffer(buffer));
192 | return true;
193 | }
194 | };
195 | //@ts-ignore: protected method
196 | client.socketSend(Buffer.from("testdata123"));
197 | });
198 | }
199 | }
200 |
201 | @TestSuite()
202 | export class TCPClientSocketClose {
203 | @Test()
204 | public successful_call() {
205 | assert.doesNotThrow(() => {
206 | const client = new TCPClient({
207 | "host": "host.com",
208 | "port": 99,
209 | "secure": false,
210 | });
211 | let hasEnded = false;
212 | //@ts-ignore: incomplete socket implementation
213 | client.socket = {
214 | "end": function() {
215 | hasEnded = true;
216 | }
217 | };
218 | assert(hasEnded == false);
219 | //@ts-ignore: protected method
220 | client.socketClose();
221 | //@ts-ignore
222 | assert(hasEnded == true);
223 | });
224 | }
225 | }
226 |
227 | @TestSuite()
228 | export class TCPClientSocketError {
229 | @Test()
230 | public successful_call() {
231 | assert.doesNotThrow(() => {
232 | const client = new TCPClient({
233 | "host": "host.com",
234 | "port": 99,
235 | "secure": false,
236 | });
237 | let flag = false;
238 | //@ts-ignore: protected method
239 | client.socketError = function(data: Buffer) {
240 | flag = true;
241 | //@ts-ignore
242 | assert(data.toString() == "Test Error");
243 | };
244 | assert(flag == false);
245 | //@ts-ignore: protected method
246 | client.error(new Error("Test Error"));
247 | //@ts-ignore
248 | assert(flag == true);
249 | });
250 | }
251 | }
252 |
253 | @TestSuite()
254 | export class TCPClientGetLocalAddress {
255 | @Test()
256 | public retrieve_undefined() {
257 | assert.doesNotThrow(() => {
258 | const client = new TCPClient({
259 | "host": "host.com",
260 | "port": 99,
261 | "secure": false,
262 | });
263 | //@ts-ignore: incomplete implementation
264 | client.socket = {
265 | }
266 | assert(client.getLocalAddress() == undefined);
267 | });
268 | }
269 |
270 | @Test()
271 | public retrieve_undefined_when_null() {
272 | assert.doesNotThrow(() => {
273 | const client = new TCPClient({
274 | "host": "host.com",
275 | "port": 99,
276 | "secure": false,
277 | });
278 | //@ts-ignore: incomplete implementation
279 | client.socket = {
280 | //@ts-ignore: unexpected data conversion
281 | localAddress: null
282 | }
283 | assert(client.getLocalAddress() == undefined);
284 | });
285 | }
286 |
287 | @Test()
288 | public retrieve_string() {
289 | assert.doesNotThrow(() => {
290 | const client = new TCPClient({
291 | "host": "host.com",
292 | "port": 99,
293 | "secure": false,
294 | });
295 | //@ts-ignore: incomplete implementation
296 | client.socket = {
297 | localAddress: "host.com"
298 | }
299 | assert(client.getLocalAddress() == "host.com");
300 | });
301 | }
302 | }
303 |
304 | @TestSuite()
305 | export class TCPClientGetRemoteAddress {
306 | @Test()
307 | public retrieve_undefined() {
308 | assert.doesNotThrow(() => {
309 | const client = new TCPClient({
310 | "host": "host.com",
311 | "port": 99,
312 | "secure": false,
313 | });
314 | //@ts-ignore: incomplete implementation
315 | client.socket = {
316 | }
317 | assert(client.getRemoteAddress() == undefined);
318 | });
319 | }
320 |
321 | @Test()
322 | public retrieve_undefined_when_null() {
323 | assert.doesNotThrow(() => {
324 | const client = new TCPClient({
325 | "host": "host.com",
326 | "port": 99,
327 | "secure": false,
328 | });
329 | //@ts-ignore: incomplete implementation
330 | client.socket = {
331 | //@ts-ignore: unexpected data conversion
332 | remoteAddress: null
333 | }
334 | assert(client.getRemoteAddress() == undefined);
335 | });
336 | }
337 |
338 | @Test()
339 | public retrieve_string() {
340 | assert.doesNotThrow(() => {
341 | const client = new TCPClient({
342 | "host": "host.com",
343 | "port": 99,
344 | "secure": false,
345 | });
346 | //@ts-ignore: incomplete implementation
347 | client.socket = {
348 | remoteAddress: "host.com"
349 | }
350 | assert(client.getRemoteAddress() == "host.com");
351 | });
352 | }
353 | }
354 |
355 | @TestSuite()
356 | export class TCPClientGetRemotePort {
357 | @Test()
358 | public retrieve_undefined() {
359 | assert.doesNotThrow(() => {
360 | const client = new TCPClient({
361 | "host": "host.com",
362 | "port": 99,
363 | "secure": false,
364 | });
365 | //@ts-ignore: incomplete implementation
366 | client.socket = {
367 | }
368 | assert(client.getRemotePort() == undefined);
369 | });
370 | }
371 |
372 | @Test()
373 | public retrieve_undefined_when_null() {
374 | assert.doesNotThrow(() => {
375 | const client = new TCPClient({
376 | "host": "host.com",
377 | "port": 99,
378 | "secure": false,
379 | });
380 | //@ts-ignore: incomplete implementation
381 | client.socket = {
382 | //@ts-ignore: unexpected data conversion
383 | remotePort: null
384 | }
385 | assert(client.getRemotePort() == undefined);
386 | });
387 | }
388 |
389 | @Test()
390 | public retrieve_string() {
391 | assert.doesNotThrow(() => {
392 | const client = new TCPClient({
393 | "host": "host.com",
394 | "port": 99,
395 | "secure": false,
396 | });
397 | //@ts-ignore: incomplete implementation
398 | client.socket = {
399 | remotePort: 999
400 | }
401 | assert(client.getRemotePort() == 999);
402 | });
403 | }
404 | }
405 |
406 | @TestSuite()
407 | export class TCPClientGetLocalPort {
408 | @Test()
409 | public retrieve_undefined() {
410 | assert.doesNotThrow(() => {
411 | const client = new TCPClient({
412 | "host": "host.com",
413 | "port": 99,
414 | "secure": false,
415 | });
416 | //@ts-ignore: incomplete implementation
417 | client.socket = {
418 | }
419 | assert(client.getLocalPort() == undefined);
420 | });
421 | }
422 |
423 | @Test()
424 | public retrieve_undefined_when_null() {
425 | assert.doesNotThrow(() => {
426 | const client = new TCPClient({
427 | "host": "host.com",
428 | "port": 99,
429 | "secure": false,
430 | });
431 | //@ts-ignore: incomplete implementation
432 | client.socket = {
433 | //@ts-ignore: unexpected data conversion
434 | localPort: null
435 | }
436 | assert(client.getLocalPort() == undefined);
437 | });
438 | }
439 |
440 | @Test()
441 | public retrieve_string() {
442 | assert.doesNotThrow(() => {
443 | const client = new TCPClient({
444 | "host": "host.com",
445 | "port": 99,
446 | "secure": false,
447 | });
448 | //@ts-ignore: incomplete implementation
449 | client.socket = {
450 | localPort: 999
451 | }
452 | assert(client.getLocalPort() == 999);
453 | });
454 | }
455 | }
456 |
--------------------------------------------------------------------------------
/test/Server.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestSuite, Test } from "testyts";
2 | import {Server, Client} from "../index";
3 |
4 | const assert = require("assert");
5 |
6 | @TestSuite()
7 | export class ServerConstructor {
8 |
9 | @Test()
10 | public valid_options() {
11 | class TestServer extends Server {
12 | _serverCreate() {
13 | }
14 | }
15 |
16 | assert.doesNotThrow(() => {
17 | const server = new TestServer({
18 | "host": "host.com",
19 | "port": 99,
20 | });
21 |
22 | //@ts-ignore: protected data
23 | assert(server.serverOptions.host == "host.com");
24 | //@ts-ignore: protected data
25 | assert(server.serverOptions.port == 99);
26 | //@ts-ignore: protected data
27 | assert(server.serverOptions.rejectUnauthorized == null);
28 | //@ts-ignore: protected data
29 | assert(server.serverOptions.bufferData == undefined);
30 | //@ts-ignore: protected data
31 | assert(server.serverOptions.ipv6Only == undefined);
32 | //@ts-ignore: protected data
33 | assert(server.serverOptions.requestCert == undefined);
34 | //@ts-ignore: protected data
35 | assert(server.serverOptions.cert == undefined);
36 | //@ts-ignore: protected data
37 | assert(server.serverOptions.key == undefined);
38 | //@ts-ignore: protected data
39 | assert(server.serverOptions.ca == undefined);
40 | //@ts-ignore: protected data
41 | assert(server.clients.length == 0);
42 | //@ts-ignore: protected data
43 | assert(Object.keys(server.eventHandlers).length == 0);
44 | //@ts-ignore: protected data
45 | assert(server._isClosed == false);
46 | });
47 | }
48 |
49 | public server_create_triggered() {
50 | let flag = false;
51 | class TestServer extends Server {
52 | _serverCreate() {
53 | flag = true;
54 | }
55 | }
56 |
57 | assert.doesNotThrow(() => {
58 | assert(flag == false);
59 | const server = new TestServer({
60 | "host": "host.com",
61 | "port": 99,
62 | });
63 | assert(flag == true);
64 | assert(server);
65 | });
66 | }
67 | }
68 |
69 | @TestSuite()
70 | export class ServerServerClose {
71 |
72 | @Test()
73 | public trigger_server_close() {
74 | let flag = false;
75 | class TestServer extends Server {
76 | _serverCreate() {
77 | }
78 | serverClose() {
79 | flag = true;
80 | }
81 | }
82 |
83 | assert.doesNotThrow(() => {
84 | const server = new TestServer({
85 | "host": "host.com",
86 | "port": 99,
87 | });
88 | assert(flag == false);
89 | //@ts-ignore
90 | server.serverClose();
91 | assert(flag == true);
92 | });
93 | }
94 | }
95 |
96 | @TestSuite()
97 | export class ServerListen {
98 |
99 | @Test()
100 | public trigger_server_listen() {
101 | let flag = false;
102 | class TestServer extends Server {
103 | _serverCreate() {
104 | }
105 | serverListen() {
106 | flag = true;
107 | }
108 | }
109 |
110 | assert.doesNotThrow(() => {
111 | const server = new TestServer({
112 | "host": "host.com",
113 | "port": 99,
114 | });
115 | assert(flag == false);
116 | server.listen();
117 | assert(flag == true);
118 | });
119 | }
120 | }
121 |
122 | @TestSuite()
123 | export class ServerClose {
124 |
125 | @Test()
126 | public call_server_close() {
127 | let flag = false;
128 | class TestServer extends Server {
129 | _serverCreate() {
130 | }
131 | serverClose() {
132 | flag = true;
133 | }
134 | }
135 |
136 | assert.doesNotThrow(() => {
137 | const server = new TestServer({
138 | "host": "host.com",
139 | "port": 99,
140 | });
141 | assert(flag == false);
142 | server.close();
143 | assert(flag == true);
144 | });
145 | }
146 |
147 | @Test()
148 | public call_clients_close() {
149 | class TestServer extends Server {
150 | _serverCreate() {
151 | }
152 | serverClose() {
153 | }
154 | }
155 |
156 | let clientCloseCounter = 0;
157 | class MyClient extends Client {
158 | close() {
159 | clientCloseCounter++;
160 | }
161 | }
162 |
163 | assert.doesNotThrow(() => {
164 | const server = new TestServer({
165 | "host": "host.com",
166 | "port": 99,
167 | });
168 |
169 | //@ts-ignore: protected data
170 | server.clients.push(new MyClient({port: 1}));
171 | //@ts-ignore: protected data
172 | server.clients.push(new MyClient({port: 2}));
173 | //@ts-ignore: protected data
174 | server.clients.push(new MyClient({port: 3}));
175 |
176 | //@ts-ignore: protected data
177 | assert(server.clients.length == 3);
178 | assert(clientCloseCounter == 0);
179 | server.close();
180 | //@ts-ignore: protected data
181 | assert(server.clients.length == 0);
182 | assert(clientCloseCounter == 3);
183 | });
184 | }
185 | }
186 |
187 | @TestSuite()
188 | export class ServerOnConnection {
189 |
190 | @Test()
191 | public trigger_connection_callback() {
192 | let flag = false;
193 | //@ts-ignore
194 | class TestServer extends Server {
195 | _serverCreate() {
196 | }
197 | on(evt: string, fn: Function) {
198 | assert(evt == "connection");
199 | assert(fn instanceof Function);
200 | }
201 | }
202 |
203 | assert.doesNotThrow(() => {
204 | assert(flag == false);
205 | const server = new TestServer({
206 | "host": "host.com",
207 | "port": 99,
208 | });
209 | server.onConnection(function(){});
210 | });
211 | }
212 | }
213 |
214 | @TestSuite()
215 | export class ServerOnError {
216 |
217 | @Test()
218 | public trigger_error_callback() {
219 | let flag = false;
220 | //@ts-ignore
221 | class TestServer extends Server {
222 | _serverCreate() {
223 | }
224 | on(evt: string, fn: Function) {
225 | assert(evt == "error");
226 | assert(fn instanceof Function);
227 | }
228 | }
229 |
230 | assert.doesNotThrow(() => {
231 | assert(flag == false);
232 | const server = new TestServer({
233 | "host": "host.com",
234 | "port": 99,
235 | });
236 | server.onError(function(){});
237 | });
238 | }
239 | }
240 |
241 | @TestSuite()
242 | export class ServerOnClose {
243 |
244 | @Test()
245 | public trigger_close_callback() {
246 | let flag = false;
247 | //@ts-ignore
248 | class TestServer extends Server {
249 | _serverCreate() {
250 | }
251 | on(evt: string, fn: Function) {
252 | assert(evt == "close");
253 | assert(fn instanceof Function);
254 | }
255 | }
256 |
257 | assert.doesNotThrow(() => {
258 | assert(flag == false);
259 | const server = new TestServer({
260 | "host": "host.com",
261 | "port": 99,
262 | });
263 | server.onClose(function(){});
264 | });
265 | }
266 | }
267 |
268 | @TestSuite()
269 | export class ServerAddClient {
270 |
271 | @Test()
272 | public successful_call() {
273 | let flag = false;
274 | //@ts-ignore
275 | class TestServer extends Server {
276 | _serverCreate() {
277 | }
278 | triggerEvent(evt: string) {
279 | assert(evt == "connection");
280 | flag = true;
281 | }
282 | }
283 |
284 | let clientOnCloseCounter = 0;
285 | class Client {
286 | onClose() {
287 | clientOnCloseCounter++;
288 | }
289 | }
290 |
291 | assert.doesNotThrow(() => {
292 | const server = new TestServer({
293 | "host": "host.com",
294 | "port": 99,
295 | });
296 | assert(flag == false);
297 | //@ts-ignore: protected data
298 | assert(server.clients.length == 0);
299 | assert(clientOnCloseCounter == 0);
300 | //@ts-ignore
301 | server.addClient(new Client());
302 | assert(flag == true);
303 | //@ts-ignore: protected data
304 | assert(server.clients.length == 1);
305 | assert(clientOnCloseCounter == 1);
306 | });
307 | }
308 | }
309 |
310 | @TestSuite()
311 | export class ServerRemoveClient {
312 |
313 | @Test()
314 | public successful_call() {
315 | //@ts-ignore
316 | class TestServer extends Server {
317 | _serverCreate() {
318 | }
319 | triggerEvent(evt: string) {
320 | }
321 | }
322 | class Client {
323 | onClose() {
324 | }
325 | }
326 | assert.doesNotThrow(() => {
327 | const server = new TestServer({
328 | "host": "host.com",
329 | "port": 99,
330 | });
331 | //@ts-ignore: protected data
332 | assert(server.clients.length == 0);
333 | const client = new Client();
334 | //@ts-ignore
335 | server.addClient(client);
336 | //@ts-ignore: protected data
337 | assert(server.clients.length == 1);
338 | //@ts-ignore
339 | server.removeClient(client);
340 | //@ts-ignore: protected data
341 | assert(server.clients.length == 0);
342 | });
343 | }
344 | }
345 |
346 | @TestSuite()
347 | export class ServerError {
348 |
349 | @Test()
350 | public successful_call() {
351 | let flag = false;
352 | //@ts-ignore
353 | class TestServer extends Server {
354 | _serverCreate() {
355 | }
356 | triggerEvent(evt: string) {
357 | assert(evt == "error");
358 | flag = true;
359 | }
360 | }
361 |
362 | assert.doesNotThrow(() => {
363 | const server = new TestServer({
364 | "host": "host.com",
365 | "port": 99,
366 | });
367 | assert(flag == false);
368 | //@ts-ignore
369 | server.serverError("msg");
370 | assert(flag == true);
371 | });
372 | }
373 | }
374 |
375 | @TestSuite()
376 | export class ServerCloseInner {
377 |
378 | @Test()
379 | public successful_call() {
380 | let flag = false;
381 | //@ts-ignore
382 | class TestServer extends Server {
383 | _serverCreate() {
384 | }
385 | triggerEvent(evt: string) {
386 | assert(evt == "close");
387 | flag = true;
388 | }
389 | }
390 |
391 | assert.doesNotThrow(() => {
392 | const server = new TestServer({
393 | "host": "host.com",
394 | "port": 99,
395 | });
396 | assert(flag == false);
397 | //@ts-ignore
398 | server.serverClosed();
399 | assert(flag == true);
400 | });
401 | }
402 | }
403 |
404 | @TestSuite()
405 | export class ServerOn {
406 |
407 | @Test()
408 | public successful_call() {
409 | class TestServer extends Server {
410 | _serverCreate() {
411 | }
412 | _socketHook() {
413 | }
414 | }
415 |
416 | assert.doesNotThrow(() => {
417 | const server = new TestServer({
418 | "host": "host.com",
419 | "port": 99,
420 | });
421 | //@ts-ignore: protected data
422 | assert(!server.eventHandlers["myevent"]);
423 | //@ts-ignore
424 | server.on("myevent", function(){});
425 | //@ts-ignore: protected data
426 | assert(server.eventHandlers["myevent"]);
427 | });
428 | }
429 | }
430 |
431 | @TestSuite()
432 | export class ServerValidateConfig {
433 |
434 | @Test()
435 | public all_options() {
436 | assert.doesNotThrow(() => {
437 | class TestServer extends Server {
438 | _serverCreate() {
439 | }
440 | }
441 |
442 | const server = new TestServer({
443 | "host": "host.com",
444 | "port": 99,
445 | "ipv6Only": false,
446 | "requestCert": true,
447 | "cert": "mycert",
448 | "key": "mykey",
449 | "ca": "myca",
450 | });
451 |
452 | //@ts-ignore: protected data
453 | assert(server.serverOptions.host == "host.com");
454 | //@ts-ignore: protected data
455 | assert(server.serverOptions.port == 99);
456 | //@ts-ignore: protected data
457 | assert(server.serverOptions.ipv6Only == false);
458 | //@ts-ignore: protected data
459 | assert(server.serverOptions.rejectUnauthorized == null);
460 | //@ts-ignore: protected data
461 | assert(server.serverOptions.requestCert == true);
462 | //@ts-ignore: protected data
463 | assert(server.serverOptions.cert == "mycert");
464 | //@ts-ignore: protected data
465 | assert(server.serverOptions.key == "mykey");
466 | //@ts-ignore: protected data
467 | assert(server.serverOptions.ca == "myca");
468 | });
469 | }
470 | }
471 |
472 | @TestSuite()
473 | export class ServerTriggerEvent {
474 |
475 | @Test()
476 | public all_options() {
477 | assert.doesNotThrow(() => {
478 | class TestServer extends Server {
479 | _serverCreate() {
480 | }
481 | }
482 |
483 | const server = new TestServer({
484 | "host": "host.com",
485 | "port": 99,
486 | "ipv6Only": false,
487 | "requestCert": true,
488 | "cert": "mycert",
489 | "key": "mykey",
490 | "ca": "myca",
491 | });
492 |
493 | let flag = false;
494 | assert(flag == false);
495 | const fn = function() {
496 | flag = true;
497 | };
498 | //@ts-ignore: private method
499 | server.on("testevent", fn);
500 | //@ts-ignore: protected method
501 | server.triggerEvent("testevent", fn);
502 | //@ts-ignore: data is changed inside trigger event callback
503 | assert(flag == true);
504 | });
505 | }
506 | }
507 |
--------------------------------------------------------------------------------
/src/SocketFactory.ts:
--------------------------------------------------------------------------------
1 | let TCPClient: any;
2 | let TCPServer: any;
3 | let WSServer: any;
4 |
5 | // Add browser/browserify/parcel dependency
6 | // @ts-ignore
7 | if(typeof "process" === "undefined" && !process && !process.versions && !process.versions.node) {
8 | require("regenerator-runtime/runtime");
9 | }
10 | else {
11 | // Import nodejs modules.
12 | TCPClient = require("./TCPClient").TCPClient; //eslint-disable-line @typescript-eslint/no-var-requires
13 | WSServer = require("./WSServer").WSServer; //eslint-disable-line @typescript-eslint/no-var-requires
14 | TCPServer = require("./TCPServer").TCPServer; //eslint-disable-line @typescript-eslint/no-var-requires
15 | }
16 |
17 | import {
18 | Server,
19 | } from "./Server";
20 |
21 | import {
22 | WSClient,
23 | } from "./WSClient";
24 |
25 | import {
26 | SocketFactoryConfig,
27 | SocketFactoryStats,
28 | ClientInterface,
29 | SocketFactoryInterface,
30 | EVENT_SOCKETFACTORY_ERROR,
31 | EVENT_SOCKETFACTORY_CLOSE,
32 | EVENT_SOCKETFACTORY_CONNECT,
33 | EVENT_SOCKETFACTORY_CLIENT_INIT_ERROR,
34 | EVENT_SOCKETFACTORY_CLIENT_CONNECT_ERROR,
35 | EVENT_SOCKETFACTORY_CLIENT_IP_REFUSE,
36 | EVENT_SOCKETFACTORY_SERVER_INIT_ERROR,
37 | EVENT_SOCKETFACTORY_SERVER_LISTEN_ERROR,
38 | DETAIL_SOCKETFACTORY_CLIENT_REFUSE_ERROR_IP_DENIED,
39 | DETAIL_SOCKETFACTORY_CLIENT_REFUSE_ERROR_IP_NOT_ALLOWED,
40 | DETAIL_SOCKETFACTORY_CLIENT_REFUSE_ERROR_IP_OVERFLOW,
41 | SocketFactoryErrorCallback,
42 | SocketFactoryClientInitErrorCallback,
43 | SocketFactoryClientConnectErrorCallback,
44 | SocketFactoryConnectCallback,
45 | SocketFactoryCloseCallback,
46 | SocketFactoryServerInitErrorCallback,
47 | SocketFactoryServerListenErrorCallback,
48 | SocketFactoryClientIPRefuseCallback,
49 | SOCKET_WEBSOCKET,
50 | SOCKET_TCP,
51 | } from "./types";
52 |
53 | /**
54 | * A SocketFactory emits connected sockets.
55 | *
56 | * An use case for SocketFactory is for peer-to-peer systems where one wants to abstract the difference
57 | * between client and server peers and instead of working with specific client or server sockets one
58 | * could offload that to the SocketFactory to be able to have a unified interface towards sockets,
59 | * because both parties can be client and server simultaneously.
60 | *
61 | * A SocketFactory is created for client sockets and/or server listener sockets.
62 | * The factory has the same interface for both client and server socket and they share any limits.
63 | *
64 | * A SocketFactory keep statistics on connections to not exceed the imposed limits on the factory.
65 | * The stats object can be shared between multiple SocketFactory instances so that they share the same rations.
66 | *
67 | * Although a client SocketFactory will at most initiate a single client socket (making the difference to directly
68 | * using a client socket small) the relevant difference is a unified interface for client and server and also
69 | * importantly that a client SocketFactory can share stats within and with other factories to avoid redundant connections.
70 | */
71 | export class SocketFactory implements SocketFactoryInterface {
72 | protected config: SocketFactoryConfig;
73 | protected stats: SocketFactoryStats;
74 | protected handlers: {[type: string]: ((data?: any) => void)[]};
75 | protected serverClientSockets: ClientInterface[];
76 | protected serverSocket?: Server;
77 | protected clientSocket?: ClientInterface;
78 | protected _isClosed: boolean;
79 | protected _isShutdown: boolean;
80 |
81 | /**
82 | * @param config provided client or server config.
83 | * The config object can afterwards be directly manipulated on to
84 | * change behaviors.such as maxConnections and denylist.
85 | * @param stats provide this to share stats with other factories.
86 | */
87 | constructor(config: SocketFactoryConfig, stats?: SocketFactoryStats) {
88 | this.config = config;
89 | this.stats = stats ?? {counters: {}};
90 | this.handlers = {};
91 | this.serverClientSockets = [];
92 | this._isClosed = false;
93 | this._isShutdown = false;
94 | }
95 |
96 | /**
97 | * For client factories initiate client connection.
98 | * For server factories setup server socket.
99 | * A factory can be both client and server.
100 | * Idempotent function, can be called again if client or server has been added,
101 | * or to manually reconnect a client if reconnectDelay was not set.
102 | */
103 | public init() {
104 | if (this.config.server) {
105 | this.openServer();
106 | }
107 |
108 | if (this.config.client) {
109 | this.connectClient();
110 | }
111 | }
112 |
113 | public getSocketFactoryConfig(): SocketFactoryConfig {
114 | return this.config;
115 | }
116 |
117 | /**
118 | * Initiate the client socket.
119 | */
120 | protected connectClient() {
121 | if (this.clientSocket || this._isClosed || this._isShutdown || !this.config.client) {
122 | return;
123 | }
124 |
125 | if (this.checkConnectionsOverflow(this.config.client.clientOptions.host ?? "localhost")) {
126 | if (this.config.client?.reconnectDelay ?? 0 > 0) {
127 | const delay = this.config.client!.reconnectDelay! * 1000;
128 | setTimeout( () => this.connectClient(), delay);
129 | }
130 | return;
131 | }
132 |
133 | try {
134 | this.clientSocket = this.createClientSocket();
135 | }
136 | catch(error) {
137 | const clientInitErrorEvent: Parameters = [error as Error];
138 |
139 | this.triggerEvent(EVENT_SOCKETFACTORY_CLIENT_INIT_ERROR, ...clientInitErrorEvent);
140 |
141 | const errorEvent: Parameters =
142 | [EVENT_SOCKETFACTORY_CLIENT_INIT_ERROR, error as Error];
143 |
144 | this.triggerEvent(EVENT_SOCKETFACTORY_ERROR, ...errorEvent);
145 |
146 | return;
147 | }
148 |
149 | this.initClientSocket();
150 | }
151 |
152 | /**
153 | * @throws
154 | */
155 | protected createClientSocket(): ClientInterface {
156 | if (this.config.client?.socketType === SOCKET_WEBSOCKET) {
157 | return new WSClient(this.config.client.clientOptions);
158 | }
159 | else if (this.config.client?.socketType === SOCKET_TCP) {
160 | if (!TCPClient) {
161 | throw new Error("TCPClient class not available.");
162 | }
163 | return new TCPClient(this.config.client.clientOptions);
164 | }
165 | throw new Error("Misconfiguration");
166 | }
167 |
168 | protected initClientSocket() {
169 | const socket = this.clientSocket;
170 | if (!socket) {
171 | return;
172 | }
173 |
174 | socket.onError( (errorMessage: string) => {
175 | if (socket === this.clientSocket) {
176 | delete this.clientSocket;
177 | }
178 |
179 | const error = new Error(errorMessage);
180 |
181 | const clientConnectErrorEvent: Parameters = [error];
182 |
183 | this.triggerEvent(EVENT_SOCKETFACTORY_CLIENT_CONNECT_ERROR,
184 | ...clientConnectErrorEvent);
185 |
186 | const errorEvent: Parameters =
187 | [EVENT_SOCKETFACTORY_CLIENT_CONNECT_ERROR, error as Error];
188 |
189 | this.triggerEvent(EVENT_SOCKETFACTORY_ERROR, ...errorEvent);
190 |
191 | if (this.config.client?.reconnectDelay ?? 0 > 0) {
192 | const delay = this.config.client!.reconnectDelay! * 1000;
193 | setTimeout( () => this.connectClient(), delay);
194 | }
195 | });
196 | socket.onConnect( () => {
197 | if (!socket || !this.config.client) {
198 | return;
199 | }
200 |
201 | // Check the limits once more when connected since there
202 | // might have been accepted server connections happening.
203 | if (this.checkConnectionsOverflow(this.config.client.clientOptions.host ?? "localhost")) {
204 | delete this.clientSocket;
205 | socket.close();
206 | if (this.config.client?.reconnectDelay ?? 0 > 0) {
207 | const delay = this.config.client!.reconnectDelay! * 1000;
208 | setTimeout( () => this.connectClient(), delay);
209 | }
210 |
211 | return;
212 | }
213 | socket.onClose( (hadError: boolean) => {
214 | if (!this.config.client) {
215 | return;
216 | }
217 |
218 | if (socket === this.clientSocket) {
219 | delete this.clientSocket;
220 | }
221 |
222 | this.decreaseConnectionsCounter(this.config.client.clientOptions.host ?? "localhost");
223 |
224 | const closeEvent: Parameters = [socket, false, hadError];
225 |
226 | this.triggerEvent(EVENT_SOCKETFACTORY_CLOSE, ...closeEvent);
227 |
228 | if (this.config.client?.reconnectDelay ?? 0 > 0) {
229 | const delay = this.config.client!.reconnectDelay! * 1000;
230 | setTimeout( () => this.connectClient(), delay);
231 | }
232 | });
233 |
234 | this.increaseConnectionsCounter(this.config.client.clientOptions.host ?? "localhost");
235 |
236 | const connectEvent: Parameters = [socket, false];
237 |
238 | this.triggerEvent(EVENT_SOCKETFACTORY_CONNECT, ...connectEvent);
239 | });
240 | socket.connect();
241 | }
242 |
243 | protected openServer() {
244 | if (this._isClosed || this._isShutdown || !this.config.server || this.serverSocket) {
245 | return;
246 | }
247 |
248 | try {
249 | this.serverSocket = this.createServerSocket();
250 | }
251 | catch(error) {
252 | const serverInitErrorEvent: Parameters = [error as Error];
253 |
254 | this.triggerEvent(EVENT_SOCKETFACTORY_SERVER_INIT_ERROR, ...serverInitErrorEvent);
255 |
256 | const errorEvent: Parameters =
257 | [EVENT_SOCKETFACTORY_SERVER_INIT_ERROR, error as Error];
258 |
259 | this.triggerEvent(EVENT_SOCKETFACTORY_ERROR, ...errorEvent);
260 |
261 | return;
262 | }
263 |
264 | this.initServerSocket();
265 | }
266 |
267 | /**
268 | * @throws
269 | */
270 | protected createServerSocket(): Server {
271 | if (this.config.server?.socketType === SOCKET_WEBSOCKET) {
272 | if (!WSServer) {
273 | throw new Error("WSServer class is not available.");
274 | }
275 | return new WSServer(this.config.server.serverOptions);
276 | }
277 | else if (this.config.server?.socketType === SOCKET_TCP) {
278 | if (!TCPServer) {
279 | throw new Error("TCPServer class is not available.");
280 | }
281 | return new TCPServer(this.config.server.serverOptions);
282 | }
283 | throw new Error("Misconfiguration");
284 | }
285 |
286 | protected initServerSocket() {
287 | if (!this.serverSocket) {
288 | return;
289 | }
290 |
291 | this.serverSocket.onConnection( async (socket: ClientInterface) => {
292 | const clientIP = socket.getRemoteAddress();
293 |
294 | if (clientIP) {
295 | if (this.isDenied(clientIP)) {
296 | socket.close();
297 |
298 | const clientRefuseEvent: Parameters =
299 | [DETAIL_SOCKETFACTORY_CLIENT_REFUSE_ERROR_IP_DENIED, clientIP];
300 |
301 | this.triggerEvent(EVENT_SOCKETFACTORY_CLIENT_IP_REFUSE, ...clientRefuseEvent);
302 |
303 | return;
304 | }
305 |
306 | if (!this.isAllowed(clientIP)) {
307 | socket.close();
308 |
309 | const clientRefuseEvent: Parameters =
310 | [DETAIL_SOCKETFACTORY_CLIENT_REFUSE_ERROR_IP_NOT_ALLOWED, clientIP];
311 |
312 | this.triggerEvent(EVENT_SOCKETFACTORY_CLIENT_IP_REFUSE, ...clientRefuseEvent);
313 |
314 | return;
315 | }
316 |
317 | if (this.checkConnectionsOverflow(clientIP, true)) {
318 | socket.close();
319 |
320 | const clientRefuseEvent: Parameters =
321 | [DETAIL_SOCKETFACTORY_CLIENT_REFUSE_ERROR_IP_OVERFLOW, clientIP];
322 |
323 | this.triggerEvent(EVENT_SOCKETFACTORY_CLIENT_IP_REFUSE, ...clientRefuseEvent);
324 |
325 | return;
326 | }
327 |
328 | this.increaseConnectionsCounter(clientIP);
329 | }
330 | socket.onClose( (hadError: boolean) => {
331 | if (clientIP) {
332 | this.decreaseConnectionsCounter(clientIP);
333 | }
334 |
335 | this.serverClientSockets = this.serverClientSockets.filter( (socket2: ClientInterface) => {
336 | return socket !== socket2;
337 | });
338 |
339 | const closeEvent: Parameters = [socket, true, hadError];
340 |
341 | this.triggerEvent(EVENT_SOCKETFACTORY_CLOSE, ...closeEvent);
342 | });
343 |
344 | const connectEvent: Parameters = [socket, true];
345 |
346 | this.triggerEvent(EVENT_SOCKETFACTORY_CONNECT, ...connectEvent);
347 | });
348 | this.serverSocket.onError( (errorMessage: string) => {
349 | const error = new Error(errorMessage);
350 |
351 | const serverListenErrorEvent: Parameters = [error];
352 |
353 | this.triggerEvent(EVENT_SOCKETFACTORY_SERVER_LISTEN_ERROR,
354 | ...serverListenErrorEvent);
355 |
356 | const errorEvent: Parameters =
357 | [EVENT_SOCKETFACTORY_SERVER_LISTEN_ERROR, error as Error];
358 |
359 | this.triggerEvent(EVENT_SOCKETFACTORY_ERROR, ...errorEvent);
360 | });
361 | this.serverSocket.listen();
362 | }
363 |
364 | protected isDenied(key: string): boolean {
365 | const deniedIPs = this.config.server?.deniedIPs || [];
366 | return deniedIPs.includes(key.toLowerCase());
367 | }
368 |
369 | protected isAllowed(key: string): boolean {
370 | const allowedIPs = this.config.server?.allowedIPs;
371 | if (!allowedIPs) {
372 | return true;
373 | }
374 | return allowedIPs.includes(key.toLowerCase());
375 | }
376 |
377 | /**
378 | * Increase the counter connections per IP or hostname,
379 | * both for outgoing and incoming connections.
380 | * Note that only when using direct connections on IP address will this be precise,
381 | * when connecting to host names it might not be possible t group that together with
382 | * a connection coming back (since localAddress won't be same as hostname).
383 | */
384 | protected increaseConnectionsCounter(address: string) {
385 | this.increaseCounter(address);
386 | this.increaseCounter("*"); // "all" counter, always increased.
387 | }
388 |
389 | protected decreaseConnectionsCounter(address: string) {
390 | this.decreaseCounter(address);
391 | this.decreaseCounter("*"); // Always decrease the "all" counter.
392 | }
393 |
394 | /**
395 | * @params key IP address or host name.
396 | * @params isServer set to true if it is server socket checking.
397 | * @returns true if any limit is reached.
398 | */
399 | protected checkConnectionsOverflow(address: string, isServer: boolean = false): boolean //eslint-disable-line @typescript-eslint/no-unused-vars
400 | {
401 | if (this.config.maxConnections !== undefined) {
402 | const allCount = this.readCounter("*");
403 | if (allCount >= this.config.maxConnections) {
404 | return true;
405 | }
406 | }
407 | if (this.config.maxConnectionsPerIp !== undefined) {
408 | const ipCount = this.readCounter(address);
409 | if (ipCount >= this.config.maxConnectionsPerIp) {
410 | return true;
411 | }
412 | }
413 | return false;
414 | }
415 |
416 | protected increaseCounter(key: string) {
417 | const obj = this.stats.counters[key] || {counter: 0};
418 | this.stats.counters[key] = obj;
419 | obj.counter++;
420 | }
421 |
422 | protected decreaseCounter(key: string) {
423 | const obj = this.stats.counters[key];
424 | if (!obj) {
425 | return;
426 | }
427 | obj.counter--;
428 | }
429 |
430 | protected readCounter(key: string): number {
431 | return (this.stats.counters[key] ?? {}).counter ?? 0;
432 | }
433 |
434 | /**
435 | * Shutdown client and server factories and close all open sockets.
436 | */
437 | public close() {
438 | if (this._isClosed) {
439 | return;
440 | }
441 | this.shutdown();
442 | this._isClosed = true;
443 | this.serverClientSockets.forEach( (client: ClientInterface) => {
444 | client.close();
445 | });
446 | this.serverClientSockets = [];
447 | this.clientSocket?.close();
448 | delete this.clientSocket;
449 | }
450 |
451 | /**
452 | * For client factories stop any further connection attempts, but leave existing connection open.
453 | * For server factories close the server socket to not allow any further connections,
454 | * but leave the existing ones open.
455 | */
456 | public shutdown() {
457 | if (this._isClosed || this._isShutdown) {
458 | return;
459 | }
460 | this._isShutdown = true;
461 | if (this.serverSocket) {
462 | this.serverSocket.close(true); // Only close server, will not close accepted sockets.
463 | delete this.serverSocket;
464 | }
465 | }
466 |
467 | /**
468 | * @returns true if factory is closed.
469 | */
470 | public isClosed(): boolean {
471 | return this._isClosed;
472 | }
473 |
474 | /**
475 | * @returns true if factory is shutdown.
476 | */
477 | public isShutdown(): boolean {
478 | return this._isShutdown;
479 | }
480 |
481 | /**
482 | * Get the stats object to be shared with other factories.
483 | * @returns stats
484 | */
485 | public getStats(): SocketFactoryStats {
486 | return this.stats;
487 | }
488 |
489 | /**
490 | * On any error also this general error event is emitted.
491 | * @param callback
492 | */
493 | public onSocketFactoryError(callback: SocketFactoryErrorCallback) {
494 | this.hookEvent(EVENT_SOCKETFACTORY_ERROR, callback);
495 | }
496 |
497 | /**
498 | * When the server socket cannot be created.
499 | * @param callback
500 | */
501 | public onServerInitError(callback: SocketFactoryServerInitErrorCallback) {
502 | this.hookEvent(EVENT_SOCKETFACTORY_SERVER_INIT_ERROR, callback);
503 | }
504 |
505 | /**
506 | * When the server socket cannot bind and listen.
507 | * @param callback
508 | */
509 | public onServerListenError(callback: SocketFactoryServerListenErrorCallback) {
510 | this.hookEvent(EVENT_SOCKETFACTORY_SERVER_LISTEN_ERROR, callback);
511 | }
512 |
513 | /**
514 | * When the client socket cannot be successfully created,
515 | * likely due to some misconfiguration.
516 | * @param callback
517 | */
518 | public onClientInitError(callback: SocketFactoryClientInitErrorCallback) {
519 | this.hookEvent(EVENT_SOCKETFACTORY_CLIENT_INIT_ERROR, callback);
520 | }
521 |
522 | /**
523 | * When the client socket cannot connect due to server not there.
524 | * @param callback
525 | */
526 | public onConnectError(callback: SocketFactoryClientConnectErrorCallback) {
527 | this.hookEvent(EVENT_SOCKETFACTORY_CLIENT_CONNECT_ERROR, callback);
528 | }
529 |
530 | /**
531 | * When client or server accepted socket has connected.
532 | * @param callback
533 | */
534 | public onConnect(callback: SocketFactoryConnectCallback) {
535 | this.hookEvent(EVENT_SOCKETFACTORY_CONNECT, callback);
536 | }
537 |
538 | /**
539 | * When a client or a server accepted socket is disconnected.
540 | * One could also directly hook onClose on the connected socket.
541 | * @param callback
542 | */
543 | public onClose(callback: SocketFactoryCloseCallback) {
544 | this.hookEvent(EVENT_SOCKETFACTORY_CLOSE, callback);
545 | }
546 |
547 | /**
548 | * When a server is refusing and closing an incoming socket due to the client IP address.
549 | * @param callback
550 | */
551 | public onClientIPRefuse(callback: SocketFactoryClientIPRefuseCallback) {
552 | this.hookEvent(EVENT_SOCKETFACTORY_CLIENT_IP_REFUSE, callback);
553 | }
554 |
555 | protected hookEvent(type: string, callback: (...args: any[]) => void) {
556 | const cbs = this.handlers[type] || [];
557 | this.handlers[type] = cbs;
558 | cbs.push(callback);
559 | }
560 |
561 | protected unhookEvent(type: string, callback: (...args: any[]) => void) {
562 | const cbs = (this.handlers[type] || []).filter( (cb: (data?: any[]) => void) =>
563 | callback !== cb );
564 |
565 | this.handlers[type] = cbs;
566 | }
567 |
568 | protected triggerEvent(type: string, ...args: any[]) {
569 | const cbs = this.handlers[type] || [];
570 | cbs.forEach( callback => {
571 | callback(...args);
572 | });
573 | }
574 | }
575 |
--------------------------------------------------------------------------------