@tmlmobilidade/go-clients-ssh 20260828.1636.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +45 -0
- package/dist/client.js +92 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/ssh.factory.d.ts +26 -0
- package/dist/ssh.factory.js +80 -0
- package/dist/tunnels.d.ts +3 -0
- package/dist/tunnels.js +6 -0
- package/package.json +49 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type Server } from 'node:net';
|
|
2
|
+
import { type ForwardOptions, type ServerOptions, type SshOptions, type TunnelOptions } from 'tunnel-ssh';
|
|
3
|
+
export interface SshConfig {
|
|
4
|
+
forwardOptions: ForwardOptions;
|
|
5
|
+
serverOptions: ServerOptions;
|
|
6
|
+
sshOptions: SshOptions;
|
|
7
|
+
tunnelOptions: TunnelOptions;
|
|
8
|
+
}
|
|
9
|
+
export interface SshTunnelOptions {
|
|
10
|
+
maxRetries?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class SshTunnel {
|
|
13
|
+
private _server;
|
|
14
|
+
private config;
|
|
15
|
+
private onDisconnect?;
|
|
16
|
+
private options;
|
|
17
|
+
private retries;
|
|
18
|
+
constructor(config: SshConfig, options?: SshTunnelOptions, onDisconnect?: () => void);
|
|
19
|
+
get server(): Server | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Establishes an SSH tunnel connection using the provided configuration options.
|
|
22
|
+
* @throws Throws an error if the connection fails after the maximum number of retries.
|
|
23
|
+
* @remarks
|
|
24
|
+
* - The method attempts to create an SSH tunnel using the `createTunnel` function with the specified options.
|
|
25
|
+
* - If the connection is successful, it logs the connected host port and sets up an error listener on the server.
|
|
26
|
+
* - If the connection fails, it retries the connection up to a maximum number of retries specified in the options.
|
|
27
|
+
* @example ```typescript
|
|
28
|
+
* const SshTunnel = new SshTunnel(config);
|
|
29
|
+
* SshTunnel.connect();
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
connect(): any;
|
|
33
|
+
/**
|
|
34
|
+
* Disconnects the SSH tunnel by closing the server.
|
|
35
|
+
* @returns A promise that resolves when the server is successfully closed.
|
|
36
|
+
* @throws Will log an error message if the server fails to close.
|
|
37
|
+
*/
|
|
38
|
+
disconnect(): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Reconnects the SSH tunnel by first disconnecting and then connecting again.
|
|
41
|
+
* This method ensures that the connection is reset.
|
|
42
|
+
* @returns A promise that resolves when the reconnection process is complete.
|
|
43
|
+
*/
|
|
44
|
+
reconnect(): Promise<void>;
|
|
45
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { Logger } from '@tmlmobilidade/logger';
|
|
3
|
+
import { createTunnel } from 'tunnel-ssh';
|
|
4
|
+
/* * */
|
|
5
|
+
export class SshTunnel {
|
|
6
|
+
_server;
|
|
7
|
+
config;
|
|
8
|
+
onDisconnect;
|
|
9
|
+
options;
|
|
10
|
+
retries = 0;
|
|
11
|
+
constructor(config, options, onDisconnect) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.onDisconnect = onDisconnect;
|
|
14
|
+
if (options)
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
get server() {
|
|
18
|
+
return this._server;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Establishes an SSH tunnel connection using the provided configuration options.
|
|
22
|
+
* @throws Throws an error if the connection fails after the maximum number of retries.
|
|
23
|
+
* @remarks
|
|
24
|
+
* - The method attempts to create an SSH tunnel using the `createTunnel` function with the specified options.
|
|
25
|
+
* - If the connection is successful, it logs the connected host port and sets up an error listener on the server.
|
|
26
|
+
* - If the connection fails, it retries the connection up to a maximum number of retries specified in the options.
|
|
27
|
+
* @example ```typescript
|
|
28
|
+
* const SshTunnel = new SshTunnel(config);
|
|
29
|
+
* SshTunnel.connect();
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
async connect() {
|
|
33
|
+
try {
|
|
34
|
+
if (this._server) {
|
|
35
|
+
// If the server is already connected, return it
|
|
36
|
+
console.log(`⤷ SSH Tunnel already connected.`);
|
|
37
|
+
return this._server;
|
|
38
|
+
}
|
|
39
|
+
const [server] = await createTunnel(this.config.tunnelOptions, this.config.serverOptions, this.config.sshOptions, this.config.forwardOptions);
|
|
40
|
+
Logger.info({ message: `SSH Tunnel connected to host port ${server.address().port}` });
|
|
41
|
+
this._server = server;
|
|
42
|
+
server.on('error', (error) => {
|
|
43
|
+
Logger.error({ error, message: 'SSH Tunnel Error' });
|
|
44
|
+
});
|
|
45
|
+
server.on('close', () => {
|
|
46
|
+
Logger.info({ message: 'SSH Tunnel closed.' });
|
|
47
|
+
});
|
|
48
|
+
return this._server;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code === 'EADDRINUSE') {
|
|
52
|
+
Logger.info({ message: `Port "${this.config.serverOptions.port}" already in use. Retrying with a different port...` });
|
|
53
|
+
this.config.serverOptions.port++;
|
|
54
|
+
return await this.connect();
|
|
55
|
+
}
|
|
56
|
+
else if (this.retries < (this.options?.maxRetries || 3)) {
|
|
57
|
+
Logger.error({ error, message: 'Failed to connect to SSH Tunnel.' });
|
|
58
|
+
this.retries++;
|
|
59
|
+
Logger.info({ message: 'Retrying SSH connection...' });
|
|
60
|
+
return await this.connect();
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
throw new Error('Error connecting to SSH tunnel', { cause: error });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Disconnects the SSH tunnel by closing the server.
|
|
69
|
+
* @returns A promise that resolves when the server is successfully closed.
|
|
70
|
+
* @throws Will log an error message if the server fails to close.
|
|
71
|
+
*/
|
|
72
|
+
async disconnect() {
|
|
73
|
+
try {
|
|
74
|
+
this._server?.close();
|
|
75
|
+
this._server = undefined;
|
|
76
|
+
this.onDisconnect?.();
|
|
77
|
+
console.log(`⤷ SSH Tunnel disconnected.`);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.log(`⤷ ERROR: Failed to disconnect from SSH Tunnel.`, error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Reconnects the SSH tunnel by first disconnecting and then connecting again.
|
|
85
|
+
* This method ensures that the connection is reset.
|
|
86
|
+
* @returns A promise that resolves when the reconnection process is complete.
|
|
87
|
+
*/
|
|
88
|
+
async reconnect() {
|
|
89
|
+
await this.disconnect();
|
|
90
|
+
await this.connect();
|
|
91
|
+
}
|
|
92
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SshTunnel } from './client.js';
|
|
2
|
+
export type SshTunnelType = 'CP' | 'GO' | 'PCGI';
|
|
3
|
+
interface SshTunnelFactoryOptions {
|
|
4
|
+
dstAddr: string;
|
|
5
|
+
dstPort: number;
|
|
6
|
+
maxRetries?: number;
|
|
7
|
+
}
|
|
8
|
+
export type SshTunnelFactory = (options: SshTunnelFactoryOptions) => null | SshTunnel;
|
|
9
|
+
/**
|
|
10
|
+
* Creates an SSH tunnel factory for the given type.
|
|
11
|
+
*
|
|
12
|
+
* The returned function reads `{type}_TUNNEL_*` environment variables
|
|
13
|
+
* and builds an `SshTunnel` when tunneling is enabled.
|
|
14
|
+
*
|
|
15
|
+
* Expected environment variables:
|
|
16
|
+
* `{type}_TUNNEL_ENABLED` — `"true"` or `"false"`; `"false"` returns `null`
|
|
17
|
+
* `{type}_TUNNEL_SSH_HOST`
|
|
18
|
+
* `{type}_TUNNEL_SSH_USERNAME`
|
|
19
|
+
* `{type}_TUNNEL_SSH_KEY_PATH` (optional)
|
|
20
|
+
* `{type}_TUNNEL_SSH_KEY` (optional)
|
|
21
|
+
* `SSH_AUTH_SOCK` (optional fallback agent)
|
|
22
|
+
*
|
|
23
|
+
* Auth priority: `TUNNEL_SSH_KEY_PATH` > `TUNNEL_SSH_KEY` > `SSH_AUTH_SOCK`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createSshTunnelFactory(type: SshTunnelType): SshTunnelFactory;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { randomInt } from 'node:crypto';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { SshTunnel } from './client.js';
|
|
5
|
+
const tunnelCache = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Creates an SSH tunnel factory for the given type.
|
|
8
|
+
*
|
|
9
|
+
* The returned function reads `{type}_TUNNEL_*` environment variables
|
|
10
|
+
* and builds an `SshTunnel` when tunneling is enabled.
|
|
11
|
+
*
|
|
12
|
+
* Expected environment variables:
|
|
13
|
+
* `{type}_TUNNEL_ENABLED` — `"true"` or `"false"`; `"false"` returns `null`
|
|
14
|
+
* `{type}_TUNNEL_SSH_HOST`
|
|
15
|
+
* `{type}_TUNNEL_SSH_USERNAME`
|
|
16
|
+
* `{type}_TUNNEL_SSH_KEY_PATH` (optional)
|
|
17
|
+
* `{type}_TUNNEL_SSH_KEY` (optional)
|
|
18
|
+
* `SSH_AUTH_SOCK` (optional fallback agent)
|
|
19
|
+
*
|
|
20
|
+
* Auth priority: `TUNNEL_SSH_KEY_PATH` > `TUNNEL_SSH_KEY` > `SSH_AUTH_SOCK`.
|
|
21
|
+
*/
|
|
22
|
+
export function createSshTunnelFactory(type) {
|
|
23
|
+
return (options) => buildSshTunnel(type, options);
|
|
24
|
+
}
|
|
25
|
+
function buildSshTunnel(type, options) {
|
|
26
|
+
const { dstAddr, dstPort, maxRetries } = options;
|
|
27
|
+
const env = (name) => process.env[`${type}_${name}`];
|
|
28
|
+
if (env('TUNNEL_ENABLED') !== 'true') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (!env('TUNNEL_SSH_HOST')) {
|
|
32
|
+
throw new Error(`Missing ${type}_TUNNEL_SSH_HOST environment variable.`);
|
|
33
|
+
}
|
|
34
|
+
if (!env('TUNNEL_SSH_USERNAME')) {
|
|
35
|
+
throw new Error(`Missing ${type}_TUNNEL_SSH_USERNAME environment variable.`);
|
|
36
|
+
}
|
|
37
|
+
if (!env('TUNNEL_SSH_KEY_PATH') && !env('TUNNEL_SSH_KEY') && !process.env.SSH_AUTH_SOCK) {
|
|
38
|
+
throw new Error(`Missing authentication configuration. Please provide ${type}_TUNNEL_SSH_KEY_PATH, ${type}_TUNNEL_SSH_KEY, or ensure SSH_AUTH_SOCK is set.`);
|
|
39
|
+
}
|
|
40
|
+
const srcPort = randomInt(8_000, 8_999);
|
|
41
|
+
const sshConfig = {
|
|
42
|
+
forwardOptions: {
|
|
43
|
+
dstAddr: dstAddr,
|
|
44
|
+
dstPort: dstPort,
|
|
45
|
+
srcAddr: 'localhost',
|
|
46
|
+
srcPort: srcPort,
|
|
47
|
+
},
|
|
48
|
+
serverOptions: {
|
|
49
|
+
port: srcPort,
|
|
50
|
+
},
|
|
51
|
+
sshOptions: {
|
|
52
|
+
agent: (env('TUNNEL_SSH_KEY_PATH') || env('TUNNEL_SSH_KEY')) ? undefined : process.env.SSH_AUTH_SOCK,
|
|
53
|
+
host: env('TUNNEL_SSH_HOST'),
|
|
54
|
+
keepaliveCountMax: 3,
|
|
55
|
+
keepaliveInterval: 10_000,
|
|
56
|
+
port: 22,
|
|
57
|
+
privateKey: env('TUNNEL_SSH_KEY_PATH')
|
|
58
|
+
? readFileSync(env('TUNNEL_SSH_KEY_PATH'))
|
|
59
|
+
: env('TUNNEL_SSH_KEY')
|
|
60
|
+
? env('TUNNEL_SSH_KEY')
|
|
61
|
+
: undefined,
|
|
62
|
+
username: env('TUNNEL_SSH_USERNAME'),
|
|
63
|
+
},
|
|
64
|
+
tunnelOptions: {
|
|
65
|
+
autoClose: false,
|
|
66
|
+
reconnectOnError: true,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const sshOptions = {
|
|
70
|
+
maxRetries: maxRetries ?? 3,
|
|
71
|
+
};
|
|
72
|
+
const cacheKey = `${type}:${dstAddr}:${dstPort}`;
|
|
73
|
+
const cached = tunnelCache.get(cacheKey);
|
|
74
|
+
if (cached) {
|
|
75
|
+
return cached;
|
|
76
|
+
}
|
|
77
|
+
const tunnel = new SshTunnel(sshConfig, sshOptions, () => tunnelCache.delete(cacheKey));
|
|
78
|
+
tunnelCache.set(cacheKey, tunnel);
|
|
79
|
+
return tunnel;
|
|
80
|
+
}
|
package/dist/tunnels.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tmlmobilidade/go-clients-ssh",
|
|
3
|
+
"version": "20260828.1636.54",
|
|
4
|
+
"author": {
|
|
5
|
+
"email": "iso@tmlmobilidade.pt",
|
|
6
|
+
"name": "TML-ISO"
|
|
7
|
+
},
|
|
8
|
+
"license": "AGPL-3.0-or-later",
|
|
9
|
+
"homepage": "https://go.tmlmobilidade.pt",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/tmlmobilidade/go/issues"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/tmlmobilidade/go.git"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"public transit",
|
|
19
|
+
"tml",
|
|
20
|
+
"transportes metropolitanos de lisboa",
|
|
21
|
+
"go"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc && resolve-tspaths",
|
|
34
|
+
"lint": "eslint ./src/ && tsc --noEmit",
|
|
35
|
+
"lint:fix": "eslint ./src/ --fix",
|
|
36
|
+
"watch": "tsc-watch --onSuccess 'resolve-tspaths'"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@tmlmobilidade/logger": "*",
|
|
40
|
+
"tunnel-ssh": "5.2.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@tmlmobilidade/go-utils-tsconfig": "*",
|
|
44
|
+
"@types/node": "26.1.2",
|
|
45
|
+
"resolve-tspaths": "0.8.23",
|
|
46
|
+
"tsc-watch": "7.2.1",
|
|
47
|
+
"typescript": "6.0.3"
|
|
48
|
+
}
|
|
49
|
+
}
|