npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

rethinkdbdash-websocket-server

v0.3.3

Published

WebSocket server that proxies to a RethinkDB instance. Supports query validation.

Downloads

4

Readme

npm version Travis

rethinkdb-websocket-server

Node.js WebSocket server that proxies to a RethinkDB instance. Supports query validation.

What is this?

This library attaches to a node.js http.Server and listens for incoming WebSocket connections at a specified path. For each incoming WebSocket connection, it opens a new TCP socket to a specified RethinkDB instance, and forwards traffic in both directions between the WebSocket and the RethinkDB socket until either side disconnects.

Each query sent from the WebSocket is parsed and validated before being forwarded to RethinkDB. This is done using a whitelist of pattern queries, described in the involved example below.

The provided WebSocket server can be used in conjunction with any of the following clients:

How do I use this?

This package should be installed with npm and should run in a node.js http.Server.

Simple example

Below is a simple example of a script that listens at ws://localhost:8000/ for incoming WebSocket connections. It blindly forwards all queries to a RethinkDB instance running locally at the default 28015 port.

var http = require('http');
var wsListen = require('rethinkdb-websocket-server').listen;

var httpServer = http.createServer();
wsListen({httpServer: httpServer, unsafelyAllowAnyQuery: true});
httpServer.listen(8000);

Involved example

In this example, we listen for WebSocket connections at ws://localhost:8000/rethinkApi and forward traffic to a RethinkDB instance running at rethink01.example.com:28015. We also serve static files in the assets directory over HTTP using express.

Rather than enabling unsafelyAllowAnyQuery, we explicitly set up a query whitelist. This one only allows two query patterns:

  1. Queries that list turtles with the same herdId as the authenticated user
  2. Queries that insert turtles with a non-empty name and a herdId referring to the primary key of an object in the herds table

In order to validate queries against the authenticated user, we create a "session" object from the query params of the WebSocket URL. In this case, the browser connects to ws://localhost:8000/rethinkApi?userId=foo&authToken=bar, the sessionCreator function looks up that user in the database, and user.curHerdId is stored in the custom session object that we have access to when validating queries from this client.

Note: in production, you should enable secure websockets so sensitive data is not vulnerable.

As you are developing, incoming queries that don't validate against the whitelist will be logged to console in a format that you can copy and paste directly into your JavaScript source file. For dynamic queries, you'll likely want to generalize the pattern using function(actual, refs, session) terms, RQ.ref() terms, and the .validate() method. Using ES6 arrow functions can make this a bit less verbose.

var express = require('express');
var http = require('http');
var Promise = require('bluebird');
var RethinkdbWebsocketServer = require('rethinkdb-websocket-server');
var r = RethinkdbWebsocketServer.r;
var RQ = RethinkdbWebsocketServer.RQ;

var options = {};
options.dbHost = 'rethink01.example.com';
options.dbPort = 28015;

var rethinkConn = Promise.promisify(r.connect)({
  host: options.dbHost,
  port: options.dbPort,
  db: 'test',
});
function runQuery(query) {
  return rethinkConn.then(function(conn) {
    return query.run(conn);
  });
}

options.sessionCreator = function(urlQueryParams) {
  var userQuery = r.table('users').get(urlQueryParams.userId);
  return runQuery(userQuery).then(function(user) {
    if (user && user.authToken === urlQueryParams.authToken) {
      return {curHerdId: user.herdId};
    } else {
      return Promise.reject('Invalid auth token');
    }
  });
};

options.queryWhitelist = [
  // r.table('turtles').filter({herdId: curHerdId})
  RQ(
    RQ.FILTER(
      RQ.TABLE("turtles"),
      {"herdId": RQ.ref('herdId')}
    )
  ).opt("db", RQ.DB("test"))
  .validate(function(refs, session) {
    return session.curHerdId === refs.herdId;
  }),

  // r.table('turtles').insert({herdId: 'alpha-squadron', name: 'Speedy'})
  RQ(
    RQ.INSERT(
      RQ.TABLE("turtles"),
      {
        "herdId": RQ.ref('herdId'),
        "name": function(actual, refs, session) {
          return typeof actual === 'string' && actual.trim();
        },
      }
    )
  ).opt("db", RQ.DB("test"))
  .validate(function(refs) {
    var herdId = refs.herdId;
    if (typeof herdId !== 'string') return false;
    var validHerdQuery = r.table('herds').get(herdId).ne(null);
    return runQuery(validHerdQuery);
  }),
];

