Skip to content

Commit 66fe156

Browse files
philippefutureboypaveltiunov
authored andcommitted
feat(cubejs-server): Integrated support for TLS (#213)
* feat(cubejs-server): Integrated support for TLS @cubejs-backend/server listen supports receiving an option object. Given env CUBEJS_ENABLE_TLS=true, the CubejsServer will use the option object in order to setup https connection. * fix(packages/cubejs-server): Fix https string for redirection * test(packages/cubejs-server): Updated snapshot test for redirector handler fn * docs(packages/cubejs-server): Updated documentation to include TLS Updated documentation to reflect changes in API and introduction of TLS support. * chore(packages/cubejs-server): Removed dependency on config/env script
1 parent be2c7cf commit 66fe156

File tree

10 files changed

+3290
-463
lines changed

10 files changed

+3290
-463
lines changed

docs/Cube.js-Backend/@cubejs-backend-server.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,13 @@ server.listen().then(({ port }) => {
2929
console.log(`🚀 Cube.js server is listening on ${port}`);
3030
});
3131
```
32+
33+
### this.listen([options])
34+
35+
Instantiates the Express.js App to listen to the specified `PORT`. Returns a promise that resolves with the following members:
36+
37+
* `port {number}` The port at which CubejsServer is listening for insecure connections for redirection to HTTPS, as specified by the environment variable `PORT`. Defaults to 4000.
38+
* `app {Express.Application}` The express App powering CubejsServer
39+
* `server {http.Server}` The `http` Server instance. If TLS is enabled, returns a `https.Server` instance instead.
40+
41+
Cube.js can also support TLS encryption. See the [Security page on how to enable tls](security#enabling-tls) for more information.

docs/Cube.js-Backend/Security.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Cube.js Javascript client accepts auth token as a first argument to [cubejs(auth
2222
**In the development environment the token is not required for authorization**, but
2323
you can still use it to [pass a security context](security#security-context).
2424

25+
Cube.js also supports Transport Layer Encryption (TLS) using Node.js native packages. For more information, see [Enabling TLS](security#enabling-tls).
26+
2527
## Generating Token
2628

2729
Auth token is generated based on your API secret. Cube.js CLI generates API Secret on app creation and saves it in `.env` file as `CUBEJS_API_SECRET` variable.
@@ -115,3 +117,86 @@ SELECT
115117
) AS orders
116118
LIMIT 10000
117119
```
120+
121+
## Enabling TLS
122+
123+
Cube.js server package supports transport layer encryption.
124+
125+
By setting the environment variable `CUBEJS_ENABLE_TLS` to true (`CUBEJS_ENABLE_TLS=true`), `@cubejs-backend/server` expects an argument to its `listen` function specifying the tls encryption options. The `tlsOption` object must match Node.js' [`https.createServer([options][, requestListener])` option object](https://nodejs.org/api/https.html#https_https_createserver_options_requestlistener).
126+
127+
This enables you to specify your TLS security directly within the Node process without having to rely on external deployment tools to manage your certificates.
128+
129+
```javascript
130+
const fs = require("fs-extra");
131+
const CubejsServer = require("@cubejs-backend/server");
132+
const cubejsOptions = require("./cubejsOptions");
133+
134+
var tlsOptions = {
135+
key: fs.readFileSync(process.env.CUBEJS_TLS_PRIVATE_KEY_FILE),
136+
cert: fs.readFileSync(process.env.CUBEJS_TLS_PRIVATE_FULLCHAIN_FILE),
137+
};
138+
139+
const cubejsServer = cubejsOptions
140+
? new CubejsServer(cubejsOptions)
141+
: new CubejsServer();
142+
143+
cubejsServer.listen(tlsOptions).then(({ tlsPort }) => {
144+
console.log(`🚀 Cube.js server is listening securely on ${tlsPort}`);
145+
});
146+
```
147+
148+
Notice that the response from the resolution of `listen`'s promise returns more than just the `port` and the express `app` as it would normally do without `CUBEJS_ENABLE_TLS` enabled. When `CUBEJS_ENABLE_TLS` is enabled, `cubejsServer.listen` will resolve with the following:
149+
150+
* `port {number}` The port at which CubejsServer is listening for insecure connections for redirection to HTTPS, as specified by the environment variable `PORT`. Defaults to 4000.
151+
* `tlsPort {number}` The port at which TLS is enabled, as specified by the environment variable `TLS_PORT`. Defaults to 4433.
152+
* `app {Express.Application}` The express App powering CubejsServer
153+
* `server {https.Server}` The `https` Server instance.
154+
155+
The `server` object is especially useful if you want to use self-signed, self-renewed certificates.
156+
157+
### Self-signed, self-renewed certificates
158+
159+
Self-signed, self-renewed certificates are useful when dealing with internal data transit, like when answering requests from private server instance to another private server instance without being able to use an external DNS CA to sign the private certificates. _Example:_ EC2 to EC2 instance communications within the private subnet of a VPC.
160+
161+
Here is an example of how to do leverage `server` to have self-signed, self-renewed encryption:
162+
163+
```js
164+
const CubejsServer = require("@cubejs-backend/server");
165+
const cubejsOptions = require("./cubejsOptions");
166+
const {
167+
createCertificate,
168+
scheduleCertificateRenewal,
169+
} = require("./certificate");
170+
171+
async function main() {
172+
const cubejsServer = cubejsOptions
173+
? new CubejsServer(cubejsOptions)
174+
: new CubejsServer();
175+
176+
const certOptions = { days: 2, selfSigned: true };
177+
const tlsOptions = await createCertificate(certOptions);
178+
179+
const ({ tlsPort, server }) = await cubejsServer.listen(tlsOptions);
180+
181+
console.log(`🚀 Cube.js server is listening securely on ${tlsPort}`);
182+
183+
scheduleCertificateRenewal(server, certOptions, (err, result) => {
184+
if (err !== null) {
185+
console.error(
186+
`🚨 Certificate renewal failed with error "${error.message}"`
187+
);
188+
// take some action here to notify the DevOps
189+
return;
190+
}
191+
console.log(`🔐 Certificate renewal successful`);
192+
});
193+
}
194+
195+
main();
196+
```
197+
198+
To generate your self-signed certificates, look into [`pem`](https://www.npmjs.com/package/pem) and [`node-forge`](https://www.npmjs.com/package/node-forge).
199+
200+
### 🚨 Node Support for Self Renewal of Secure Context
201+
202+
Certificate Renewal using [`server.setSecureContext(options)`](https://nodejs.org/api/tls.html#tls_server_setsecurecontext_options) is only available as of Node.js v11.x
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const http = jest.requireActual("http");
2+
3+
http.__mockServer = {
4+
listen: jest.fn((opts, cb) => cb && cb(null)),
5+
close: jest.fn((cb) => cb && cb(null)),
6+
delete: jest.fn()
7+
};
8+
9+
http.createServer = jest.fn(() => http.__mockServer);
10+
11+
12+
module.exports = http;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const https = jest.requireActual("https");
2+
3+
https.__mockServer = {
4+
listen: jest.fn((opts, cb) => cb && cb(null)),
5+
close: jest.fn((cb) => cb && cb(null)),
6+
delete: jest.fn(),
7+
setSecureContext: jest.fn()
8+
};
9+
10+
https.createServer = jest.fn(() => https.__mockServer);
11+
12+
module.exports = https;
13+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`CubeServer listen given that CUBEJS_ENABLE_TLS is true, should create an http server listening to PORT to redirect to https 1`] = `
4+
"(req, res) => {
5+
res.writeHead(301, {
6+
Location: \`https://\${req.headers.host}:\${TLS_PORT}\${req.url}\`
7+
});
8+
res.end();
9+
}"
10+
`;

packages/cubejs-server/index.js

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,89 @@ class CubejsServer {
55
constructor(config) {
66
config = config || {};
77
this.core = CubejsServerCore.create(config);
8+
this.redirector = null;
9+
this.server = null;
810
}
911

10-
async listen() {
12+
async listen(options = {}) {
1113
try {
12-
const express = require('express');
14+
if (this.server) {
15+
throw new Error("CubeServer is already listening");
16+
}
17+
18+
const http = require("http");
19+
const https = require("https");
20+
const util = require("util");
21+
const express = require("express");
1322
const app = express();
14-
const bodyParser = require('body-parser');
15-
app.use(require('cors')());
16-
app.use(bodyParser.json({ limit: '50mb' }));
23+
const bodyParser = require("body-parser");
24+
app.use(require("cors")());
25+
app.use(bodyParser.json({ limit: "50mb" }));
1726

1827
await this.core.initApp(app);
19-
const port = process.env.PORT || 4000;
2028

2129
return new Promise((resolve, reject) => {
22-
app.listen(port, (err) => {
23-
if (err) {
24-
reject(err);
25-
return;
26-
}
27-
resolve({ app, port });
28-
});
29-
})
30+
const PORT = process.env.PORT || 4000;
31+
const TLS_PORT = process.env.TLS_PORT || 4433;
32+
33+
if (process.env.CUBEJS_ENABLE_TLS === "true") {
34+
this.redirector = http.createServer((req, res) => {
35+
res.writeHead(301, {
36+
Location: `https://${req.headers.host}:${TLS_PORT}${req.url}`
37+
});
38+
res.end();
39+
});
40+
this.redirector.listen(PORT);
41+
this.server = https.createServer(options, app);
42+
this.server.listen(TLS_PORT, err => {
43+
if (err) {
44+
this.server = null;
45+
this.redirector = null;
46+
reject(err);
47+
return;
48+
}
49+
this.redirector.close = util.promisify(this.redirector.close);
50+
this.server.close = util.promisify(this.server.close);
51+
resolve({ app, port: PORT, tlsPort: TLS_PORT, server: this.server });
52+
});
53+
} else {
54+
this.server = http.createServer(options, app);
55+
this.server.listen(PORT, err => {
56+
if (err) {
57+
this.server = null;
58+
this.redirector = null;
59+
reject(err);
60+
return;
61+
}
62+
resolve({ app, port: PORT, server: this.server });
63+
});
64+
}
65+
});
66+
} catch (e) {
67+
this.core.event &&
68+
(await this.core.event("Dev Server Fatal Error", {
69+
error: (e.stack || e.message || e).toString()
70+
}));
71+
throw e;
72+
}
73+
}
74+
75+
async close() {
76+
try {
77+
if (!this.server) {
78+
throw new Error("CubeServer is not started.");
79+
}
80+
await this.server.close();
81+
this.server = null;
82+
if (this.redirector) {
83+
await this.redirector.close();
84+
this.redirector = null;
85+
}
3086
} catch (e) {
31-
this.core.event && (await this.core.event('Dev Server Fatal Error', {
32-
error: (e.stack || e.message || e).toString()
33-
}));
87+
this.core.event &&
88+
(await this.core.event("Dev Server Fatal Error", {
89+
error: (e.stack || e.message || e).toString()
90+
}));
3491
throw e;
3592
}
3693
}

0 commit comments

Comments
 (0)