Skip to content

Commit

Permalink
api and documentation optimized
Browse files Browse the repository at this point in the history
  • Loading branch information
Leonewu committed Dec 24, 2022
1 parent 227c09c commit 6093ade
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 71 deletions.
215 changes: 158 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
# Ssh tunneling for nodejs

A ssh tunneling written in nodejs.
A ssh tunneling written in nodejs, which can do command executing and port forwarding.

## install
## Installation

```sh
npm i ssh-tunneling
```

## features
## Features

-***auto reconnect***: A ssh client which can always reconnect automatically by client side.
-***port forward***: Another mainly capacity is ssh tunnel port forwarding even behind a hopping server,such as a socks server.
-***port checking and finding***: If local port is used, the client will find a available local port to proxy.
-***command executing***: Execute any linux command.
-***connection keep-alive***: Keeping the ssh connection alive whenever you use it.
-***port forwarding***: Ssh tunneling port forward even behind a hopping server,such as a socks server.
-***port checking and finding***: The client will automatically find a available local port to forward when the port is using.
-***command executing***: Execute linux commands.

### examples
## Api reference

#### simple ssh port forwarding
### `new SshTunel(config)`

An example that fowarding port 3000 to 192.168.1.1:3000 through a ssh tunnel.
The original ssh command is `ssh -L 3000:192.168.1.1:3000 -i ~/.ssh/myPrivateKey [email protected]`
options

- host: [**required**] ssh ip
- port: [**required**] ssh port
- username: [**required**] ssh user name
- privateKey: [**required**] ssh private key
- hoppingServer: [**optional**] Currently it just supports the socks4 or socks5 server,such as 'socks5://180.80.80.80:1080' or 'socks4://180.80.80.80:1080'

```typescript
import { SshTunnel } from 'ssh-tunneling';
Expand All @@ -31,23 +36,27 @@ const sshConfig = {
username: 'myUsername',
privateKey: fs.readFileSync('~/.ssh/myPrivateKey'),
};
const sshTunnel = new SshTunnel(sshConfig);
const result = await sshTunnel.proxy('3000:192.168.1.1:3000');
// { localPort: 3000, destHost: '192.168.1.1', destPort: 3000, key: '3000:192.168.1.1:3000' }
// or multiple port fowarding if passing an array
const multiResult = await sshTunnel.proxy(['3000:192.168.1.1:3000', '3001:192.168.1.1:3001']);
// [
// { localPort: 3001, destHost: '192.168.1.1', destPort: 3000, key: '3000:192.168.1.1:3000' },
// { localPort: 3002, destHost: '192.168.1.1', destPort: 3001, key: '3001:192.168.1.1:3001' },
// ]
// And it will auto find a idle local port if the port pass in is using.
const client = new client(sshConfig);
```

or establish a connection behind a socks5 server

```typescript
import { SshTunnel } from 'ssh-tunneling';

const sshConfig = {
host: '192.168.1.1',
port: 22,
username: 'myUsername',
privateKey: fs.readFileSync('~/.ssh/myPrivateKey'),
hoppingServer: 'socks://180.80.80.80:1080'
};
const client = new client(sshConfig);
```

#### ssh port forwarding through a socks5 server
### forwardOut

An example that fowarding port 3000 to 192.168.1.1:3000 through a ssh tunnel which only can be connect through a sock5 server.
The original ssh command is `ssh -o ProxyCommand="nc -X 5 -x 180.80.80.80:1080 %h %p" -L 3000:192.168.1.1:3000 -i ~/.ssh/myPrivateKey [email protected]`
Forward local port to remote port.