var app = express();
app.use('/', express.static('assets'));
var httpServer = http.createServer(app);
options.httpServer = httpServer;
options.httpPath = '/rethinkApi';

RethinkdbWebsocketServer.listen(options);
httpServer.listen(8000);

Query whitelist syntax

Before rethinkdb-websocket-server 0.3.0, the syntax for expressing whitelist queries closely reflected the RethinkDB JSON protocol sent over the wire. The RQ object made this a bit more readable, e.g. RQ.TABLE("turtles") instead of [15, ["turtles"]].

Starting with version 0.3.0, there is a new experimental "vanilla" syntax that looks much closer to ordinary ReQL. As this becomes more stable, the vanilla syntax will be recommended and the old RQ syntax will be deprecated and ultimately removed.

In the 0.3 releases, the whitelist can contain pattern queries from either syntax. This makes it easy to migrate queries to the new syntax one at a time. Incoming queries are logged in both formats.

At least until there is a stable RethinkDB release with the toString() fix, the vanilla syntax will remain experimental.

RQ/vanilla syntax comparison

  • Both RQ and vanilla syntax chain .validate(fn) after queries to add validation functions.
  • Both RQ and vanilla syntax chain .opt(key, value) after queries to set query options like db and durability.
    • However, the value argument can differ in syntax: .opt("db", RQ.DB("test")) vs .opt("db", r.db("test").
  • RQ.ref(refName) in the RQ syntax has been changed to RP.ref(refName)
    • RP stands for ReQL Pattern, and separating the RQ and RP object helps ensure you are using the right syntax version.
  • Pattern functions like function(actual, refs, session) {...} in the RQ syntax must now be wrapped in RP.check(function(actual, refs, session) {...}.
    • This is necessary because JavaScript functions would otherwise be ambiguous in the ReQL AST. I.e. r.filter(function(x) {...}) should only be able to specify a filter function, not a whitelist pattern function.
  • The biggest difference: the underlying syntax is completely different.
    • In the RQ syntax, the expressions represent the underlying JSON protocol, whereas the vanilla syntax is the same as writing ReQL with the JavaScript driver. The following queries are equivalent:
    • RQ(RQ.FILTER(RQ.TABLE("turtles"), {"herdId": RQ.ref('herdId')}))
    • r.table("turtles").filter({"herdId": RP.ref('herdId')})

Vanilla syntax example

Below is the query whitelist excerpt of the "involved example" above, rewritten with the vanilla syntax:

var RP = RethinkdbWebsocketServer.RP;

options.queryWhitelist = [
  r.table('turtles')
   .filter({"herdId": RP.ref('herdId')})
   .opt("db", r.db("test"))
   .validate(function(refs, session) {
     return session.curHerdId === refs.herdId;
   }),

  r.table('turtles')
   .insert({
     "herdId": RP.ref('herdId'),
     "name": RP.check(function(actual, refs, session) {
       return typeof actual === 'string' && actual.trim();
     }),
   })
   .opt("db", r.db("test"))
   .validate(function(refs) {
     var herdId = refs.herdId;
     if (typeof herdId !== 'string') return false;
     var validHerdQuery = r.table('herds').get(herdId).ne(null);
     return runQuery(validHerdQuery);
   }),
];

Written a bit more concisely, and with some ES6 syntax, this becomes:

var {RP} = RethinkdbWebsocketServer;

options.queryWhitelist = [
  r.table('turtles')
   .filter({herdId: RP.ref('herdId')})
   .opt("db", r.db("test"))
   .validate(({herdId}, session) => session.curHerdId === herdId),

  r.table('turtles')
   .insert({
     herdId: RP.ref('herdId'),
     name: RP.check(x => typeof x === 'string' && x.trim())
   })
   .opt("db", r.db("test"))
   .validate(({herdId}) => (
     typeof herdId !== 'string' &&
     runQuery(r.table('herds').get(herdId).ne(null))
   )),
];