GRIP Interface Library for JavaScript
Authors: Katsuyuki Ohmuro harmony7@pex2.jp, Konstantin Bokarius kon@fanout.io
Description
A GRIP interface library for NodeJS. For use with HTTP reverse proxy servers
that support the GRIP interface, such as Pushpin.
Installation
npm install @fanoutio/grip
Sample Usage
Examples for how to publish HTTP response and HTTP stream messages to GRIP proxy endpoints via the GripPubControl class.
var pubcontrol = require('@fanoutio/pubcontrol');
var grip = require('@fanoutio/grip');
var callback = function(success, message, context) {
if (success) {
console.log('Publish successful!');
}
else {
console.log("Publish failed!");
console.log("Message: " + message);
console.log("Context: ");
console.dir(context);
}
};
var grippub = new grip.GripPubControl({
'control_uri': 'https://api.fanout.io/realm/<myrealm>',
'control_iss': '<myrealm>',
'key': Buffer.from('<myrealmkey>', 'base64')});
grippub.applyGripConfig([{'control_uri': '<myendpoint_uri_1>'},
{'control_uri': '<myendpoint_uri_2>'}]);
grippub.removeAllClients();
var pubclient = new pubcontrol.PubControlClient('<myendpoint_uri>');
grippub.addClient(pubclient);
grippub.publishHttpResponse('<channel>', 'Test Publish!', callback);
grippub.publishHttpStream('<channel>', 'Test Publish!', callback);
Validate the Grip-Sig request header from incoming GRIP messages. This ensures that the message was sent from a valid
source and is not expired. Note that when using Fanout.io the key is the realm key, and when using Pushpin the key
is configurable in Pushpin's settings.
var grip = require('grip');
var isValid = grip.validateSig(req.headers['grip-sig'], '<key>');
Long polling example via response headers. The client connects to a GRIP proxy over HTTP and the proxy forwards the
request to the origin. The origin subscribes the client to a channel and instructs it to long poll via the response
headers. Note that with the recent versions of Apache it's not possible to send a 304 response containing custom
headers, in which case the response body should be used instead (next usage example below).
var http = require('http');
var grip = require('@fanoutio/grip');
http.createServer(function (req, res) {
if (!grip.validateSig(req.headers['grip-sig'], '<key>')) {
res.writeHead(401);
res.end('invalid grip-sig token');
return;
}
res.writeHead(200, {
'Grip-Hold': 'response',
'Grip-Channel': grip.createGripChannelHeader('<channel>')});
res.end();
}).listen(80, '0.0.0.0');
console.log('Server running...')
Long polling example via response body. The client connects to a GRIP proxy over HTTP and the proxy forwards the
request to the origin. The origin subscribes the client to a channel and instructs it to long poll via the response
body.
var http = require('http');
var grip = require('@fanoutio/grip');
http.createServer(function (req, res) {
if (!grip.validateSig(req.headers['grip-sig'], '<key>')) {
res.writeHead(401);
res.end('invalid grip-sig token');
return;
}
res.writeHead(200, {'Content-Type': 'application/grip-instruct'});
res.end(grip.createHoldResponse('<channel>'));
}).listen(80, '0.0.0.0');
console.log('Server running...')
WebSocket example using nodejs-websocket. A client connects to a GRIP proxy via WebSockets and the proxy forward the request to the origin. The origin accepts the connection over a WebSocket and responds with a control message indicating that the client should be subscribed to a channel. Note that in order for the GRIP proxy to properly interpret the control messages, the origin must provide a 'grip' extension in the 'Sec-WebSocket-Extensions' header. To accomplish this with nodejs-websocket, edit Connection.js and ensure that the following header is appended to the 'this.socket.write()' function call in the answerHandshake() method: 'Sec-WebSocket-Extensions: grip; message-prefix=""\r\n' To accomplish this with ws, add the ws.on('headers', ...) check to your app, for example:
wss.on('headers', function processHeaders(headers, req) {
headers.push('Sec-WebSocket-Extensions: grip; message-prefix=""');
});
...
server.on('upgrade', function upgrade(request, socket, head) {
wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit('connection', ws, request);
});
});
var ws = require("nodejs-websocket")
var pubcontrol = require('@fanoutio/pubcontrol');
var grip = require('@fanoutio/grip');
ws.createServer(function (conn) {
conn.sendText('c:' + grip.webSocketControlMessage(
'subscribe', {'channel': '<channel>'}));
setTimeout(function() {
var grippub = new grip.GripPubControl({
'control_uri': '<myendpoint>'});
grippub.publish('test_channel', new pubcontrol.Item(
new grip.WebSocketMessageFormat(
'Test WebSocket Publish!!')));
}, 5000);
}).listen(80, '0.0.0.0');
console.log('Server running...');
WebSocket over HTTP example. In this case, a client connects to a GRIP proxy via WebSockets and the GRIP proxy communicates with the origin via HTTP.
var http = require('http');
var pubcontrol = require('@fanoutio/pubcontrol');
var grip = require('@fanoutio/grip');
http.createServer(function (req, res) {
if (!grip.validateSig(req.headers['grip-sig'], 'changeme')) {
res.writeHead(401);
res.end('invalid grip-sig token');
return;
}
res.writeHead(200, {
'Sec-WebSocket-Extensions': 'grip; message-prefix=""',
'Content-Type': 'application/websocket-events'});
var body = '';
req.on('data', function (chunk) {
body += chunk;
});
req.on('end', function() {
var inEvents = grip.decodeWebSocketEvents(body);
if (inEvents[0].getType() == 'OPEN') {
var outEvents = [];
outEvents.push(new grip.WebSocketEvent('OPEN'));
outEvents.push(new grip.WebSocketEvent('TEXT', 'c:' +
grip.webSocketControlMessage('subscribe',
{'channel': 'channel'})));
res.end(grip.encodeWebSocketEvents(outEvents));
setTimeout(function() {
var grippub = new grip.GripPubControl({
'control_uri': '<myendpoint>'});
grippub.publish('channel', new pubcontrol.Item(
new grip.WebSocketMessageFormat(
'Test WebSocket Publish!!')));
}, 5000);
}
});
}).listen(80, '0.0.0.0');
console.log('Server running...');
Parse a GRIP URI to extract the URI, ISS, and key values. The values will be returned in a dictionary containing 'control_uri', 'control_iss', and 'key' keys.
var grip = require('@fanoutio/grip');
var config = grip.parseGripUri('http://api.fanout.io/realm/<myrealm>' +
'?iss=<myrealm>&key=base64:<myrealmkey>');
Consuming this library
CommonJS
The CommonJS version of this package requires Node v8 or newer.
Require in your JavaScript:
const grip = require('@fanoutio/grip');
const grippub = new grip.GripPubControl({control_uri: "<endpoint_uri>"});
If you are building a bundle, you may also import in your JavaScript.
import grip from '@fanoutio/grip';
const pub = new grip.GripPubControl({control_uri: "<endpoint_uri>"});
This package comes with full TypeScript type definitions, so you may use it with
TypeScript as well.
import grip, { IHoldInstruction } from '@fanoutio/grip';
const pub = new grip.GripPubControl({control_uri: "<endpoint_uri>"});
Demo
Included in this package is a demo that publishes a message using a Grip Stream
to a sample server that is proxied behind the open-source Pushpin (https://pushpin.org/) server.
To run the demo:
- Clone this repository, then build the commonjs build of this library
npm install
npm run build-commonjs
- Start the server process. This runs on
localhost:3000
.
node demo/server
- Install Pushpin (see https://pushpin.org/docs/install/)
- Make sure Pushpin points to
localhost:3000
.
routes
file:
* localhost:3000
- Start Pushpin.
pushpin
- In another terminal window, open a long-lived connection to the
pushpin stream.
curl http://localhost:7999/long-poll
- In another terminal window, run the publish demo file.
node demo/publish test "Message"
- In the window that you opened in step 6, you should see the test message.
License
(C) 2015, 2020 Fanout, Inc.
Licensed under the MIT License, see file COPYING for details.