```typescript
import { SshTunnel } from 'ssh-tunneling';
Expand All @@ -57,23 +66,14 @@ const sshConfig = {
port: 22,
username: 'myUsername',
privateKey: fs.readFileSync('~/.ssh/myPrivateKey'),
socksServer: 'socks5://180.80.80.80:1080',
};
const sshTunnel = new SshTunnel(sshConfig);
const result = await sshTunnel.proxy('3000:192.168.1.1:3000');
// { localPort: 3000, destHost: '192.168.1.1', destPort: 3000, key: '3000:192.168.1.1:3000' }
// or multiple port fowarding if passing an array
const multiResult = await sshTunnel.proxy(['3000:192.168.1.1:3000', '3001:192.168.1.1:3001']);
// [
// { localPort: 3001, destHost: '192.168.1.1', destPort: 3000, key: '3000:192.168.1.1:3000' },
// { localPort: 3002, destHost: '192.168.1.1', destPort: 3001, key: '3001:192.168.1.1:3001' },
// ]
// And it will auto find a idle local port if the port pass in is using.
const client = new client(sshConfig);
const forwardInfo = client.forwardOut('3000:192.168.1.1:3000');
console.log(forwardInfo);
// { localPort: 3000, destPort: 3000, destHost: '192.168.1.1' key: '3000:192.168.1.1:3000' }
```

### command executing

Also, you can execute any command through the ssh client
If the local port is occupied, it will choose a idle local port to listen and return the info in result.

```typescript
import { SshTunnel } from 'ssh-tunneling';
Expand All @@ -84,15 +84,40 @@ const sshConfig = {
username: 'myUsername',
privateKey: fs.readFileSync('~/.ssh/myPrivateKey'),
};
const sshTunnel = new SshTunnel(sshConfig);
const result = await sshTunnel.exec('uptime');
const client = new client(sshConfig);
const forwardInfo1 = client.forwardOut('3000:192.168.1.1:3000');
const forwardInfo2 = client.forwardOut('3000:192.168.1.1:3000');
console.log(forwardInfo1);
// port 3000 is idle
// { localPort: 3000, destPort: 3000, destHost: '192.168.1.1' key: '3000:192.168.1.1:3000', type: 'out' }
console.log(forwardInfo2);
// port 3000 is using, so it use another idle port 3001
// { localPort: 3001, destPort: 3000, destHost: '192.168.1.1' key: '3000:192.168.1.1:3000', type: 'out' }

```

## instance api
Also, you can pass an array to forward multiple port and it will return the result array too.

- `proxy`
- `exec`
- `close`
```typescript
import { SshTunnel } from 'ssh-tunneling';

const sshConfig = {
host: '192.168.1.1',
port: 22,
username: 'myUsername',
privateKey: fs.readFileSync('~/.ssh/myPrivateKey'),
};
const client = new client(sshConfig);
const forwardInfo = client.forwardOut(['3000:192.168.1.1:3000', '3001:192.168.1.1:3001']);
console.log(forwardInfo);
// output
// [
// { localPort: 3000, destPort: 3000, destHost: '192.168.1.1' key: '3000:192.168.1.1:3000', type: 'out' },
// { localPort: 3001, destPort: 3001, destHost: '192.168.1.1' key: '3001:192.168.1.1:3001', type: 'out' },
// ]
```

### `exec`

```typescript
import { SshTunnel } from 'ssh-tunneling';
Expand All @@ -103,24 +128,100 @@ const sshConfig = {
username: 'myUsername',
privateKey: fs.readFileSync('~/.ssh/myPrivateKey'),
};
const sshTunnel = new SshTunnel(sshConfig);
const client = new SshTunnel(sshConfig);
// execute echo command
const execRes = await sshTunnel.exec('echo 1');
// execRes: '1'
const echo = await client.exec('echo 1');
console.log(echo);
// 1
// Also, if passing a command array, it will execute every commands one time and return by order
const batchRes = await sshTunnel.exec([
const batchEcho = await sshTunnel.exec([
'echo 1',
'echo 2',
'echo 3'
]);
// batchRes: [{ command: 'echo 1', result: '1' }, { command: 'echo 2', result: '2' }, { command: 'echo 3', result: '3' }]
// forward local port 3000 to 192.168.1.1:3000
const proxyRes = sshTunnel.proxy('3000:192.168.1.1:3000');
// forward multiple port to specific servers
const proxyResList = sshTunnel.proxy(['3000:192.168.1.1:3000', '3001:192.168.1.1:3001']);
// or just close one port forwarding server
sshTunnel.close(proxyRes.key);
// if you don't need ssh tunnel and all the port forwarding server, pass empty params to close it all
sshTunnel.close();
// batchEcho: [{ command: 'echo 1', result: '1' }, { command: 'echo 2', result: '2' }, { command: 'echo 3', result: '3' }]

```

