UNPKG

3.35 kBJavaScriptView Raw
1'use strict'
2
3const debug = require('./debug')
4const tunnelSSH = require('tunnel-ssh')
5const host = require('./host')
6const util = require('util')
7const { once, EventEmitter } = require('events')
8const createSSHTunnel = util.promisify(tunnelSSH)
9
10const getBastion = function (config, baseName) {
11 const { sample } = require('lodash')
12 // If there are bastions, extract a host and a key
13 // otherwise, return an empty Object
14
15 // If there are bastions:
16 // * there should be one *_BASTION_KEY
17 // * pick one host from the comma-separated list in *_BASTIONS
18 // We assert that _BASTIONS and _BASTION_KEY always exist together
19 // If either is falsy, pretend neither exist
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
28exports.getBastion = getBastion
29
30const 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
51exports.env = env
52
53function 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
67exports.tunnelConfig = tunnelConfig
68
69function 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
84exports.getConfigs = getConfigs
85
86class 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
110async 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
130exports.sshTunnel = sshTunnel
131
132async 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
141exports.fetchConfig = fetchConfig