1 | 'use strict'
|
2 |
|
3 | const debug = require('./debug')
|
4 | const tunnelSSH = require('tunnel-ssh')
|
5 | const host = require('./host')
|
6 | const util = require('util')
|
7 | const { once, EventEmitter } = require('events')
|
8 | const createSSHTunnel = util.promisify(tunnelSSH)
|
9 |
|
10 | const getBastion = function (config, baseName) {
|
11 | const { sample } = require('lodash')
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | const bastionKey = config[`${baseName}_BASTION_KEY`]
|
22 | const bastionHost = sample((config[`${baseName}_BASTIONS`] || '').split(','))
|
23 | return (!(bastionKey && bastionHost))
|
24 | ? {}
|
25 | : { bastionHost, bastionKey }
|
26 | }
|
27 |
|
28 | exports.getBastion = getBastion
|
29 |
|
30 | const env = function (db) {
|
31 | let baseEnv = Object.assign({
|
32 | PGAPPNAME: 'psql non-interactive',
|
33 | PGSSLMODE: (!db.hostname || db.hostname === 'localhost') ? 'prefer' : 'require'
|
34 | }, process.env)
|
35 | let mapping = {
|
36 | PGUSER: 'user',
|
37 | PGPASSWORD: 'password',
|
38 | PGDATABASE: 'database',
|
39 | PGPORT: 'port',
|
40 | PGHOST: 'host'
|
41 | }
|
42 | Object.keys(mapping).forEach((envVar) => {
|
43 | let val = db[mapping[envVar]]
|
44 | if (val) {
|
45 | baseEnv[envVar] = val
|
46 | }
|
47 | })
|
48 | return baseEnv
|
49 | }
|
50 |
|
51 | exports.env = env
|
52 |
|
53 | function tunnelConfig (db) {
|
54 | const localHost = '127.0.0.1'
|
55 | const localPort = Math.floor(Math.random() * (65535 - 49152) + 49152)
|
56 | return {
|
57 | username: 'bastion',
|
58 | host: db.bastionHost,
|
59 | privateKey: db.bastionKey,
|
60 | dstHost: db.host,
|
61 | dstPort: db.port,
|
62 | localHost: localHost,
|
63 | localPort: localPort
|
64 | }
|
65 | }
|
66 |
|
67 | exports.tunnelConfig = tunnelConfig
|
68 |
|
69 | function getConfigs (db) {
|
70 | let dbEnv = env(db)
|
71 | const dbTunnelConfig = tunnelConfig(db)
|
72 | if (db.bastionKey) {
|
73 | dbEnv = Object.assign(dbEnv, {
|
74 | PGPORT: dbTunnelConfig.localPort,
|
75 | PGHOST: dbTunnelConfig.localHost
|
76 | })
|
77 | }
|
78 | return {
|
79 | dbEnv: dbEnv,
|
80 | dbTunnelConfig: dbTunnelConfig
|
81 | }
|
82 | }
|
83 |
|
84 | exports.getConfigs = getConfigs
|
85 |
|
86 | class Timeout {
|
87 | constructor (timeout, message) {
|
88 | this.timeout = timeout
|
89 | this.message = message;
|
90 | this.events = new EventEmitter()
|
91 | }
|
92 |
|
93 | async promise () {
|
94 | this.timer = setTimeout(() => {
|
95 | this.events.emit('error', new Error(this.message))
|
96 | }, this.timeout)
|
97 |
|
98 | try {
|
99 | await once(this.events, 'cancelled')
|
100 | } finally {
|
101 | clearTimeout(this.timer)
|
102 | }
|
103 | }
|
104 |
|
105 | cancel () {
|
106 | this.events.emit('cancelled')
|
107 | }
|
108 | }
|
109 |
|
110 | async function sshTunnel (db, dbTunnelConfig, timeout = 10000) {
|
111 | if (!db.bastionKey) {
|
112 | return null
|
113 | }
|
114 |
|
115 | const timeoutInstance = new Timeout(timeout, 'Establishing a secure tunnel timed out');
|
116 | try {
|
117 | const tunnelInstance = await Promise.race([
|
118 | timeoutInstance.promise(),
|
119 | createSSHTunnel(dbTunnelConfig)
|
120 | ])
|
121 | return tunnelInstance
|
122 | } catch (err) {
|
123 | debug(err)
|
124 | throw new Error('Unable to establish a secure tunnel to your database.')
|
125 | } finally {
|
126 | timeoutInstance.cancel()
|
127 | }
|
128 | }
|
129 |
|
130 | exports.sshTunnel = sshTunnel
|
131 |
|
132 | async function fetchConfig (heroku, db) {
|
133 | return heroku.get(
|
134 | `/client/v11/databases/${encodeURIComponent(db.id)}/bastion`,
|
135 | {
|
136 | host: host(db)
|
137 | }
|
138 | )
|
139 | }
|
140 |
|
141 | exports.fetchConfig = fetchConfig
|