Skip to content

Commit

Permalink
feat(cubejs-server): Integrated support for TLS (#213)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
philippefutureboy authored and paveltiunov committed Sep 27, 2019
1 parent be2c7cf commit 66fe156
Show file tree
Hide file tree
Showing 10 changed files with 3,290 additions and 463 deletions.
10 changes: 10 additions & 0 deletions docs/Cube.js-Backend/@cubejs-backend-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,13 @@ server.listen().then(({ port }) => {
console.log(`🚀 Cube.js server is listening on ${port}`);
});
```

### this.listen([options])

Instantiates the Express.js App to listen to the specified `PORT`. Returns a promise that resolves with the following members:

* `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.
* `app {Express.Application}` The express App powering CubejsServer
* `server {http.Server}` The `http` Server instance. If TLS is enabled, returns a `https.Server` instance instead.

Cube.js can also support TLS encryption. See the [Security page on how to enable tls](security#enabling-tls) for more information.
85 changes: 85 additions & 0 deletions docs/Cube.js-Backend/Security.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Cube.js Javascript client accepts auth token as a first argument to [cubejs(auth
**In the development environment the token is not required for authorization**, but
you can still use it to [pass a security context](security#security-context).

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

## Generating Token

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.
Expand Down Expand Up @@ -115,3 +117,86 @@ SELECT
) AS orders
LIMIT 10000
```

## Enabling TLS

Cube.js server package supports transport layer encryption.

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).
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.
```javascript
const fs = require("fs-extra");
const CubejsServer = require("@cubejs-backend/server");
const cubejsOptions = require("./cubejsOptions");
var tlsOptions = {
key: fs.readFileSync(process.env.CUBEJS_TLS_PRIVATE_KEY_FILE),
cert: fs.readFileSync(process.env.CUBEJS_TLS_PRIVATE_FULLCHAIN_FILE),
};
const cubejsServer = cubejsOptions
? new CubejsServer(cubejsOptions)
: new CubejsServer();
cubejsServer.listen(tlsOptions).then(({ tlsPort }) => {
console.log(`🚀 Cube.js server is listening securely on ${tlsPort}`);
});
```
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:

* `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.
* `tlsPort {number}` The port at which TLS is enabled, as specified by the environment variable `TLS_PORT`. Defaults to 4433.
* `app {Express.Application}` The express App powering CubejsServer
* `server {https.Server}` The `https` Server instance.

The `server` object is especially useful if you want to use self-signed, self-renewed certificates.

### Self-signed, self-renewed certificates

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.

Here is an example of how to do leverage `server` to have self-signed, self-renewed encryption:

```js
const CubejsServer = require("@cubejs-backend/server");
const cubejsOptions = require("./cubejsOptions");
const {
createCertificate,
scheduleCertificateRenewal,
} = require("./certificate");
async function main() {
const cubejsServer = cubejsOptions
? new CubejsServer(cubejsOptions)
: new CubejsServer();
const certOptions = { days: 2, selfSigned: true };
const tlsOptions = await createCertificate(certOptions);
const ({ tlsPort, server }) = await cubejsServer.listen(tlsOptions);
console.log(`🚀 Cube.js server is listening securely on ${tlsPort}`);
scheduleCertificateRenewal(server, certOptions, (err, result) => {
if (err !== null) {
console.error(
`🚨 Certificate renewal failed with error "${error.message}"`
);
// take some action here to notify the DevOps
return;
}
console.log(`🔐 Certificate renewal successful`);
});
}
main();
```
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).
### 🚨 Node Support for Self Renewal of Secure Context
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
12 changes: 12 additions & 0 deletions packages/cubejs-server/__mocks__/http.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const http = jest.requireActual("http");

http.__mockServer = {
listen: jest.fn((opts, cb) => cb && cb(null)),
close: jest.fn((cb) => cb && cb(null)),
delete: jest.fn()
};

http.createServer = jest.fn(() => http.__mockServer);


module.exports = http;
13 changes: 13 additions & 0 deletions packages/cubejs-server/__mocks__/https.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const https = jest.requireActual("https");

https.__mockServer = {
listen: jest.fn((opts, cb) => cb && cb(null)),
close: jest.fn((cb) => cb && cb(null)),
delete: jest.fn(),
setSecureContext: jest.fn()
};

https.createServer = jest.fn(() => https.__mockServer);

module.exports = https;

10 changes: 10 additions & 0 deletions packages/cubejs-server/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`CubeServer listen given that CUBEJS_ENABLE_TLS is true, should create an http server listening to PORT to redirect to https 1`] = `
"(req, res) => {
res.writeHead(301, {
Location: \`https://\${req.headers.host}:\${TLS_PORT}\${req.url}\`
});
res.end();
}"
`;
91 changes: 74 additions & 17 deletions packages/cubejs-server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,89 @@ class CubejsServer {
constructor(config) {
config = config || {};
this.core = CubejsServerCore.create(config);
this.redirector = null;
this.server = null;
}

async listen() {
async listen(options = {}) {
try {
const express = require('express');
if (this.server) {
throw new Error("CubeServer is already listening");
}

const http = require("http");
const https = require("https");
const util = require("util");
const express = require("express");
const app = express();
const bodyParser = require('body-parser');
app.use(require('cors')());
app.use(bodyParser.json({ limit: '50mb' }));
const bodyParser = require("body-parser");
app.use(require("cors")());
app.use(bodyParser.json({ limit: "50mb" }));

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

return new Promise((resolve, reject) => {
app.listen(port, (err) => {
if (err) {
reject(err);
return;
}
resolve({ app, port });
});
})
const PORT = process.env.PORT || 4000;
const TLS_PORT = process.env.TLS_PORT || 4433;

if (process.env.CUBEJS_ENABLE_TLS === "true") {
this.redirector = http.createServer((req, res) => {
res.writeHead(301, {
Location: `https://${req.headers.host}:${TLS_PORT}${req.url}`
});
res.end();
});
this.redirector.listen(PORT);
this.server = https.createServer(options, app);
this.server.listen(TLS_PORT, err => {
if (err) {
this.server = null;
this.redirector = null;
reject(err);
return;
}
this.redirector.close = util.promisify(this.redirector.close);
this.server.close = util.promisify(this.server.close);
resolve({ app, port: PORT, tlsPort: TLS_PORT, server: this.server });
});
} else {
this.server = http.createServer(options, app);
this.server.listen(PORT, err => {
if (err) {
this.server = null;
this.redirector = null;
reject(err);
return;
}
resolve({ app, port: PORT, server: this.server });
});
}
});
} catch (e) {
this.core.event &&
(await this.core.event("Dev Server Fatal Error", {
error: (e.stack || e.message || e).toString()
}));
throw e;
}
}

async close() {
try {
if (!this.server) {
throw new Error("CubeServer is not started.");
}
await this.server.close();
this.server = null;
if (this.redirector) {
await this.redirector.close();
this.redirector = null;
}
} catch (e) {
this.core.event && (await this.core.event('Dev Server Fatal Error', {
error: (e.stack || e.message || e).toString()
}));
this.core.event &&
(await this.core.event("Dev Server Fatal Error", {
error: (e.stack || e.message || e).toString()
}));
throw e;
}
}
Expand Down
Loading

0 comments on commit 66fe156

Please sign in to comment.