Skip to content

Commit 78bd738

Browse files
authored
Ssh port fowarding (#23)
* support ssh portfowrding * update README * enable to set config value from environment variable
1 parent 6dc9eae commit 78bd738

File tree

7 files changed

+172
-21
lines changed

7 files changed

+172
-21
lines changed

README.md

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,29 +60,79 @@ $ sql-language-server up --method stdio
6060
{
6161
"adapter": "mysql",
6262
"host": "localhost",
63-
"port": 3306,
63+
"port": 3307,
6464
"user": "username",
6565
"password": "password",
66-
"database": "mysql-development"
66+
"database": "mysql-development",
67+
"ssh": {
68+
"user": "ubuntu",
69+
"remoteHost": "ec2-xxx-xxx-xxx-xxx.ap-southeast-1.compute.amazonaws.com",
70+
"dbHost": "127.0.0.1",
71+
"port": 3306,
72+
"identityFile": "~/.ssh/id_rsa",
73+
"passphrase": "123456"
74+
}
6775
}
6876
```
6977

70-
- Details
71-
- adapter: "mysql" | "postgres"
72-
- host: string
73-
- port: number
74-
- user: string
75-
- password: string
76-
- database: string
77-
7878
Please restart sql-language-server process after create .sqlrc.json.
7979

80+
#### Parameters
81+
82+
| Key | Description | value | required | default |
83+
| -------- | --------------------------- | ----------------------- | -------- | --------------------------------- |
84+
| adapter | Database type | `"mysql" | "postgres"` | true | |
85+
| host | Database host | string | true | |
86+
| port | Database port | string | false | mysql:3306, postgres:5432 |
87+
| user | Database user | string | true | mysql:"root", postgres:"postgres" |
88+
| password | Database password | string | false | |
89+
| database | Database name | string | false | |
90+
| ssh | Settings for port fowarding | \*see below SSH section | false | |
91+
92+
##### SSH
93+
94+
| Key | Description | value | required | default |
95+
| ------------ | ---------------------------------------- | ------ | -------- | ------------------------- |
96+
| remoteHost | The host address you want to connect to | string | true | |
97+
| remotePort | Port number of the server for ssh | number | false | 22 |
98+
| user | User name on the server | string | false | |
99+
| dbHost | Database host on the server | string | false | 127.0.0.1 |
100+
| dbPort | Databse port on the server | number | false | mysql:3306, postgres:5432 |
101+
| identitiFile | Identity file for ssh | string | false | ~/.ssh/config/id_rsa |
102+
| passphrase | Passphrase to allow to use identity file | string | false | |
103+
104+
#### Inject envitonment variables
105+
106+
${ssm:VARIABLE_NAME} syntax allows you to replace configuration value with environt variable.
107+
This is useful when you don't write actual file on configuration file.
108+
109+
##### example
110+
111+
```json
112+
{
113+
"adapter": "mysql",
114+
"host": "localhost",
115+
"port": 3307,
116+
"user": "username",
117+
"password": "${env:DB_PASSWORD}",
118+
"database": "mysql-development",
119+
"ssh": {
120+
"user": "ubuntu",
121+
"remoteHost": "ec2-xxx-xxx-xxx-xxx.ap-southeast-1.compute.amazonaws.com",
122+
"dbHost": "127.0.0.1",
123+
"port": 3306,
124+
"identityFile": "~/.ssh/id_rsa",
125+
"passphrase": "${env:SSH_PASSPHRASE}"
126+
}
127+
}
128+
```
129+
80130
### TODO
81131

82132
- [x] SELECT
83133
- [x] INSERT
84134
- [x] UPDATE
85135
- [x] DELETE
136+
- [x] ssh port forwarding
86137
- [ ] Beautify
87138
- [ ] Lint
88-
- [ ] ssh port forwarding

packages/server/SettingStore.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,23 @@ import EventEmitter from 'events'
44

55
const logger = log4js.getLogger()
66

7+
export type SSHConfig = {
8+
remoteHost: string
9+
remotePort?: number
10+
dbHost?: string
11+
dbPort?: number
12+
user?: string
13+
passphrase?: string
14+
identityFile?: string
15+
}
716
export type Settings = {
817
adapter: 'mysql' | 'postgresql',
918
host: string | null,
1019
port: number | null,
1120
user: string | null,
1221
database: string | null,
13-
password: string | null
22+
password: string | null,
23+
ssh: SSHConfig | null
1424
}
1525

1626
export default class SettingStore extends EventEmitter {
@@ -20,7 +30,8 @@ export default class SettingStore extends EventEmitter {
2030
port: null,
2131
user: null,
2232
database: null,
23-
password: null
33+
password: null,
34+
ssh: null
2435
}
2536
private static instance: SettingStore;
2637

@@ -68,7 +79,22 @@ export default class SettingStore extends EventEmitter {
6879
}
6980

7081
setSetting(setting: Partial<Settings>) {
71-
this.state = Object.assign({}, this.state, setting)
82+
const replaceEnv = (v: { [key: string]: any }) => {
83+
for (const k in v) {
84+
if (v[k] && typeof v[k] === 'object') {
85+
replaceEnv(v[k])
86+
} else if (typeof v[k] === 'string') {
87+
const matched = (v[k] as string).match(/\${env:(.*?)}/)
88+
if (matched) {
89+
v[k] = (v[k] as string).replace(`\${env:${matched[1]}}`, process.env[matched[1]] || '')
90+
}
91+
}
92+
}
93+
}
94+
const newSetting = Object.assign({}, setting)
95+
newSetting.ssh = newSetting.ssh ? Object.assign({}, newSetting.ssh) : null
96+
replaceEnv(newSetting)
97+
this.state = Object.assign({}, this.state, newSetting)
7298
this.emit('change', this.state)
7399
}
74100
}

packages/server/database_libs/AbstractClient.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Settings } from '../SettingStore'
22
import log4js from 'log4js';
3+
import { SSHConnection } from 'node-ssh-forward'
4+
import { readFileSync } from 'fs'
35

46
const logger = log4js.getLogger()
57

@@ -29,11 +31,34 @@ export default abstract class AbstractClient {
2931
abstract disconnect(): void
3032
abstract getTables(): Promise<string[]>
3133
abstract getColumns(tableName: string): Promise<RawField[]>
34+
abstract DefaultPort: number
35+
abstract DefaultHost: string
36+
abstract DefaultUser: string
3237

3338
async getSchema(): Promise<Schema> {
3439
let schema: Schema = []
40+
const sshConnection =
41+
this.settings.ssh ? new SSHConnection({
42+
endHost: this.settings.ssh.remoteHost,
43+
username: this.settings.ssh.user,
44+
privateKey: readFileSync(this.settings.ssh.identityFile || `${process.env.HOME}/.ssh/id_rsa`),
45+
passphrase: this.settings.ssh.passphrase || ''
46+
}) : null
47+
if (sshConnection) {
48+
await sshConnection.forward({
49+
fromPort: this.settings.port || this.DefaultPort,
50+
toPort: this.settings.ssh?.dbPort || this.DefaultPort,
51+
toHost: this.settings.ssh?.dbHost || '127.0.0.1'
52+
}).then(v => {
53+
if (v) {
54+
logger.error('Failed to ssh remote server')
55+
logger.error(v)
56+
}
57+
return []
58+
})
59+
}
60+
this.connect()
3561
try {
36-
this.connect()
3762
const tables = await this.getTables()
3863
schema = await Promise.all(
3964
tables.map((v) => this.getColumns(v).then(columns => ({
@@ -42,11 +67,14 @@ export default abstract class AbstractClient {
4267
columns: columns.map(v => this.toColumnFromRawField(v)) }
4368
)))
4469
)
45-
this.disconnect()
4670
} catch (e) {
47-
this.disconnect()
4871
logger.error(e)
4972
throw e
73+
} finally {
74+
this.disconnect()
75+
if (sshConnection) {
76+
sshConnection.shutdown()
77+
}
5078
}
5179
return schema
5280
}

packages/server/database_libs/MysqlClient.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ export default class MysqlClient extends AbstractClient {
99
super(settings)
1010
}
1111

12+
get DefaultPort() { return 3306 }
13+
get DefaultHost() { return '127.0.0.1' }
14+
get DefaultUser() { return 'root' }
15+
1216
connect() {
1317
this.connection = mysql.createConnection({
14-
host: this.settings.host || 'localhost',
18+
host: this.settings.host || this.DefaultHost,
1519
password: this.settings.password || '',
16-
user: this.settings.user || 'root',
17-
port: this.settings.port || 3306,
18-
database: this.settings.database || undefined
20+
user: this.settings.user || this.DefaultUser,
21+
port: this.settings.port || this.DefaultPort,
22+
database: this.settings.database || ''
1923
})
2024
}
2125

packages/server/database_libs/PostgresClient.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export default class PosgresClient extends AbstractClient {
99
super(settings)
1010
}
1111

12+
get DefaultPort() { return 5432 }
13+
get DefaultHost() { return '127.0.0.1' }
14+
get DefaultUser() { return 'postgres' }
15+
1216
connect() {
1317
const client: PG.Client = new PG.Client({
1418
user: this.settings.user || '',

packages/server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"jest": "^26.0.1",
2323
"log4js": "^6.2.1",
2424
"mysql": "^2.15.0",
25+
"node-ssh-forward": "^0.6.3",
2526
"pg": "^7.4.3",
2627
"vscode-languageclient": "^6.1.3",
2728
"vscode-languageserver": "^4.1.2",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import SettingStore from "../SettingStore"
2+
3+
describe('setSetting', () => {
4+
beforeAll(() => {
5+
process.env.setSetting_DB_PASSWORD="replacedPassWord"
6+
process.env.setSetting_SSH_PASSPHRASE="replacedPassphrase"
7+
})
8+
afterAll(() => {
9+
process.env.setSetting_DB_PASSWORD=""
10+
process.env.setSetting_SSH_PASSPHRASE=""
11+
})
12+
it('should replace ${env:VARIABLE_NAME} syntax with environment variable', () => {
13+
const setting = {
14+
"adapter": 'mysql' as 'mysql',
15+
"host": "localhost",
16+
"port": 3307,
17+
"user": "username",
18+
"password": "${env:setSetting_DB_PASSWORD}",
19+
"database": "mysql-development",
20+
"ssh": {
21+
"user": "ubuntu",
22+
"remoteHost": "ec2-xxx-xxx-xxx-xxx.ap-southeast-1.compute.amazonaws.com",
23+
"dbHost": "127.0.0.1",
24+
"port": 3306,
25+
"identityFile": "~/.ssh/id_rsa",
26+
"passphrase": "${env:setSetting_SSH_PASSPHRASE}"
27+
}
28+
}
29+
const store = SettingStore.getInstance()
30+
store.setSetting(setting)
31+
expect(store.getSetting()).toMatchObject({
32+
"password": "replacedPassWord",
33+
"ssh": {
34+
"passphrase": "replacedPassphrase"
35+
}
36+
})
37+
})
38+
})

0 commit comments

Comments
 (0)