Skip to content
This repository has been archived by the owner on Oct 26, 2019. It is now read-only.

Commit

Permalink
Separate socket server from the HTTP API. (#241)
Browse files Browse the repository at this point in the history
* Separate socket server from the HTTP API.

This changes the API so users have to set up both the HTTP API and the
socket server.

* Update docs

* lint fixes
  • Loading branch information
goto-bus-stop authored Jul 15, 2018
1 parent f9a15b3 commit f123c41
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 136 deletions.
54 changes: 46 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ handlers.

## API

```js
import { createHttpApi, createSocketServer } from 'u-wave-http-api'
const { createHttpApi, createSocketServer } = require('u-wave-http-api')
```

### api = createHttpApi(uwave, options={})

Creates a middleware for use with [Express][] or another such library. The first
parameter is a `u-wave-core` instance. Available options are:

- `server` - An HTTP server instance. `u-wave-http-api` uses WebSockets, and it
needs an HTTP server to listen to for incoming WebSocket connections. An
example for how to obtain this server from an Express app is shown below.
- `socketPort` - The WebSocket server can also listen on its own port instead
of attaching to the HTTP server. In that case, specify the port number here.
- `secret` - A string or Buffer containing a secret used to encrypt
authentication tokens. It's important that this is the same as the `secret`
option passed to the core library.
Expand All @@ -42,14 +42,15 @@ parameter is a `u-wave-core` instance. Available options are:
- `onError` - Error handler function, use for recording errors. First parameter
is the request object that caused the error, second is the error itself.

**Example**

```js
import express from 'express';
import stubTransport from 'nodemailer-stub-transport';
import uwave from 'u-wave-core';
import createHttpApi from 'u-wave-http-api';
import { createHttpApi } from 'u-wave-http-api';

const app = express();
const server = app.listen();

const secret = fs.readFileSync('./secret.dat');

Expand All @@ -58,7 +59,6 @@ const uw = uwave({
});
const api = createHttpApi(uw, {
secret: secret, // Encryption secret
server: server, // HTTP server
recaptcha: { secret: 'AABBCC...' }, // Optional
mailTransport: stubTransport(), // Optional
onError: (req, error) => {}, // Optional
Expand All @@ -75,6 +75,8 @@ object to the request. The `u-wave-core` instance will be available as
`req.uwaveHttp`. This is useful if you want to access these objects in custom
routes, that are not in the `u-wave-http-api` namespace. E.g.:

**Example**

```js
app.use('/api', api);

Expand All @@ -87,6 +89,42 @@ app.get('/profile/:user', api.attachUwaveToRequest(), (req, res) => {
});
```

### sockets = createSocketServer(uwave, options={})

Create the WebSocket server used for realtime communication, like advance
notifications and chat messages.

- `server` - An HTTP server instance. `u-wave-http-api` uses WebSockets, and it
needs an HTTP server to listen to for incoming WebSocket connections. An
example for how to obtain this server from an Express app is shown below.
- `port` - The WebSocket server can also listen on its own port instead
of attaching to the HTTP server. In that case, specify the port number here.
- `secret` - A string or Buffer containing a secret used to encrypt
authentication tokens. It's important that this is the same as the `secret`
option passed to the core library and the `createHttpApi` function.

**Example**

```js
import express from 'express';
import { createSocketServer } from 'u-wave-http-api';

const app = express();
const server = app.listen(8080);

const secret = fs.readFileSync('./secret.dat');

const sockets = createSocketServer(uw, {
server, // The HTTP server
secret: secret, // Encryption secret
});
// ALTERNATIVELY:
const sockets = createSocketServer(uw, {
port: 6042, // Port to listen on—make sure to configure web clients for this
secret: secret, // Encryption secret
});
```

## Contributing

There is a development server included in this repository. To use it, first you
Expand Down
50 changes: 29 additions & 21 deletions dev/u-wave-api-dev-server
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ const express = require('express');
const config = require('./dev-server-config.json');
const mailDebug = require('debug')('uwave:mail');

const testTransport = {
name: 'test',
version: '0.0.0',
send(mail, callback) {
mail.message.createReadStream().pipe(concat((message) => {
mailDebug(mail.message.getEnvelope().to, message.toString('utf8'));
callback(null, {
envelope: mail.message.getEnvelope(),
messageId: mail.message.messageId()
});
}));
}
};

function tryRequire(file, message) {
try {
// eslint-disable-next-line import/no-dynamic-require
Expand Down Expand Up @@ -44,19 +58,19 @@ function loadDevModules() {
'u-wave-core/src/index.js',
'Could not find the u-wave core module. Did you run `npm link u-wave-core`?'
);
const createWebApi = require('../src').default;
const { createHttpApi, createSocketServer } = require('../src');

return { uwave, createWebApi };
return { uwave, createHttpApi, createSocketServer };
}

function loadProdModules() {
const uwave = tryRequire(
'u-wave-core',
'Could not find the u-wave core module. Did you run `npm link u-wave-core`?'
);
const createWebApi = require('../');
const { createHttpApi, createSocketServer } = require('../');

return { uwave, createWebApi };
return { uwave, createHttpApi, createSocketServer };
}

/**
Expand All @@ -68,7 +82,8 @@ function start() {

const {
uwave,
createWebApi
createHttpApi,
createSocketServer,
} = watch ? loadDevModules() : loadProdModules();

const uw = uwave(config);
Expand All @@ -92,27 +107,20 @@ function start() {
app.set('json spaces', 2);

const apiUrl = '/api';
const secret = Buffer.from('none', 'utf8');

app.use(apiUrl, createWebApi(uw, {
app.use(apiUrl, createHttpApi(uw, {
recaptcha: { secret: recaptchaTestKeys.secret },
server,
secret: Buffer.from('none', 'utf8'),
secret,
auth: config.auth,
mailTransport: {
name: 'test',
version: '0.0.0',
send(mail, callback) {
mail.message.createReadStream().pipe(concat((message) => {
mailDebug(mail.message.getEnvelope().to, message.toString('utf8'));
callback(null, {
envelope: mail.message.getEnvelope(),
messageId: mail.message.messageId()
});
}));
}
}
mailTransport: testTransport,
}));

createSocketServer(uw, {
server,
secret,
});

return app;
}

Expand Down
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default {
input: 'src/index.js',
output: [{
file: pkg.main,
exports: 'default',
exports: 'named',
format: 'cjs',
sourcemap: true,
}, {
Expand Down
29 changes: 29 additions & 0 deletions src/AuthRegistry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import crypto from 'crypto';
import { promisify } from 'util';

const randomBytes = promisify(crypto.randomBytes);

export default class AuthRegistry {
constructor(redis) {
this.redis = redis;
}

async createAuthToken(user) {
const token = (await randomBytes(64)).toString('hex');
await this.redis.set(`http-api:socketAuth:${token}`, user.id, 'EX', 60);
return token;
}

async getTokenUser(token) {
if (token.length !== 128) {
throw new Error('Invalid token');
}
const [userID] = await this.redis
.multi()
.get(`http-api:socketAuth:${token}`)
.del(`http-api:socketAuth:${token}`)
.exec();

return userID;
}
}
54 changes: 3 additions & 51 deletions src/HttpApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,7 @@ import errorHandler from './middleware/errorHandler';
import rateLimit from './middleware/rateLimit';

import createPassport from './passport';
import WSServer from './sockets';

function missingServerOption() {
throw new TypeError(`
Exactly one of "options.server" and "options.socketPort" is required. These
options are used to attach the WebSocket server to the correct HTTP server.
An example of how to attach the WebSocket server to an existing HTTP server
using Express:
import httpApi from 'u-wave-http-api';
const app = express();
const server = app.listen(80);
app.use('/api', httpApi(uwave, {
server: server,
...
}));
Alternatively, you can provide a port for the socket server to listen on:
import httpApi from 'u-wave-http-api';
const app = express();
app.use('/api', httpApi(uwave, {
socketPort: 6042,
...
}));
`);
}
import AuthRegistry from './AuthRegistry';

function defaultCreatePasswordResetEmail({ token, requestUrl }) {
const parsed = url.parse(requestUrl);
Expand Down Expand Up @@ -81,10 +52,6 @@ export default class UwaveHttpApi extends Router {
+ 'developing, you may have to upgrade your u-wave-* modules.');
}

if (!options.server && !options.socketPort) {
missingServerOption(options);
}

if (!options.secret) {
throw new TypeError('"options.secret" is empty. This option is used to sign authentication '
+ 'keys, and is required for security reasons.');
Expand All @@ -104,11 +71,8 @@ export default class UwaveHttpApi extends Router {
const router = super(options);

this.uw = uw;
this.sockets = new WSServer(uw, {
port: options.socketPort,
server: options.server,
secret: options.secret,
});

this.authRegistry = new AuthRegistry(uw.redis);

this.passport = createPassport(uw, {
secret: options.secret,
Expand Down Expand Up @@ -159,16 +123,4 @@ export default class UwaveHttpApi extends Router {
attachUwaveToRequest() {
return attachUwaveMeta(this, this.uw);
}

/**
* @return Number of open guest connections.
*/
getGuestCount() {
return this.sockets.getGuestCount();
}

destroy() {
this.sockets.destroy();
this.sockets = null;
}
}
Loading

0 comments on commit f123c41

Please sign in to comment.