Skip to content

Commit

Permalink
2.6.0
Browse files Browse the repository at this point in the history
2.6.0 - 2017-08-28
-----------------------------------------------

This release enables SSL with one line -- `config.enableSSL(options)`!

Now SSL and plain HTTP can live side-by-side, on the same port, thanks
to a new `tcpHandler()` function that proxies data to the correct
listening port, avoiding the need to expose more than one port publicly.

All changes:

## Config
* Adds `config.getKoaApp()`, for passing a function that access Koa's
`app` instance, allowing you to do something with `app` that's not
covered by other functions (closes
#65)
* Adds `config.enableSSL(opt)`, for running Koa via a HTTPS/SSL server
* Adds `config.disableHTTP()`, for disabling a plain HTTP server
listener (used in conjunction with `config.enableSSL()`, when you want
SSL-only)
* Adds `config.forceSSL(opts)`, for re-writing plain `http://` ->
`https://` using [koa-sslify](https://github.com/turboMaCk/koa-sslify)

## Server
* Adds `http` and `https` configurations, for enabling SSL
* Adds `get-port`, for assigning http(s) listeners to random ports
* Adds `listen()` function, for proxying traffic to http(s) depending
on the request received (if the first byte == 22 ? SSL : HTTP)
* Refactors `kit/entry/server.js` to no longer export an immediate
`async` function (no longer returns a Promise)
* Refactors `kit/entry/server_*` to use the new `server.js` default
export

## App
* Adds example use of `config.getKoaApp()`, that extends the
`app.context` prototype with an `engine = 'ReactQL'` key, which we
later use in middleware to add a 'Powered-By' response header. Also
adds a general error catching function, for server-level errors.
* Adds `src/cert/self_signed.js`, to export a sample self-signed SSL
certificate to enable SSL in userland
* Adds `config.enableSSL()` example, for running HTTPS side-by-side
with the regular HTTP port
* Adds commentary for new `config.disableHTTP()` and
`config.forceSSL()` functions

## NPM
* Adds packages:
"get-port": "^3.2.0"
"koa-sslify": "^2.1.2"

* Bumps packages:
graphql                 ^0.10.5  →  ^0.11.1
apollo-server-koa   ^1.1.0  →   ^1.1.2
react-apollo       ^1.4.14  →  ^1.4.15
react-router        ^4.1.2  →   ^4.2.0
react-router-dom    ^4.1.2  →   ^4.2.2
npm-run-all         ^4.0.2  →   ^4.1.0
  • Loading branch information
leebenson committed Aug 28, 2017
1 parent 46c7ae4 commit e442ee5
Show file tree
Hide file tree
Showing 9 changed files with 437 additions and 117 deletions.
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,44 @@
2.6.0 - 2017-08-28
-----------------------------------------------

This release enables SSL with one line -- `config.enableSSL(options)`!

Now SSL and plain HTTP can live side-by-side, on the same port, thanks to a new `tcpHandler()` function that proxies data to the correct listening port, avoiding the need to expose more than one port publicly.

All changes:

## Config
* Adds `config.getKoaApp()`, for passing a function that access Koa's `app` instance, allowing you to do something with `app` that's not covered by other functions (closes https://github.com/reactql/kit/issues/65)
* Adds `config.enableSSL(opt)`, for running Koa via a HTTPS/SSL server
* Adds `config.disableHTTP()`, for disabling a plain HTTP server listener (used in conjunction with `config.enableSSL()`, when you want SSL-only)
* Adds `config.forceSSL(opts)`, for re-writing plain `http://` -> `https://` using [koa-sslify](https://github.com/turboMaCk/koa-sslify)

## Server
* Adds `http` and `https` configurations, for enabling SSL
* Adds `get-port`, for assigning http(s) listeners to random ports
* Adds `listen()` function, for proxying traffic to http(s) depending on the request received (if the first byte == 22 ? SSL : HTTP)
* Refactors `kit/entry/server.js` to no longer export an immediate `async` function (no longer returns a Promise)
* Refactors `kit/entry/server_*` to use the new `server.js` default export

## App
* Adds example use of `config.getKoaApp()`, that extends the `app.context` prototype with an `engine = 'ReactQL'` key, which we later use in middleware to add a 'Powered-By' response header. Also adds a general error catching function, for server-level errors.
* Adds `src/cert/self_signed.js`, to export a sample self-signed SSL certificate to enable SSL in userland
* Adds `config.enableSSL()` example, for running HTTPS side-by-side with the regular HTTP port
* Adds commentary for new `config.disableHTTP()` and `config.forceSSL()` functions

## NPM
* Adds packages:
"get-port": "^3.2.0"
"koa-sslify": "^2.1.2"

* Bumps packages:
graphql ^0.10.5 → ^0.11.1
apollo-server-koa ^1.1.0 → ^1.1.2
react-apollo ^1.4.14 → ^1.4.15
react-router ^4.1.2 → ^4.2.0
react-router-dom ^4.1.2 → ^4.2.2
npm-run-all ^4.0.2 → ^4.1.0

2.5.3 - 2017-08-26
-----------------------------------------------

Expand Down
42 changes: 42 additions & 0 deletions kit/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ if (SERVER) {
// Create a set for routes -- to retrieve based on insertion order
this.routes = new Set();

// Koa application function. But default, this is null
this.koaAppFunc = null;

// Flag for setting whether plain HTTP should be disabled
this.enableHTTP = true;

// Force SSL. Rewrites all non-SSL queries to SSL. False, by default.
this.enableForceSSL = false;

// Options for enabling SSL. By default, this is null. If SSL is enabled
// in userland, this would instead hold an object of options
this.sslOptions = null;

// Custom middleware -- again, based on insertion order
this.middleware = new Set();

Expand All @@ -107,6 +120,35 @@ if (SERVER) {

/* WEB SERVER / SSR */

// Get access to Koa's `app` instance, for adding custom instantiation
// or doing something that's not covered by other functions
getKoaApp(func) {
this.koaAppFunc = func;
}

// Enable SSL. Normally, you'd use an upstream proxy like Nginx to handle
// SSL. But if you want to run a 'bare' Koa HTTPS web server, you can pass
// in the certificate details here and it'll respond to SSL requests
enableSSL(opt) {
// At a minimum, we should have `key` and `cert` -- check for those
if (typeof opt !== 'object' || !opt.key || !opt.cert) {
throw new Error('Cannot enable SSL. Missing `key` and/or `cert`');
}
this.sslOptions = opt;
}

// Force SSL. Rewrites all non-SSL queries to SSL. Any options here are
// passed to `koa-sslify`, the SSL enforcement middleware
forceSSL(opt = {}) {
this.enableForceSSL = opt;
}

// Disable plain HTTP. Note this should only be used if you've also set
// `enableSSL()` and you don't want dual-HTTP+SSL config
disableHTTP() {
this.enableHTTP = false;
}

// Disable the optional `koa-bodyparser`, to prevent POST data being sent to
// each request. By default, body parsing is enabled.
disableBodyParser() {
Expand Down
114 changes: 108 additions & 6 deletions kit/entry/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@
// For pre-pending a `<!DOCTYPE html>` stream to the server response
import { PassThrough } from 'stream';

// HTTP & SSL servers. We can use `config.enableSSL|disableHTTP()` to enable
// HTTPS and disable plain HTTP respectively, so we'll use Node's core libs
// for building both server types. We'll let our TCP handler (defined later)
// figure out the request type and pipe to the relevant host
import net from 'net';
import http from 'http';
import https from 'https';

/* NPM */

// Patch global.`fetch` so that Apollo calls to GraphQL work
Expand All @@ -31,12 +39,19 @@ import ReactDOMServer from 'react-dom/server';
// the React render, or any of the static assets being compiled
import Koa from 'koa';

// Get available ports, for creating a separate HTTP + SSL server that
// can listen for traffic from our common `kit/lib/env.getPort()` call
import getAvailablePort from 'get-port';

// Apollo tools to connect to a GraphQL server. We'll grab the
// `ApolloProvider` HOC component, which will inject any 'listening' React
// components with GraphQL data props. We'll also use `getDataFromTree`
// to await data being ready before rendering back HTML to the client
import { ApolloProvider, getDataFromTree } from 'react-apollo';

// Enforce SSL, if required
import koaSSL from 'koa-sslify';

// Enable cross-origin requests
import koaCors from 'kcors';

Expand Down Expand Up @@ -295,6 +310,13 @@ const app = new Koa()
return next();
});

/* FORCE SSL */

// Middleware to re-write HTTP requests to SSL, if required.
if (config.enableForceSSL) {
app.use(koaSSL(config.enableForceSSL));
}

// Attach custom middleware
config.middleware.forEach(middlewareFunc => app.use(middlewareFunc));

Expand Down Expand Up @@ -359,10 +381,90 @@ if (config.enableBodyParser) {
));
}

// Run the server
export default (async function server() {
return {
router,
app,
/* CUSTOM APP INSTANTIATION */

// Pass the `app` to do anything we need with it in userland. Useful for
// custom instantiation that doesn't fit into the middleware/route functions
if (typeof config.koaAppFunc === 'function') {
config.koaAppFunc(app);
}

// Listener function that will start http(s) server(s) based on userland
// config and available ports, and create a public-facing proxy that will
// pipe traffic to the correct http/ssl port -- to enable single port HTTP + SSL
const listen = async port => {
// Create the ports
const ports = {};

// Userland config can disable plain HTTP, so check we need it
if (config.enableHTTP) {
ports.http = await getAvailablePort();
}

// Do we have an SSL server?
if (config.sslOptions) {
ports.ssl = await getAvailablePort();
}

// If we have NEITHER, then throw an error!
if (!ports.http && !ports.ssl) {
throw new Error('You have disabled plain HTTP and not enabled SSL!');
}

// TCP handler. This will allow us to spawn a Koa server on a common port
// and proxy through to HTTP(S) as necessary.
const tcpHandler = conn => {
conn.once('data', buf => {
let address;

if (!ports.http) {
// If HTTP is disabled, *always* use SSL
address = ports.ssl;
} else if (!ports.ssl) {
// If SSL is disabled, *always* use HTTP
address = ports.http;
} else {
// The first byte of a TLS handshake is always byte 22. If we have
// something different, use plain HTTP
address = buf[0] === 22 ? ports.ssl : ports.http;
}

// Create a proxy to the underlying port, so we can simply pipe all
// data to the right listener
const proxy = net.createConnection(address, () => {
proxy.write(buf);
conn.pipe(proxy).pipe(conn);
});
});
};
}());

// Spawn the listeners. We'll store them on an array and return them as
// the resolved Promise value, in case we need to mess with them somehow
// in the calling function
const servers = [
net.createServer(tcpHandler).listen(port),
];

// Plain HTTP
if (ports.http) {
servers.push(
http.createServer(app.callback()).listen(ports.http),
);
}

// SSL
if (ports.ssl) {
servers.push(
https.createServer(config.sslOptions, app.callback()).listen(ports.ssl),
);
}

return servers;
};

// Export everything we need to run the server (in dev or prod)
export default {
router,
app,
listen,
};
25 changes: 15 additions & 10 deletions kit/entry/server_dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ const scripts = [
'vendor.js',
'browser.js'].map(key => `/${key}`);

// Spawn the server
server.then(({ router, app }) => {
// Spawn the development server.
// Runs inside an immediate `async` block, to await listening on ports
(async () => {
const { app, router, listen } = server;

// Create proxy to tunnel requests to the browser `webpack-dev-server`
router.get('/*', createReactHandler(css, scripts));

Expand All @@ -44,12 +47,14 @@ server.then(({ router, app }) => {
.use(router.routes())
.use(router.allowedMethods());

app.listen({ host: HOST, port: PORT }, () => {
logServerStarted({
type: 'server-side rendering',
host: HOST,
port: PORT,
chalk: chalk.bgYellow.black,
});
// Spawn the server
await listen(getPort());

// Log to the terminal that we're ready for action
logServerStarted({
type: 'server-side rendering',
host: HOST,
port: PORT,
chalk: chalk.bgYellow.black,
});
});
})();
23 changes: 14 additions & 9 deletions kit/entry/server_prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,25 @@ const scripts = [
'vendor.js',
'browser.js'].map(key => manifest[key]);

// Spawn the server
server.then(({ router, app }) => {
// Spawn the development server.
// Runs inside an immediate `async` block, to await listening on ports
(async () => {
const { app, router, listen } = server;

// Connect the production routes to the server
router.get('/*', createReactHandler(css, scripts, chunkManifest));
app
.use(staticMiddleware())
.use(router.routes())
.use(router.allowedMethods());

app.listen({ host: HOST, port: PORT }, () => {
logServerStarted({
type: 'server',
host: HOST,
port: PORT,
});
// Spawn the server
await listen(getPort());

// Log to the terminal that we're ready for action
logServerStarted({
type: 'server',
host: HOST,
port: PORT,
});
});
})();
Loading

0 comments on commit e442ee5

Please sign in to comment.