### `close`

Since the ssh connection is established, it can be closed manualy.

- close one server `close(serverKey)`
- close all server `close()`

```typescript
import { SshTunnel } from 'ssh-tunneling';

const sshConfig = {
host: '192.168.1.1',
port: 22,
username: 'myUsername',
privateKey: fs.readFileSync('~/.ssh/myPrivateKey'),
};
const client = new SshTunnel(sshConfig);
// execute echo command
const echo = await client.exec('echo 1');
const forwardInfo = client.forwardOut(['3000:192.168.1.1:3000', '3001:192.168.1.1:3001']);
// close one proxy server
client.close(forwardInfo[0].key);
// close all proxy server
client.close();
```

## Examples

### Simple ssh port forwarding out

An example that fowarding port 3000 to 192.168.1.1:3000 through a ssh tunnel.
The original ssh command is `ssh -L 3000:192.168.1.1:3000 -i ~/.ssh/myPrivateKey [email protected]`

```typescript
import { SshTunnel } from 'ssh-tunneling';

const sshConfig = {
host: '192.168.1.1',
port: 22,
username: 'myUsername',
privateKey: fs.readFileSync('~/.ssh/myPrivateKey'),
};
const client = new SshTunnel(sshConfig);
const forwardInfo = await client.forwardOut('3000:192.168.1.1:3000');
// { localPort: 3000, destHost: '192.168.1.1', destPort: 3000, key: '3000:192.168.1.1:3000', type: 'out' }
```

### Ssh port forwarding through a socks5 server

An example that fowarding port 3000 to 192.168.1.1:3000 through a ssh tunnel which only can be connect through a sock5 server.
The original ssh command is `ssh -o ProxyCommand="nc -X 5 -x 180.80.80.80:1080 %h %p" -L 3000:192.168.1.1:3000 -i ~/.ssh/myPrivateKey [email protected]`

```typescript
import { SshTunnel } from 'ssh-tunneling';

const sshConfig = {
host: '192.168.1.1',
port: 22,
username: 'myUsername',
privateKey: fs.readFileSync('~/.ssh/myPrivateKey'),
hoppingServer: 'socks5://180.80.80.80:1080',
};
const client = new SshTunnel(sshConfig);
const forwardIngo = await client.forwardOut('3000:192.168.1.1:3000');
// { localPort: 3000, destHost: '192.168.1.1', destPort: 3000, key: '3000:192.168.1.1:3000', type: 'out' }
```

### Commands executing

