

VERSION 3
Introduction
Happn is a mini database combined with pub/sub, the system stores json objects on paths. Paths can be queried using wildcard syntax. The happn client can run in the browser or in a node process. Happn clients can subscribe to events on paths, events happn when data is changed by a client on a path, either by a set or a remove operation.
Happn stores its data in a collection called 'happn' by default on your mongodb/nedb. The happn system is actually built to be a module, this is because the idea is that you will be able to initialize a server in your own code, and possibly attach your own plugins to various system events.
A paid for alternative to happn would be firebase
Technologies used:
Happn uses Primus to power websockets for its pub/sub framework and mongo or nedb depending on the mode it is running in as its data store, the API uses connect.
nedb as the embedded database, although we have forked it happn's purposes here
VERSION 2 and what has changed
Happn v2 can be found here
changes are:
(1) more modular layout, services are broken up into logical modules
(2) introduction of a queue service
(3) introduction of a protocol service, this allows for the creation of protocol plugins that takemessages of the inbound and outbound queues and convert them into happn messages, essentially means we are able to use different protocols to talk to happn (ie. MQTT)
(4) simplified intra process client instantiation
(5) intra process client shares the same code as the websockets client, using a special intra-proc socket, instead of a primus spark
Getting started
npm install happn-3
You need NodeJS and NPM of course, you also need to know how node works (as my setup instructions are pretty minimal)
To run the tests, clone the repo, npm install then npm test:
git clone https://github.com/happner/happn-3.git
npm install
npm test
But if you want to run your own service do the following:
Create a directory you want to run your happn in, create a node application in it - with some kind of main.js and a package.json
In node_modules/happn/test in your folder, the 1_eventemitter_embedded_sanity.js and 2_websockets_embedded_sanity.js scripts demonstrate the server and client interactions shown in the following code snippets
starting service:
The service runs on port 55000 by default - the following code snippet demonstrates how to instantiate a server.
var happn = require('happn')
var happnInstance;
happn.service.create({
utils: {
logLevel: 'error',
}
},
function (e, happn) {
if (e)
return callback(e);
happnInstance = happn;
happnInstance.log.info('server up');
});
In your console, go to your application folder and runnode mainyour server should start up and be listening on your port of choice.
Connecting to Happn
Using node:
var happn = require('happn');
var my_client_instance;
happn.client.create([options], function(e, instance) {
my_client_instance = instance;
});
To use the browser client, make sure the server is running, and reference the client javascript with the url pointing to the running server instances port and ip address like so:
<script type="text/javascript" src="http://localhost:55000/browser_client"></script>
<script>
var my_client_instance;
HappnClient.create([options], function(e, instance) {
my_client_instance = instance;
});
</script>
Intra-process client:
service.create(function (e, happnInst) {
if (e) return callback(e);
happnInstance = happnInst;
happnInstance.services.session.localClient(function(e, instance){
var myLocalClient = instance;
});
});
SET
Puts the json in the branch e2e_test1/testsubscribe/data, creates the branch if it does not exist
my_client_instance.set('e2e_test1/testsubscribe/data/', {property1:'property1',property2:'property2',property3:'property3'}, {noPublish:true}, function(e, result){
});
NB - by setting the option merge:true, the data at the end of the path is not overwritten by your json, it is rather merged with the data in your json, overwriting the fields you specify in your set data, but leaving the fields that are already at that branch.
SET SIBLING
sets your data to a unique path starting with the path you passed in as a parameter, suffixed with a random short id
my_client_instance.setSibling('e2e_test1/siblings', {property1:'sib_post_property1',property2:'sib_post_property2'}, function(e, results){
GET
Gets the data living at the specified branch
my_client_instance.get('e2e_test1/testsubscribe/data',
null,
function(e, results){
You can also use wildcards, gets all items with the path starting e2e_test1/testsubscribe/data
my_client_instance.get('e2e_test1/testsubscribe/data*',
null,
function(e, results){
results.map(function(item){
});
You can also just get paths, without data
my_client_instance.getPaths('e2e_test1/testwildcard/*', function(e, results){
SEARCH
You can pass mongo style search parameters to look for data sets within specific key ranges
var options = {
fields: {"name": 1},
sort: {"name": 1},
limit: 1
}
var criteria = {
$or: [{"region": {$in: ['North', 'South', 'East', 'West']}},
{"town": {$in: ['North.Cape Town', 'South.East London']}}],
"surname": {$in: ["Bishop", "Emslie"]}
}
publisherclient.get('/users/*', {
criteria: criteria,
options: options
},
function (e, search_results) {
search_results.map(function(user){
if (user.name == 'simon')
throw new Error('stay away from this chap, he is dodgy');
});
}
);
DELETE
deletes the data living at the specified branch
my_client_instance.remove('/e2e_test1/testsubscribe/data/delete_me', null, function(e, result){
if (!e)
EVENTS
you can listen to any SET & REMOVE events happening in your data - you can specifiy a path you want to listen on or you can listen to all SET and DELETE events using a catch-all listener
Specific listener:
my_client_instance.on('/e2e_test1/testsubscribe/data/delete_me',
{event_type:'remove',
count:0},
function(
message,
meta){
},
function(e){
});
Catch all listener:
my_client_instance.onAll(function(
message,
meta){
},
function(e){
});
EVENT DATA
- you can grab the data you are listening for immediately either by causing the events to be emitted immediately on successful subscription or you can have the data returned as part of the subscription callback using the initialCallback and initialEmit options respectively*
listenerclient.on('/e2e_test1/testsubscribe/data/values_on_callback_test/*',
{"event_type": "set",
"initialCallback":true
}, function (message) {
expect(message.updated).to.be(true);
callback();
}, function(e, reference, response){
if (e) return callback(e);
try{
expect(response.length).to.be(2);
expect(response[0].test).to.be('data');
expect(response[1].test).to.be('data1');
listenerclient.set('/e2e_test1/testsubscribe/data/values_on_callback_test/1', {"test":"data", "updated":true}, function(e){
if (e) return callback(e);
});
}catch(e){
return callback(e);
}
});
listenerclient.on('/e2e_test1/testsubscribe/data/values_emitted_test/*',
{"event_type": "set",
"initialEmit":true
}, function (message, meta) {
caughtEmitted++;
if (caughtEmitted == 2){
expect(message.test).to.be("data1");
callback();
}
}, function(e){
if (e) return callback(e);
});
UNSUBSCRIBING FROM EVENTS
//use the .off method to unsubscribe from a specific event (the handle is returned by the .on callback) or the .offPath method to unsubscribe from all listeners on a path:
var currentListenerId;
var onRan = false;
var pathOnRan = false;
listenerclient.on('/e2e_test1/testsubscribe/data/on_off_test', {event_type: 'set', count: 0}, function (message) {
if (pathOnRan) return callback(new Error('subscription was not removed by path'));
else pathOnRan = true;
listenerclient.offPath('/e2e_test1/testsubscribe/data/on_off_test', function (e) {
if (e)
return callback(new Error(e));
listenerclient.on('/e2e_test1/testsubscribe/data/on_off_test', {event_type: 'set', count: 0},
function (message) {
if (onRan) return callback(new Error('subscription was not removed'));
else {
onRan = true;
listenerclient.off(currentListenerId, function (e) {
if (e)
return callback(new Error(e));
publisherclient.set('/e2e_test1/testsubscribe/data/on_off_test', {"test":"data"}, function (e, setresult) {
if (e) return callback(new Error(e));
setTimeout(callback, 2000);
});
});
}
},
function (e, listenerId) {
if (e) return callback(new Error(e));
currentListenerId = listenerId;
publisherclient.set('/e2e_test1/testsubscribe/data/on_off_test', {"test":"data"}, function (e, setresult) {
if (e) return callback(new Error(e));
});
});
});
}, function (e, listenerId) {
if (e) return callback(new Error(e));
currentListenerId = listenerId;
publisherclient.set('/e2e_test1/testsubscribe/data/on_off_test', {"test":"data"}, function (e) {
if (e) return callback(new Error(e));
});
});
TAGGING
You can do a set command and specify that you want to tag the data at the end of the path (or the data that is created as a result of the command), tagging will take a snapshot of the data as it currently stands, and will save the snapshot to a path that starts with the path you specify, and a '/' with the tag you specify at the end
var randomTag = require('shortid').generate();
my_client_instance.set('e2e_test1/test/tag', {property1:'property1',property2:'property2',property3:'property3'}, {tag:randomTag}, function(e, result){
MERGING
you can do a set command and specify that you want to merge the json you are pushing with the existing dataset, this means any existing values that are not in the set json but exist in the database are persisted
my_client_instance.set('e2e_test1/testsubscribe/data/', {property1:'property1',property2:'property2',property3:'property3'}, {merge:true}, function(e, result){
});
SECURITY SERVER
happn server instances can be secured with user and group authentication, a default user and group called _ADMIN is created per happn instance, the admin password is 'happn' but is configurable (MAKE SURE PRODUCTION INSTANCES DO NOT RUN OFF THE DEFAULT PASSWORD)
var happn = require('happn')
var happnInstance;
happn.service.create({secure:true, adminUser:{password:'testPWD'}},
function (e, instance) {
if (e)
return callback(e);
happnInstance = instance;
});
at the moment, adding users, groups and permissions can only be done by directly accessing the security service, to see how this is done - please look at the functional test for security
SECURITY CLIENT
the client needs to be instantiated with user credentials and with the secure option set to true to connect to a secure server
var happn = require('happn');
happn.client.create({config:{username:'_ADMIN', password:'testPWD'}, secure:true},function(e, instance) {
SECURITY PROFILES
profiles can be configured to fit vdifferent session types
var serviceConfig = {
services:{
data:{
},
security: {
config: {
sessionTokenSecret:"TESTTOKENSECRET",
keyPair: {
privateKey: 'Kd9FQzddR7G6S9nJ/BK8vLF83AzOphW2lqDOQ/LjU4M=',
publicKey: 'AlHCtJlFthb359xOxR5kiBLJpfoC2ZLPLWYHN3+hdzf2'
},
profiles:[
{
name:"web-session",
session:{
$and:[{
user:{username:{$eq:'WEB_SESSION'}},
type:{$eq:0}
}]
},
policy:{
ttl: "4 seconds",
inactivity_threshold:2000
}
}, {
name:"rest-device",
session:{
$and:[{
user:{groups:{
"REST_DEVICES" : { $exists: true }
}},
type:{$eq:0}
}]},
policy: {
ttl: 2000
}
},{
name:"trusted-device",
session:{
$and:[{
user:{groups:{
"TRUSTED_DEVICES" : { $exists: true }
}},
type:{$eq:1}
}]},
policy: {
ttl: 2000,
permissions:{
'/TRUSTED_DEVICES/*':{actions: ['*']}
}
}
},{
name:"specific-device",
session:{$and:[{
type:{$in:[0,1]},
ip_address:{$eq:'127.0.0.1'}
}]},
policy: {
ttl: Infinity,
inactivity_threshold:Infinity,
permissions:{
'/SPECIFIC_DEVICE/*':{actions: ['get','on']}
}
}
},
{
name:"non-reusable",
session:{$and:[{
user:{groups:{
"LIMITED_REUSE" : { $exists: true }
}},
type:{$in:[0,1]}
}]},
policy: {
usage_limit:2
}
}, {
name:"default-stateful",
session:{
$and:[{type:{$eq:1}}]
},
policy: {
ttl: Infinity,
inactivity_threshold:Infinity
}
}, {
name:"default-stateless",
session:{
$and:[{type:{$eq:0}}]
},
policy: {
ttl: 60000 * 10,
inactivity_threshold:Infinity
}
}
]
}
}
}
};
the test that clearly demonstrates profiles can be found here
WEB PATH LEVEL SECURITY
the http/s server that happn uses can also have custom routes associated with it, when the service is run in secure mode - only people who belong to groups that are granted @HTTP permissions that match wildcard patterns for the request path can access resources on the paths, here is how we grant permissions to paths:
var happn = require('happn')
var happnInstance;
happn.service.create({secure:true, adminUser:{password:'testPWD'}},
function (e, instance) {
if (e)
return callback(e);
happnInstance = instance;
var testGroup = {
name:'TEST GROUP',
custom_data:{
customString:'custom1',
customNumber:0
}
}
testGroup.permissions = {
'/@HTTP/secure/route/*':{actions:['get']},
'/@HTTP/secure/another/route/test':{actions:['put','post']}
};
happnInstance.services.security.upsertGroup(testGroup, {}, function(e, group){
happnInstance.connect.use('/secure/route/test', function(req, res, next){
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"secure":"value"}));
});
happnInstance.connect.use('/secure/another/route/test', function(req, res, next){
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"secure":"value"}));
});
});
});
logging in with a secure client gives us access to a token that can be used, either by embedding the token in a cookie called happn_token or a query string parameter called happn_token, if the login has happened on the browser, the happn_token is autmatically set by default
var happn = require('happn');
happn.client.create({config:{username:'_ADMIN', password:'testPWD'}, secure:true},function(e, instance) {
var http = require('http');
var options = {
host: '127.0.0.1',
port:55000,
path:'/secure/route/test'
}
if (use_query_string)
options.path += '?happn_token=' + instance.session.token;
else
options.headers = {'Cookie': ['happn_token=' + instance.session.token]}
http.request(options, function(response){
}).end();
});
HTTPS SERVER
happn can also run in https mode, the config has a section called transport
var config = {
transport:{
mode:'https',
cert: '-----BEGIN CERTIFICATE-----\n[CERT ETC...]\n-----END CERTIFICATE-----',
key: '-----BEGIN RSA PRIVATE KEY-----\n[KEY ETC...]\n-----END RSA PRIVATE KEY-----'
}
}
var config = {
transport:{
mode:'https',
certPath:'home/my_cert.pem',
keyPath:'home/my_key.rsa'
}
}
var config = {
transport:{
mode:'https'
}
}
var happn = require('../lib/index')
var service = happn.service;
var happnInstance;
service.create(config ...
HTTPS CLIENT
NB - the client must now be initialized with a protocol of https, and if it is the node based client and the cert and key file was self signed, the allowSelfSignedCerts option must be set to true
var happn = require('happn');
happn.client.create({config:{protocol:'https', allowSelfSignedCerts:true}},function(e, instance) {
...
PAYLOAD ENCRYPTION
if the server is running in secure mode, it can also be configured to encrypt payloads between it and socket clients, this means that the client must include a keypair as part of its credentials on logging in, to see payload encryption in action plase go to the following test
PUBSUB MIDDLEWARE
incoming and outgoing packets delivery can be intercepted on the server side, this is how payload encryption works, to add a custom middleware you need to add it to the pubsub service's configuration, a middleware must adhere to a specific interface, as demonstrated below:
var testMiddleware = {
incomingCount:0,
outgoingCount:0,
incoming:function(packet, next){
packet.modified = true;
this.incomingCount++;
next();
},
outgoing:function(packet, next){
packet.modified = true;
this.outgoingCount++;
next();
}
};
var happn_service = happn.service;
var test_client = happn.client;
var testConfig = {
secure: true,
port:44445,
services:{
pubsub:{
config:{
transformMiddleware:[{instance:testMiddleware}]
}
}
}
};
service.create(testConfig,
function (e, happnInst) {
if (e)
return callback(e);
serviceInst = happnInst;
happn_client.create({
config: {
port:44445,
username: '_ADMIN',
password: 'happn'
},
info:{
from:'startup'
}
}, function (e, instance) {
if (e) return callback(e);
clientInst = instance;
expect(testMiddleware.incomingCount > 0).to.be(true);
expect(testMiddleware.outgoingCount > 0).to.be(true);
clientInst.disconnect(function(){
serviceInst.stop(callback);
});
});
}
);
TESTING WITH KARMA
testing payload encryption on the browser:
gulp --gulpfile test/test-browser/gulp-01.js
OTHER PLACES WHERE HAPPN IS USED:
HAPPNER - an experimental application engine that uses happn for its nervous system, see: www.github.com/happner/happner