```typescript
import { SshTunnel } from 'ssh-tunneling';

const sshConfig = {
host: '192.168.1.1',
port: 22,
username: 'myUsername',
privateKey: fs.readFileSync('~/.ssh/myPrivateKey'),
};
const client = new SshTunnel(sshConfig);
const result = await client.exec('echo 1');
// 1
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": false,
"name": "ssh-tunneling",
"version": "1.0.9",
"version": "1.1.0",
"description": "a ssh-tunneling client for nodejs",
"keywords": ["ssh tunnel", "ssh tunneling", "ssh proxy", "ssh port forward", "ssh shell exec", "ssh keep-alive"],
"main": "dist/index.js",
Expand Down
28 changes: 15 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ class SshTunnel {
* @description socks hopping server for ssh connection
* @example socks5:180.80.80.80:1080
*/
socksServer?: string
hoppingServer?: string
}) {
const { socksServer, ...restConfig } = sshConfig;
if (socksServer) {
const { hoppingServer, ...restConfig } = sshConfig;
if (hoppingServer) {
// 初始化 socks 配置
// socks5://180.80.80.80:1080
const socksReg = /socks(\d):\/\/([\d.]+):(\d+)/;
const [, hoppingSocksType, hoppingIp, hoppingPort] =
socksReg.exec(socksServer) || [];
socksReg.exec(hoppingServer) || [];
if (!hoppingIp || !hoppingPort || !hoppingSocksType) {
throw new Error('socks服务配置错误');
}
Expand Down Expand Up @@ -63,6 +63,7 @@ class SshTunnel {
destPort: number;
key?: string | number;
server: net.Server;
type: 'out' | 'in';
}[] = [];

private socksSocket?: net.Socket;
Expand Down Expand Up @@ -237,8 +238,8 @@ class SshTunnel {
public async exec(command: any): Promise<any> {
if (Array.isArray(command)) {
const divider = '__ssh_tunneling_divider__'
const combinedCommand = command.join(` && echo ${divider} && `);
const res = (await this._exec(combinedCommand)).split(`${divider}\n`);
const combinedCommand = command.join(` && echo -n ${divider} && `);
const res = (await this._exec(combinedCommand)).split(divider);
return command.map((item, i) => {
return {
command: item,
Expand Down Expand Up @@ -310,7 +311,7 @@ class SshTunnel {
return `ssh -o StrictHostKeyChecking=no -i ~/.ssh/${this.sshConfig.username} ${this.sshConfig.username}@${destHost} -L ${localPort}:${destHost}:${destPort}`;
}

private _proxy = async (proxyConfig: ProxyConfig) => {
private _forwardOut = async (proxyConfig: ProxyConfig) => {
const { localPort, destHost, destPort, key } = proxyConfig;
if (this.proxyList.find(item => item.localPort === localPort && item.server?.listening)) {
throw new Error(`localPort ${localPort} is proxying`);
Expand Down Expand Up @@ -382,7 +383,8 @@ class SshTunnel {
destHost,
destPort,
server,
key
key,
type: 'out'
});
logger.cyan(
`proxy server listening on 127.0.0.1:${localPort} => ${destHost}:${destPort}`,
Expand All @@ -395,16 +397,16 @@ class SshTunnel {
};


public proxy(proxyConfig: string): Promise<ProxyConfig>
public forwardOut(proxyConfig: string): Promise<ProxyConfig>

public proxy(proxyConfig: string[]): Promise<ProxyConfig[]>
public forwardOut(proxyConfig: string[]): Promise<ProxyConfig[]>

/**
* @description ssh port forwarding
* @expample proxy('3000:192.168.1.1:3000')
* @expample proxy(['3000:192.168.1.1:3000', '3001:192.168.1.1:3001'])
*/
public async proxy (proxyConfig: any): Promise<any> {
public async forwardOut (proxyConfig: any): Promise<any> {
if (Array.isArray(proxyConfig)) {
const result: ProxyConfig[] = [];
await proxyConfig.reduce((pre, config) => {
Expand All @@ -417,7 +419,7 @@ class SshTunnel {
destPort: Number(destPort),
key: config
}
await this._proxy(params);
await this._forwardOut(params);
result.push(params);
});
}, Promise.resolve());
Expand All @@ -432,7 +434,7 @@ class SshTunnel {
destPort: Number(destPort),
key: proxyConfig
}
await this._proxy(params);
await this._forwardOut(params);
return params;
}
throw new Error(`params ${proxyConfig} is invalid`);
Expand Down

0 comments on commit 6093ade

Please sign in to comment.