UNPKG

70.1 kBMarkdownView Raw
1[![npm](https://img.shields.io/npm/v/happn-3.svg)](https://www.npmjs.com/package/happn-3)
2[![Build Status](https://travis-ci.org/happner/happn-3.svg?branch=master)](https://travis-ci.org/happner/happn-3)
3[![Coverage Status](https://coveralls.io/repos/happner/happn-3/badge.svg?branch=master&service=github)](https://coveralls.io/github/happner/happn-3?branch=master)
4[![David](https://img.shields.io/david/happner/happn-3.svg)](https://img.shields.io/david/happner/happn-3.svg)
5
6<img src="https://raw.githubusercontent.com/happner/happner-website/master/images/HAPPN%20Logo%20B.png" width="300"></img>
7
8VERSION 3
9---------
10
11Introduction
12-------------------------
13
14Happn 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.
15
16Happn 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.
17
18A paid for alternative to happn would be [firebase](https://www.firebase.com)
19
20Key technologies used:
21Happn uses [Primus](https://github.com/primus/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](https://github.com/senchalabs/connect).
22[nedb](https://github.com/louischatriot/nedb) as the embedded database, although we have forked it happn's purposes [here](https://github.com/happner/happn-nedb)
23
24VERSION 2 and what has changed
25------------------------------
26
27Happn v2 can be found [here](https://github.com/happner/happn)
28
29changes are:
30
311. more modular layout, services are broken up into logical modules
322. introduction of a queue service
333. introduction of a protocol service, this allows for the creation of protocol plugins that take messages 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)
344. simplified intra process client instantiation
355. intra process client shares the same code as the websockets client, using a special intra-proc socket, instead of a primus spark
366. database is now versioned and must be in sync with package.json
37
38[Migration plan from happn 2 to happn-3](https://github.com/happner/happn-3/blob/master/docs/migration-plan.md)
39--------------------------------------
40
41Getting started
42---------------------------
43
44```bash
45npm install happn-3
46```
47
48You need NodeJS and NPM of course, you also need to know how node works (as my setup instructions are pretty minimal)
49To run the tests, clone the repo, npm install then npm test:
50
51```bash
52git clone https://github.com/happner/happn-3.git
53npm install
54npm test
55```
56
57But if you want to run your own service do the following:
58Create 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
59
60*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*
61
62[configuration](https://github.com/happner/happn-3/blob/master/docs/config.md)
63--------------------------------------
64*for full service and client configuration options*
65
66starting service:
67-------------------------------------------------------
68
69The service runs on port 55000 by default - the following code snippet demonstrates how to instantiate a server.
70
71```javascript
72var happn = require('happn-3')
73var happnInstance; //this will be your server instance
74
75//we are using a compact default config here, port defaults to 55000
76
77 happn.service.create({
78 utils: {
79 logLevel: 'error',
80 // see happn-logger module for more config options
81 }
82},
83function (e, happn) {
84 if (e)
85 return callback(e);
86
87 happnInstance = happn; //here it is, your server instance
88 happnInstance.log.info('server up');
89
90});
91
92```
93In your console, go to your application folder and run*node main*your server should start up and be listening on your port of choice.
94
95THE HAPPN CLIENT
96-------------------------
97
98Using node:
99
100```javascript
101var happn = require('happn-3');
102var my_client_instance; //this will be your client instance
103
104/**
105example options are :
106{
107 host: "127.0.0.1", //(default)
108 port: 55000, //(default)
109 username: 'username', //only necessary if server is secure
110 password: 'password', //only necessary if server is secure
111 socket: {
112 pingTimeout: 45e3 // 45 seconds by default, if set to false the client
113 // will not detect connection failures and emit the
114 // 'reconnect scheduled event'
115 }
116}
117**/
118
119happn.client.create([options], function(e, instance) {
120
121 //instance is now connected to the server listening on port 55000
122 my_client_instance = instance;
123});
124
125```
126
127To 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:
128
129```html
130<script type="text/javascript" src="http://localhost:55000/browser_client"></script>
131<script>
132
133var my_client_instance;
134
135HappnClient.create([options], function(e, instance) {
136
137 //instance is now connected to the server listening on port 55000
138 my_client_instance = instance;
139
140});
141
142</script>
143```
144Intra-process / local client:
145-----------------------------
146*although we have direct access to the services (security included) - this method still requires a username and password if the happn instance is secure*
147```javascript
148
149service.create(function (e, happnInst) {
150
151 if (e) throw e;
152
153 happnInstance = happnInst;
154
155 happnInstance.services.session.localClient(/*credentials argument only necessary if secure*/{username:'test', password:'test'}, function(e, instance){
156
157 var myLocalClient = instance;
158
159 //myLocalClient.set(...)
160
161 });
162
163 });
164
165```
166
167Intra-process / local admin client:
168-----------------------------------------------------
169*will pass back a client with admin rights no username or password necessary, this is because we have direct access to the services, security included*
170```javascript
171
172service.create(function (e, happnInst) {
173
174 if (e) throw e;
175
176 happnInstance = happnInst;
177
178 happnInstance.services.session.localAdminClient(function(e, instance){
179
180 var myLocalAdminClient = instance;
181
182 //myLocalClient.set(...)
183
184 });
185
186 });
187
188```
189
190##NB: NODE_ENV environment variable
191*Set your NODE_ENV variable to "production" when running happn in a production environment, otherwise your browser client file will be regenerated every time the happn server is restarted.*
192
193SET
194-------------------------
195
196*Puts the json in the branch e2e_test1/testsubscribe/data, creates the branch if it does not exist*
197
198```javascript
199
200//the noPublish parameter means this data change wont be published to other subscribers, it is false by default
201//there are a bunch other parameters - like noStore (the json isnt persisted, but the message is published)
202
203my_client_instance.set('e2e_test1/testsubscribe/data/', {property1:'property1',property2:'property2',property3:'property3'}, {noPublish:true}, function(e, result){
204
205 //your result object has a special _meta property that contains its actual _id, path, created and modified dates
206 //so you get back {property1:'property1',property2:'property2',property3:'property3', _meta:{path:'e2e_test1/testsubscribe/data/', created:20151011893020}}
207
208
209});
210
211```
212
213*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.*
214
215PUBLISH
216-------------------------
217
218*publishes the json to all topic subscribers that match e2e_test1/testsubscribe/data, the data is not stored or returned in the response, only the _meta is returned*
219
220```javascript
221
222my_client_instance.publish('e2e_test1/testsubscribe/data/', {property1:'property1',property2:'property2',property3:'property3'}, function(e, result){
223
224 //your result does not contain the changed data, but it still has the _meta property:
225 result = {
226 _meta:{
227 published: true,
228 type: 'response',
229 status: 'ok',
230 eventId: 4, //eventId matching event handler on client
231 sessionId: '[guid: your current session id]'
232 }
233 }
234});
235
236```
237
238SET SIBLING
239-------------------------
240
241*sets your data to a unique path starting with the path you passed in as a parameter, suffixed with a random short id*
242
243```javascript
244 my_client_instance.setSibling('e2e_test1/siblings', {property1:'sib_post_property1',property2:'sib_post_property2'}, function(e, results){
245 //you get back {property1:'sib_post_property1',property2:'sib_post_property2', _meta:{path:'e2e_test1/siblings/yCZ678__'}}
246 //you would get all siblings by querying the path e2e_test1/siblings*
247```
248
249INCREMENT
250-------------------------
251
252*allows a counter to be incremented by an increment value*
253
254*single guage - causing on event:*
255```javascript
256
257 //listen on a path
258 myclient.on('my/increment/guage', function(data){
259
260 //NB; the data on the event will look like this
261 //{guage:'counter', value:1}
262
263 myclient.get('my/increment/guage', function(e, gotIncrementedData){
264
265 expect(gotIncrementedData[data.value].value).to.be(1);
266 });
267
268 }, function(e){
269
270 if (e) throw e;
271
272 //increment convenience method
273 myclient.increment('my/increment/guage', 1, function(e){
274
275 if (e) throw e;
276 });
277 });
278```
279
280*increment multiple times, guage defaults to counter and increment value is 1:*
281```javascript
282 var async = require('async');
283
284 async.timesSeries(10, function (time, timeCB) {
285
286 myclient.increment('my/guage', function (e) {
287
288 timeCB(e);
289 });
290
291 }, function (e) {
292
293 myclient.get('my/guage', function (e, result) {
294
295 expect(result['counter-0'].value).to.be(10);
296
297 });
298 });
299```
300
301*multiple guages on the same path:*
302```javascript
303 var async = require('async');
304
305 async.timesSeries(10, function (time, timeCB) {
306
307 myclient.increment('my/dashboard', 'counter-' + time, 1, function (e) {
308
309 timeCB(e);
310 });
311
312 }, function (e) {
313
314 myclient.get('my/dashboard', function (e, result) {
315
316 expect(result['counter-0'].value).to.be(1);
317 expect(result['counter-1'].value).to.be(1);
318 expect(result['counter-2'].value).to.be(1);
319 expect(result['counter-3'].value).to.be(1);
320 expect(result['counter-4'].value).to.be(1);
321 expect(result['counter-5'].value).to.be(1);
322 expect(result['counter-6'].value).to.be(1);
323 expect(result['counter-7'].value).to.be(1);
324 expect(result['counter-8'].value).to.be(1);
325 expect(result['counter-9'].value).to.be(1);
326
327 });
328 });
329```
330
331*decrement a guage with a minus value:*
332```javascript
333
334 var incrementCount = 0;
335
336 //listening on the event
337 myclient.on('my/test/guage', function (data) {
338
339 incrementCount++;
340
341 if (incrementCount == 1){
342 expect(data.value).to.be(3);
343 expect(data.guage).to.be('custom');
344 }
345
346 if (incrementCount == 2){
347 expect(data.value).to.be(1);
348 expect(data.guage).to.be('custom');
349 }
350
351 }, function (e) {
352
353 myclient.increment('my/test/guage', 'custom', 3, function (e) {
354
355 myclient.get('my/test/guage', function (e, result) {
356
357 expect(result['custom'].value).to.be(1);
358
359 myclient.increment('my/test/guage', 'custom', -2, function (e) {
360
361 myclient.get('my/dashboard', function (e, result) {
362
363 expect(result['custom'].value).to.be(1);
364 });
365 });
366 });
367 });
368 });
369```
370
371GET
372---------------------------
373
374*Gets the data living at the specified branch*
375
376```javascript
377my_client_instance.get('e2e_test1/testsubscribe/data',
378 null, //options
379 function(e, results){
380 //results is your data, if you used a wildcard in your path, you get back an array
381 //if you used an explicit path, you get back your data as the object on that path
382
383```
384
385*You can also use wildcards, gets all items with the path starting e2e_test1/testsubscribe/data*
386
387```javascript
388my_client_instance.get('e2e_test1/testsubscribe/data*',
389 null,
390 function(e, results){
391 //results is your data
392 results.map(function(item){
393
394 });
395```
396
397*You can also just get paths, without data*
398
399```javascript
400my_client_instance.getPaths('e2e_test1/testwildcard/*', function(e, results){
401```
402
403SEARCH
404---------------------------
405
406*You can pass mongo style search parameters to look for data sets within specific key ranges, using limit and skip*
407
408```javascript
409
410 var options = {
411 fields: {"name": 1},
412 sort: {"name": 1},
413 limit: 10,
414 skip:5
415 }
416
417 var criteria = {
418 $or: [{"region": {$in: ['North', 'South', 'East', 'West']}},
419 {"town": {$in: ['North.Cape Town', 'South.East London']}}],
420 "surname": {$in: ["Bishop", "Emslie"]}
421 }
422
423 publisherclient.get('/users/*', {
424 criteria: criteria,
425 options: options
426 },
427 function (e, search_results) {
428 //and your results are here
429 search_results.map(function(user){
430 if (user.name == 'simon')
431 throw new Error('stay away from this chap, he is dodgy');
432 });
433 }
434 );
435
436```
437
438*Using $regex is a bit different, you can pass in a regex string representation or an array with regex string as the first element and modifiers such as i as the second element depending on whether you want to pass in regex arguments such as case insensitive (i)*
439
440```javascript
441
442 var options = {
443 fields: {"name": 1},
444 sort: {"name": 1},
445 limit: 1
446 }
447
448 var criteria = {
449 "name": {
450 "$regex": [".*simon.*", "i"]//array with regex first, then "i" argument, does Regex.apply(null, [your array]) in backend
451 }
452 };
453
454 //alternatively - for a case sensitive simpler search:
455 var criteriaSimple = {
456 "name": {
457 "$regex": ".*simon.*"//array with regex first, then "i" argument, does Regex.apply(null, [your array]) in backend
458 }
459 };
460
461 publisherclient.get('/users/*', {
462 criteria: criteria,//(or criteriaSimple)
463 options: options
464 },
465 function (e, search_results) {
466 //and your results are here
467 search_results.map(function(user){
468 if (user.name == 'simon')
469 throw new Error('stay away from this chap, he is dodgy');
470 });
471 }
472 );
473
474```
475
476*You can also use skip and limit for paging:*
477
478```javascript
479
480var totalRecords = 100;
481var pageSize = 10;
482var expectedPages = totalRecords / pageSize;
483var indexes = [];
484
485for (let i = 0; i < totalRecords; i++) indexes.push(i);
486
487for (let index of indexes){
488 await searchClient.set('series/horror/' + index, {
489 name: 'nightmare on elm street',
490 genre: 'horror',
491 episode:index
492 });
493 await searchClient.set('series/fantasy/' + index, {
494 name: 'game of thrones',
495 genre: 'fantasy',
496 episode:index
497 });
498}
499
500var options = {
501 sort: {
502 "_meta.created": -1
503 },
504 limit: pageSize
505};
506
507var criteria = {
508 "genre": "horror"
509};
510
511var foundPages = [];
512
513for (let i = 0; i < expectedPages; i++){
514 options.skip = foundPages.length;
515 let results = await searchClient.get('series/*', {
516 criteria: criteria,
517 options: options
518 });
519 foundPages = foundPages.concat(results);
520}
521
522let allResults = await searchClient.get('series/*', {
523 criteria: criteria,
524 options: {
525 sort: {
526 "_meta.created": -1
527 }
528 }
529 });
530
531expect(allResults.length).to.eql(foundPages.length);
532expect(allResults).to.eql(foundPages);
533```
534
535DELETE / REMOVE
536---------------------------
537
538*deletes the data living at the specified branch*
539
540```javascript
541 my_client_instance.remove('/e2e_test1/testsubscribe/data/delete_me', null, function(e, result){
542 if (!e)
543 //your item was deleted, result.payload is an object that lists the amount of objects deleted
544```
545
546EVENTS
547----------------------------
548
549*you can listen to any SET & REMOVE events happening in your data - you can specify a path you want to listen on or you can listen to all SET and DELETE events using a catch-all listener, the * character denotes a wildcard*
550
551NB about wildcards:
552-------------------
553
554As of version 8.0.0 the wildcard is a whole word, and the / is used to denote path segments - ie: to get all events for a set or remove with path /my/test/event you need to subscribe to /my/\*/\*, /my/\* and /my\* or /my/te\*/event will no longer work. One deviation from this limitation is in the usage of variable depth subscriptions (as documented below).
555
556Specific listener:
557```javascript
558my_client_instance.on('/e2e_test1/testsubscribe/data/delete_me', //the path you are listening on
559 {event_type:'remove', // either set, remove or all - defaults to all
560 count:0},// how many times you want your handler to handle for before it is removed - default is 0 (infinity)
561 function(//your listener event handler
562 message, //the actual object data being set or removed
563 meta){ //the meta data - path, modified,created _id etc.
564
565
566 },
567 function(e){
568 //passes in an error if you were unable to register your listener
569 });
570//this is now promise based as of v11.5.0
571const handle = await my_client_instance.on('/e2e_test1/testsubscribe/data/delete_me', //the path you are listening on
572 {event_type:'remove', // either set, remove or all - defaults to all
573 count:0},// how many times you want your handler to handle for before it is removed - default is 0 (infinity)
574 function(//your listener event handler
575 message, //the actual object data being set or removed
576 meta){ //the meta data - path, modified,created _id etc.
577 //event happened
578 });
579await my_client_instance.off(handle); //unsubscribe
580```
581
582Catch all listener:
583```javascript
584my_client_instance.onAll(function(//your listener event handler
585 message, //the actual object data being set or removed
586 meta){
587 //the meta data - path, modified,created _id, also tells you what type of operation happened - ie. GET, SET etc.
588 },
589 function(e){
590 //passes in an error if you were unable to register your listener
591 });
592//this is now promise based as of v11.5.0
593const handle = await my_client_instance.onAll(function(//your listener event handler
594 message, //the actual object data being set or removed
595 meta){ //the meta data - path, modified,created _id, also tells you what type of operation happened - ie. GET, SET etc.
596});
597```
598
599Once listener:
600```javascript
601const handle = await my_client_instance.once('/e2e_test1/testsubscribe/data/delete_me', //the path you are listening on
602 { event_type:'*' }, // either set, remove or all - defaults to all
603 function(//your listener event handler
604 message, //the actual object data being set or removed
605 meta){ //the meta data - path, modified,created _id etc.
606 //event happened
607 });
608await my_client_instance.off(handle); //unsubscribe, these will auto-expire after they have received a single message
609```
610
611EVENT DATA
612----------------------------
613
614*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*
615
616```javascript
617//get the data back as part of the subscription callback
618listenerclient.on('/e2e_test1/testsubscribe/data/values_on_callback_test/*',
619 {"event_type": "set",
620 "initialCallback":true //set to true, causes data to be passed back
621 }, function (message) {
622
623 expect(message.updated).to.be(true);
624 callback();
625
626 }, function(e, reference, response){
627 if (e) return callback(e);
628 try{
629
630 //the response is your data, ordered by modified - will always be in an array even if only one or none is found
631
632 expect(response.length).to.be(2);
633 expect(response[0].test).to.be('data');
634 expect(response[1].test).to.be('data1');
635
636 listenerclient.set('/e2e_test1/testsubscribe/data/values_on_callback_test/1', {"test":"data", "updated":true}, function(e){
637 if (e) return callback(e);
638 });
639
640 }catch(e){
641 return callback(e);
642 }
643 });
644
645```
646
647```javascript
648//get the data emitted back immediately
649
650listenerclient.on('/e2e_test1/testsubscribe/data/values_emitted_test/*',
651 {"event_type": "set",
652 "initialEmit":true //set to true causes emit to happen on successful subscription
653 }, function (message, meta) {
654 //this emit handler runs immediately
655 caughtEmitted++;
656
657 if (caughtEmitted == 2){
658 expect(message.test).to.be("data1");
659 callback();
660 }
661
662
663 }, function(e){
664 if (e) return callback(e);
665 });
666
667```
668
669UNSUBSCRIBING FROM EVENTS
670----------------------------
671
672//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:
673
674```javascript
675
676var currentListenerId;
677var onRan = false;
678var pathOnRan = false;
679
680listenerclient.on('/e2e_test1/testsubscribe/data/on_off_test', {event_type: 'set', count: 0}, function (message) {
681
682 if (pathOnRan) return callback(new Error('subscription was not removed by path'));
683 else pathOnRan = true;
684
685 //NB - unsubscribing by path
686 listenerclient.offPath('/e2e_test1/testsubscribe/data/on_off_test', function (e) {
687
688 if (e)
689 return callback(new Error(e));
690
691 listenerclient.on('/e2e_test1/testsubscribe/data/on_off_test', {event_type: 'set', count: 0},
692 function (message) {
693 if (onRan) return callback(new Error('subscription was not removed'));
694 else {
695 onRan = true;
696 //NB - unsubscribing by listener handle
697 listenerclient.off(currentListenerId, function (e) {
698 if (e)
699 return callback(new Error(e));
700
701 publisherclient.set('/e2e_test1/testsubscribe/data/on_off_test', {"test":"data"}, function (e, setresult) {
702 if (e) return callback(new Error(e));
703 setTimeout(callback, 2000);
704 });
705 });
706 }
707 },
708 function (e, listenerId) {
709
710 //NB - listener id is passed in on the .on callback
711
712 if (e) return callback(new Error(e));
713
714 currentListenerId = listenerId;
715
716 publisherclient.set('/e2e_test1/testsubscribe/data/on_off_test', {"test":"data"}, function (e, setresult) {
717 if (e) return callback(new Error(e));
718 });
719 });
720 });
721
722}, function (e, listenerId) {
723 if (e) return callback(new Error(e));
724
725 currentListenerId = listenerId;
726
727 publisherclient.set('/e2e_test1/testsubscribe/data/on_off_test', {"test":"data"}, function (e) {
728 if (e) return callback(new Error(e));
729 });
730
731});
732
733```
734
735TARGETING EVENTS
736----------------
737
738*sets and removes can be targeted for a specific client session, if you have access to a client session id, or need to do a return-ticket post, you can add the session id's you want your event data to go to to the targetClients option*
739
740```
741var mySessionId = my_client_instance.sesson.id;
742
743//only publish to myself:
744
745other_client_instance.on('for/my/eyes/only', function(data){
746//should NOT receive this
747});
748
749my_client_instance.on('for/my/eyes/only', function(data){
750//should receive this
751});
752
753my_client_instance.set('for/my/eyes/only', {property1:'property1'}, {targetClients:[mySessionId]}, function(e, result){
754 ...
755});
756
757```
758
759EVENTS WITH CUSTOM META
760-----------------------
761
762*sets and removes can declare custom metadata that will be sent to subscribers*
763
764```javascript
765client.set('/some/topic', {DATA: 1}, {meta: {custom: 1}}, function(e) {})
766
767// elsewhere
768client.on('/some/topic', function(data, meta) {
769 meta.custom == 1;
770});
771```
772
773Reserved meta key names will have no effect. ('created','modified','path','action','type','published','status','eventId','sessionId')
774
775MERGE SUBSCRIPTIONS
776-----------------------
777
778*you can subscribe to data changes on set/remove and specify only to recieve the merge data as posted in the set operation with the merge:true option, NB: this is not a true delta, as you may receive some duplicate input fields*
779
780```javascript
781
782 my_client_instance.on('/merge/only/path', {
783 event_type: 'set',
784 merge: true
785 }, function (message) {
786 console.log('emit happened - message is {some:"data"}');
787 }, function (e) {
788 console.log('subscription happened');
789 });
790
791my_client_instance.set('/merge/only/path',
792 {some:"data"},
793 {merge:true},
794 function (e) {
795 console.log('set happened');
796 });
797
798```
799
800VARIABLE DEPTH SUBSCRIPTIONS
801-----------------------
802*A special subscription, with a trailing /\*\* on the path, allows for subscriptions to multiple wildcard paths, up to a specific depth*
803
804```javascript
805
806var happn = require('../../../lib/index');
807var happn_client = happn.client;
808
809//NB the default variable depth is 5, you can set it when initialising the client like so:
810myclient = await happn_client.create({config:{username:'_ADMIN', password:'happn', defaultVariableDepth:10}});
811
812var handler = function(data){
813
814};
815
816myclient.on('/test/path/**', { depth:4 }, handler, function(e, variableDepthHandle){
817
818 //you can unsubscribe as per normal
819 // ie: myclient.off(variableDepthHandle)
820});
821
822//is the same as
823myclient.on('/test/path/*', handler, function(e){
824
825});
826myclient.on('/test/path/*/*', handler, function(e){
827
828});
829myclient.on('/test/path/*/*/*', handler, function(e){
830
831});
832myclient.on('/test/path/*/*/*/*', handler, function(e){
833
834});
835
836//NB: up to a depth of 4, so the event will not fire for a larger depth, ie: /test/path/1/2/3/4/5
837//NB: this functionality also works with initialCallback and initialEmit
838
839myclient.on('/test/path/**', {
840 "event_type": "set",
841 "initialEmit": true
842}, function (message, meta) {
843 //items will be immediately emitted up to the depth specified
844});
845
846myclient.on('/test/path/**', {
847 "event_type": "set",
848 "initialCallback": true
849}, function (message) {
850
851 expect(message.updated).to.be(true);
852 callback();
853
854}, function (e, reference, response) {
855 //response will be an array of items that exist to the specified depth
856});
857
858```
859
860TAGGING
861----------------------------
862
863*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*
864
865```javascript
866
867var randomTag = require('shortid').generate();
868
869my_client_instance.set('e2e_test1/test/tag', {property1:'property1',property2:'property2',property3:'property3'}, {tag:randomTag}, function(e, result){
870
871```
872
873MERGING
874----------------------------
875
876*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*
877
878```javascript
879
880my_client_instance.set('e2e_test1/testsubscribe/data/', {property1:'property1',property2:'property2',property3:'property3'}, {merge:true}, function(e, result){
881
882});
883
884```
885
886SESSION AND CONNECTION EVENTS:
887------------------------------
888
889*these events are emitted if the client connection state or session state changes*
890
891```javascript
892
893// session-ended event is emitted if the session is disconnected from the server side
894my_client_instance.onEvent('session-ended', (evt) => {
895 //evt.reason could be:
896 //inactivity-threshold - the client has been inactive for a period exceeding what the session is profiled for (see profiles)
897 //session-revoked - the client session has been revoked on the server side
898 //security directory update: user deleted - the user that the session is associated with has been deleted on the server
899});
900
901// reconnect-scheduled event is emitted if the connection with the server has been interrupted
902my_client_instance.onEvent('reconnect-scheduled', (evt) => {
903
904});
905
906// reconnect-successful event is emitted if the connection with the server has been restored
907my_client_instance.onEvent('reconnect-successful', (evt) => {
908
909});
910
911```
912
913After version 11.6.0, by default some basic session info is logged whenever a client attached or detaches in the format, as stringified JSON:
914
915```json
916{
917 event, //session attached / session detatched
918 username, //user name or 'anonymous (unsecure connection)',
919 sourceAddress, //session source address,
920 sourcePort, //client side port
921 upgradeUrl,// primus upgrade url for establishing the connection socket
922 happnVersion, // for new clients, package version
923 happnProtocolVersion // happn_4, happn (old connections)
924 }
925```
926
927This can be switched off by updating the session service config:
928```javascript
929const Happn = require('happn-3');
930let myService = await Happn.service.create({
931 name: 'TEST-NAME',
932 secure: true,
933 services: {
934 session: {
935 config: {
936 disableSessionEventLogging:true
937 }
938 }
939 }
940});
941```
942
943SECURITY SERVER
944---------------
945
946*happn server instances can be secured with user and group authentication and authorisation, 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)*
947
948```javascript
949
950var happn = require('happn-3');
951var happnInstance; //this will be your server instance
952
953happn.service.create({secure:true, adminUser:{password:'testPWD'}},
954function (e, instance) {
955
956 if (e)
957 return callback(e);
958
959 happnInstance = instance; //here it is, your server instance
960
961});
962
963
964```
965
966*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](https://github.com/happner/happn-3/blob/master/test/integration/security/access_sanity.js)*
967
968SECURITY CLIENT
969----------------
970
971*the client needs to be instantiated with user credentials and with the secure option set to true to connect to a secure server*
972
973```javascript
974
975//logging in with the _ADMIN user
976
977var happn = require('happn-3');
978happn.client.create({username:'_ADMIN', password:'testPWD', secure:true},function(e, instance) {
979
980
981```
982
983SECURITY USERS AND GROUPS
984-------------------------
985
986*to modify users and groups, a direct code based connection to the happn-3 security service is required, thus users and groups should not be modified in any way over the wire*
987
988### add a group
989
990```javascript
991
992var happn = require('happn-3')
993var happnInstance; //this will be your server instance
994
995happn.service.create({secure:true, adminUser:{password:'testPWD'}},
996function (e, myHappn3Instance) {
997
998 var myGroup = {
999 name:'TEST',
1000 permissions:{
1001 '/test/path/*':{actions:['get', 'set']}, //allow only gets and sets to this path
1002 '/test/allow/all':{actions:['*']} //allow all actions to this path
1003 },
1004 custom_data:{//any custom data you want
1005 test:'data'
1006 }
1007 };
1008
1009 //NB! permissions are stored separately to the group, so when upserting the group and it allready exists
1010 //with other permissions the current upserts permissions are merged with the existing ones, down to action level
1011
1012 myHappn3Instance.services.security.groups.upsertGroup(myGroup)
1013 .then(function(upserted){
1014 //group added
1015 })
1016
1017});
1018```
1019
1020### list groups
1021
1022```javascript
1023
1024var happn = require('happn-3')
1025var happnInstance; //this will be your server instance
1026
1027happn.service.create({secure:true, adminUser:{password:'testPWD'}},
1028function (e, myHappn3Instance) {
1029
1030 var myGroup = {
1031 name:'TEST',
1032 permissions:{
1033 '/test/path/*':{actions:['get', 'set']}, //allow only gets and sets to this path
1034 '/test/allow/all':{actions:['*']} //allow all actions to this path
1035 },
1036 custom_data:{//any custom data you want
1037 test:'data'
1038 }
1039 };
1040
1041 //NB! permissions are stored separately to the group, so when upserting the group and it allready exists
1042 //with other permissions the current upserts permissions are merged with the existing ones, down to action level
1043
1044 myHappn3Instance.services.security.groups.upsertGroup(myGroup)
1045 .then(function(upserted){
1046 //group added
1047 return myHappn3Instance.services.security.groups.listGroups('TES*', {
1048 criteria:{
1049 name:{$eq:'TEST'}
1050 },
1051 /* optional
1052 skip:2,
1053 limit:5,
1054 count:true //will only return the groups count
1055 */
1056 });
1057 })
1058 .then(function(group){
1059 //group just added would be returned
1060 });
1061});
1062```
1063
1064#### if we are using mongodb, we are able to specify collation for listing users see the [mongo tests](https://github.com/happner/happn-3/blob/master/test/integration/security/groups_users_permissions_sanity-mongo.js).
1065
1066#### NB! permissions are separate to the group, so when upserting the group and it already exists with other permissions the current upserts permissions are merged with the existing ones, down to action level
1067
1068### add a user
1069
1070```javascript
1071
1072var happn = require('happn-3')
1073var happnInstance; //this will be your server instance
1074
1075happn.service.create({secure:true, adminUser:{password:'testPWD'}},
1076function (e, myHappn3Instance) {
1077
1078 var myUser = {
1079 username: 'TEST',
1080 password: 'TEST PWD',
1081 custom_data: {
1082 something: 'usefull'
1083 }
1084 };
1085
1086 myHappn3Instance.services.security.users.upsertUser(myUser)
1087 .then(function(upserted){
1088 //user added, with no permissions yet - permissions must be assigned to the user by linking the user to a group
1089 })
1090
1091});
1092```
1093
1094### link a fetched user to a fetched group
1095*demonstrates getUser, getGroup and linkGroup*
1096```javascript
1097
1098var happn = require('happn-3')
1099var happnInstance; //this will be your server instance
1100
1101happn.service.create({secure:true, adminUser:{password:'testPWD'}},
1102function (e, myHappn3Instance) {
1103
1104 var myUser, myGroup;
1105
1106 myHappn3Instance.services.security.users.getUser('TEST')
1107 .then(function(user){
1108 myUser = user;
1109 return myHappn3Instance.services.security.groups.getGroup('TEST');
1110 })
1111 .then(function(group){
1112 myGroup = group;
1113 return myHappn3Instance.services.security.groups.linkGroup(myGroup, myUser);
1114 })
1115 .then(function(){
1116 //your TEST user now has the permissions assigned by your TEST group
1117 });
1118
1119});
1120
1121
1122```
1123
1124### list users
1125*user can be listed by group name (exact match) or by a username (partial match with wildcard - with optional additional criteria)*
1126```javascript
1127
1128var happn = require('happn-3')
1129var happnInstance; //this will be your server instance
1130
1131happn.service.create({secure:true, adminUser:{password:'testPWD'}},
1132function (e, myHappn3Instance) {
1133
1134 //by username, with more specific criteria (mongo style)
1135 myHappn3Instance.services.security.users.listUsers('TEST*', {
1136 criteria:{username:{$eq:'TESTUSER1'}},
1137 /* optional
1138 skip:2,
1139 limit:5,
1140 count:true //will only return the users count
1141 */
1142 })
1143 .then(function(users){
1144
1145 //returns:
1146 // [
1147 // {username:'test1', custom_data:{test:1}},
1148 // {username:'test2', custom_data:{test:1}},
1149 // {username:'test3', custom_data:{test:1}}
1150 // ]
1151
1152 //by group name, note optional criteria
1153 return myHappn3Instance.services.security.users.listUsersByGroup('TEST', {criteria:{custom_data:1}})
1154 })
1155 .then(function(users){
1156
1157 //returns:
1158 // [
1159 // {username:'test1', custom_data:{test:1}}
1160 // ]
1161
1162 //much faster - just get the usernames by the group name
1163 return myHappn3Instance.services.security.users.listUserNamesByGroup('TEST');
1164 })
1165 .then(function(usernames){
1166
1167 //returns:
1168 // [
1169 // 'test1'
1170 // ]
1171
1172 });
1173});
1174```
1175
1176### upsert a permission
1177
1178*permissions can be merged by saving a group, or permissions can be added to a group piecemeal in the following way:*
1179
1180```javascript
1181
1182var happn = require('happn-3')
1183var happnInstance; //this will be your server instance
1184
1185happn.service.create({secure:true, adminUser:{password:'testPWD'}},
1186 function (e, myHappn3Instance) {
1187
1188 myHappn3Instance.services.security.groups.upsertPermission('TEST'/* group name*/, '/test/path/*' /* permission path */, 'on' /* action */, true /* allow (default) - if false the permission is removed */)
1189 .then(function () {
1190
1191 //users belonging to the TEST group can now do "set", "get" AND "on" operations as opposed to only set and get (check above addGroup example to double check the pre-existing permissions)
1192
1193 });
1194 });
1195
1196```
1197
1198
1199### remove a permission
1200
1201*permissions can be removed piecemeal as follows:*
1202
1203```javascript
1204
1205var happn = require('happn-3')
1206var happnInstance; //this will be your server instance
1207
1208happn.service.create({secure:true, adminUser:{password:'testPWD'}},
1209function (e, myHappn3Instance) {
1210
1211 myHappn3Instance.services.security.groups.removePermission('TEST'/* group name*/, '/test/path/*' /* permission path */, 'on' /* action */)
1212 .then(function () {
1213
1214 //users belonging to the TEST group can now only do "set" and "get" operations, the right to do "on" has been revoked
1215
1216 })
1217 .catch(done);
1218});
1219
1220```
1221
1222### remove a permission by upserting a group
1223
1224* a group can be upserted with a set of permissions which are merged into the permissions tree, permissions that have "prohibit" actions are removed *
1225
1226```javascript
1227
1228var happn = require('happn-3')
1229var happnInstance; //this will be your server instance
1230
1231happn.service.create({secure:true, adminUser:{password:'testPWD'}},
1232function (e, myHappn3Instance) {
1233
1234 var myGroup = {
1235 name:'TEST',
1236 permissions:{
1237 '/test/path/*':{actions:['get', 'set']}, //allow only gets and sets to this path
1238 '/test/allow/all':{actions:['*']} //allow all actions to this path
1239 '/test/do/not/subscribe':{prohibit:['on']} //prohibit "on" requests to this path
1240 },
1241 custom_data:{//any custom data you want
1242 test:'data'
1243 }
1244 };
1245
1246 myHappn3Instance.services.security.groups.upsertGroup(myGroup)
1247 .then(function(upserted){
1248 //group updated, "on" permissions to "/test/do/not/subscribe" have been deleted if they existed previously
1249 });
1250 });
1251
1252```
1253
1254USER PERMISSIONS
1255----------------
1256*It is not necessary to use groups as the containers for permissions - permissions can also be added directly to users:*
1257
1258```javascript
1259const myUser = {
1260 username: 'test_username',
1261 password: 'test_pwd',
1262 permissions: {}
1263};
1264myUser.permissions['/test/path/*'] = {
1265 actions: ['on', 'set']
1266};
1267
1268await myHappn3Instance.services.security.users.upsertUser(testUser2, {});
1269
1270//this user specifically can set and listen on /test/path/*
1271
1272//you can now remove the 'on' permission:
1273await myHappn3Instance.services.security.users.removePermission(
1274 addedTestuser2.username,
1275 '/test/path/*',
1276 'on'
1277)
1278
1279//you can now remove the 'set' permission:
1280await myHappn3Instance.services.security.users.removePermission(
1281 addedTestuser2.username,
1282 '/test/path/*',
1283 'set'
1284)
1285
1286//you can now remove the 'all' permissions:
1287await myHappn3Instance.services.security.users.removePermission(
1288 addedTestuser2.username,
1289 '/test/path/*'
1290)
1291
1292//you can re-add a permission
1293await myHappn3Instance.services.security.users.upsertPermission(
1294 addedTestuser2.username,
1295 '/test/path/*',
1296 'set'
1297)
1298
1299//you can list permissions for a user
1300const permissions = await myHappn3Instance.services.security.users.listPermissions(
1301 addedTestuser2.username
1302)
1303
1304//list looks like this:
1305[
1306 { action: 'set', authorized: true, path: '/test/path/*' }
1307]
1308```
1309
1310
1311VOLATILE PERMISSIONS
1312--------------------
1313
1314*by default, permissions are persisted via the default data provider, this is typically to a file (when using nedb) - or to a mongo database. In situations where you do not want to persist permissions between restarts, you can by adjusting the persistPermissions security config setting to false:*
1315
1316```javascript
1317const Happn = require('happn-3');
1318let myService = await Happn.service.create({
1319 name: 'TEST-NAME',
1320 secure: true,
1321 services: {
1322 security: {
1323 config: {
1324 persistPermissions:false
1325 }
1326 },
1327 data: {
1328 config: {
1329 filename: filename
1330 }
1331 }
1332 }
1333});
1334```
1335
1336*NB: *
1337
1338SECURITY PROFILES
1339-----------------
1340
1341*profiles can be configured to fit different session types, profiles are ordered sets of rules that match incoming sessions with specific policies, the first matching rule in the set is selected when a session is profiled, so the order they are configured in the array is important*
1342
1343```javascript
1344
1345//there are 2 default profiles that exist in secure systems - here is an example configuration
1346//showing how profiles can be configured for a service:
1347
1348 var serviceConfig = {
1349 services:{
1350 security: {
1351 config: {
1352 sessionTokenSecret:"TESTTOKENSECRET",
1353 keyPair: {
1354 privateKey: 'Kd9FQzddR7G6S9nJ/BK8vLF83AzOphW2lqDOQ/LjU4M=',
1355 publicKey: 'AlHCtJlFthb359xOxR5kiBLJpfoC2ZLPLWYHN3+hdzf2'
1356 },
1357 profiles:[ //profiles are in an array, in descending order of priority, so if you fit more than one profile, the top profile is chosen
1358 {
1359 name:"web-session",
1360 session:{
1361 $and:[{
1362 user:{username:{$eq:'WEB_SESSION'}},
1363 type:{$eq:0}
1364 }]
1365 },
1366 policy:{
1367 ttl: "4 seconds",//4 seconds = 4000ms, 4 days = 1000 * 60 * 60 * 24 * 4, allow for hours/minutes
1368 inactivity_threshold:2000//this is costly, as we need to store state on the server side
1369 }
1370 }, {
1371 name:"rest-device",
1372 session:{
1373 $and:[{ //filter by the security properties of the session - check if this session user belongs to a specific group
1374 user:{groups:{
1375 "REST_DEVICES" : { $exists: true }
1376 }},
1377 type:{$eq:0} //token stateless
1378 }]},
1379 policy: {
1380 ttl: 2000//stale after 2 seconds
1381 }
1382 },{
1383 name:"trusted-device",
1384 session:{
1385 $and:[{ //filter by the security properties of the session, so user, groups and permissions
1386 user:{groups:{
1387 "TRUSTED_DEVICES" : { $exists: true }
1388 }},
1389 type:{$eq:1} //stateful connected device
1390 }]},
1391 policy: {
1392 ttl: 2000,//stale after 2 seconds
1393 permissions:{//permissions that the holder of this token is limited, regardless of the underlying user
1394 '/TRUSTED_DEVICES/*':{actions: ['*']}
1395 }
1396 }
1397 },{
1398 name:"specific-device",
1399 session:{$and:[{ //instance based mapping, so what kind of session is this?
1400 type:{$in:[0,1]}, //any type of session
1401 ip_address:{$eq:'127.0.0.1'}
1402 }]},
1403 policy: {
1404 ttl: Infinity,//this device has this access no matter what
1405 inactivity_threshold:Infinity,
1406 permissions:{//this device has read-only access to a specific item
1407 '/SPECIFIC_DEVICE/*':{actions: ['get','on']}
1408 }
1409 }
1410 },
1411 {
1412 name:"non-reusable",
1413 session:{$and:[{ //instance based mapping, so what kind of session is this?
1414 user:{groups:{
1415 "LIMITED_REUSE" : { $exists: true }
1416 }},
1417 type:{$in:[0,1]} //stateless or stateful
1418 }]},
1419 policy: {
1420 usage_limit:2//you can only use this session call twice
1421 }
1422 }, {
1423 name:"default-stateful",// this is the default underlying profile for stateful sessions
1424 session:{
1425 $and:[{type:{$eq:1}}]
1426 },
1427 policy: {
1428 ttl: Infinity,
1429 inactivity_threshold:Infinity
1430 }
1431 }, {
1432 name:"default-stateless",// this is the default underlying profile for ws sessions
1433 session:{
1434 $and:[{type:{$eq:0}}]
1435 },
1436 policy: {
1437 ttl: 60000 * 10,//session goes stale after 10 minutes
1438 inactivity_threshold:Infinity
1439 }
1440 }, {
1441 name:"ip address whitelist",// this ensures the _ADMIN user can only login from a whitelisted set of IP addresses (in this case locally)
1442 session:{
1443 $and:[{
1444 user:{username:{$eq:'WEB_SESSION'}}
1445 }]
1446 },
1447 policy: {
1448 sourceIPWhitelist: [
1449 '127.0.0.1',
1450 '::ffff:127.0.0.1' //NOTE: if proxied be sure to also allow for IPV6 prefixed addresses
1451 ]
1452 }
1453 }
1454 ]
1455 }
1456 }
1457 }
1458 };
1459
1460```
1461
1462*the test that clearly demonstrates profiles can be found [here](https://github.com/happner/happn-3/blob/master/test/integration/security/default_profiles.js)*
1463
1464*the default policies look like this:*
1465
1466```javascript
1467//stateful - so ws sessions:
1468{
1469 name:"default-stateful",// this is the default underlying profile for stateful sessions
1470 session:{
1471 $and:[{type:{$eq:1}}]
1472 },
1473 policy: {
1474 ttl: 0, //never goes stale
1475 inactivity_threshold:Infinity
1476 }
1477 }
1478
1479
1480//stateless - so token based http requests (REST)
1481{
1482 name:"default-stateless",// this is the default underlying profile for stateless sessions (REST)
1483 session:{
1484 $and:[{type:{$eq:0}}]
1485 },
1486 policy: {
1487 ttl: 0, //never goes stale
1488 inactivity_threshold:Infinity
1489 }
1490 }
1491
1492```
1493
1494*NB NB - if no matching profile is found for an incoming session, one of the above is selected based on whether the session is stateful or stateless, there is no ttl or inactivity timeout on both policies - this means that tokens can be reused forever (unless the user in the token is deleted) rather push to default policies to your policy list which would sit above these less secure ones, with a ttl and possibly inactivity timeout*
1495
1496TEMPLATED PERMISSIONS
1497---------------------
1498
1499*permissions can be templated to the current session using {{handlebars}} syntax, the template context is the current session and its sub-objects*
1500
1501code snippets have been taken from the [this test](https://github.com/happner/happn-3/blob/master/test/integration/security/templated_permissions.js)
1502
1503average session object looks like this:
1504
1505```javascript
1506var sessionObj = {
1507 "id": "ec5d673b-cf28-4a0a-8a12-396f20aaaf57",//session unique id, transient
1508 "username": "TEST USER@blah.com1528278832638_V1DWIA5xxB",//session username
1509 "isToken": false,//whether the session was accessed via a token or not
1510 "permissionSetKey": "uJgq//rc4saoc1MSjqyB3KvJEUs=",//hash of user permissions for quick access lookup
1511 "user": {
1512 "username": "TEST USER@blah.com1528278832638_V1DWIA5xxB",
1513 "custom_data": {//any custom user info
1514 "custom_field2": "custom_field2_changed",
1515 "custom_field3": "custom_field3_changed",
1516 "custom_field4": "custom_field4_value",
1517 "custom_field5": "custom_field5_value",
1518 "custom_field_forbidden": "*"
1519 },
1520 "groups": {//groups the user belongs to
1521 "TEST GROUP1528278832638_V1DWIA5xxB": {
1522 "data": {}//any data that may have been added to the group
1523 }
1524 }
1525 }
1526}
1527```
1528
1529we can save a group with templated permissions that give the user access to paths containing its username like so:
1530
1531```javascript
1532
1533var testGroup = {
1534 name: 'TEST GROUP' + test_id,
1535 custom_data: {
1536 customString: 'custom1',
1537 customNumber: 0
1538 }
1539};
1540
1541testGroup.permissions = {
1542 '/gauges/{{user.username}}': {
1543 actions: ['*']
1544 },
1545 '/gauges/{{user.username}}/*': {//NB: note here how the path relates to the above session object's user.username
1546 actions: ['*']
1547 },
1548 '/custom/{{user.custom_data.custom_field1}}': {//NB: note here how the path relates to the above session object
1549 actions: ['get', 'set']
1550 }
1551};
1552
1553var testUser = {
1554 username: 'TEST USER@blah.com' + test_id,
1555 password: 'TEST PWD',
1556 custom_data: {
1557 custom_field1: 'custom_field1_value'//NB: note here how the value will match the below requests
1558 }
1559};
1560
1561var addedTestGroup, addedTestUser, testClient;
1562
1563//assuming we have created a happn instance and assigned it to myHappnService
1564myHappnService.services.security.groups.upsertGroup(testGroup)
1565 .then(function(result){
1566
1567 addedTestGroup = result;
1568 return myHappnService.services.security.users.upsertUser(testUser);
1569 })
1570 .then(function(result){
1571
1572 addedTestUser = result;
1573 return myHappnService.services.security.users.linkGroup(addedTestGroup, addedTestuser);
1574 })
1575 .then(function(result){
1576
1577 return myHappnService.services.session.localClient({
1578 username: testUser.username,
1579 password: 'TEST PWD'
1580 });
1581 })
1582 .then(function(clientInstance){
1583 testClient = clientInstance;
1584 done();
1585 })
1586 .catch(done);
1587
1588```
1589
1590then from the testClient access the path the template resolves to like so:
1591
1592```javascript
1593
1594var username = 'TEST USER@blah.com' + test_id;
1595
1596testClient.on('/gauges/' + username, function(data) {
1597 expect(data.value).to.be(1);
1598 expect(data.gauge).to.be('logins');
1599 done();
1600}, function(e) {
1601 if (e) return done(e);
1602 testClient.increment('/gauges/' + username, 'logins', 1, function(e) {
1603 if (e) return done(e);
1604 });
1605});
1606
1607```
1608
1609NB: permissions that resolve to context properties with * in them are ignored lest there be the chance for unauthorized promotion of the users privileges, ie:
1610
1611```javascript
1612//for the given user object with a custom_data property with a * fields
1613var exampleUser = {
1614 username:'test',
1615 password:'blah',
1616 custom_data:{
1617 test:'*'
1618 }
1619}
1620
1621//this permission will not work:
1622var exampleGroup = {
1623 name:'test',
1624 permissions:{
1625 'users/{{user.custom_data.test}}/gauges/*':{actions:['on', 'set']}
1626 }
1627}
1628
1629//because the template would resolve to users/*/gauges/*, which may leak other users data
1630
1631```
1632
1633WEB PATH LEVEL SECURITY
1634-----------------------
1635
1636*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:*
1637
1638
1639```javascript
1640
1641var happn = require('happn-3')
1642var happnInstance; //this will be your server instance
1643
1644happn.service.create({secure:true, adminUser:{password:'testPWD'}},
1645function (e, instance) {
1646
1647 if (e)
1648 return callback(e);
1649
1650 happnInstance = instance; //here it is, your server instance
1651
1652 var testGroup = {
1653 name:'TEST GROUP',
1654 custom_data:{
1655 customString:'custom1',
1656 customNumber:0
1657 }
1658 }
1659
1660 testGroup.permissions = {
1661 '/@HTTP/secure/route/*':{actions:['get']},//NB - we can wildcard the path
1662 '/@HTTP/secure/another/route/test':{actions:['put','post']}//NB - actions confirm to http verbs
1663 };
1664
1665 happnInstance.services.security.upsertGroup(testGroup, {}, function(e, group){
1666
1667 //our group has been upserted with the right permissions
1668
1669 //this is how we add custom routes to the service, these routes are both available to users who belong to the 'TEST GROUP' group or the _ADMIN user (who has permissions to all routes)
1670
1671 happnInstance.connect.use('/secure/route/test', function(req, res, next){
1672
1673 res.setHeader('Content-Type', 'application/json');
1674 res.end(JSON.stringify({"secure":"value"}));
1675
1676 });
1677
1678 happnInstance.connect.use('/secure/another/route/test', function(req, res, next){
1679
1680 res.setHeader('Content-Type', 'application/json');
1681 res.end(JSON.stringify({"secure":"value"}));
1682
1683 });
1684 });
1685});
1686
1687```
1688
1689*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 automatically set by default*
1690
1691
1692```javascript
1693
1694//logging in with the _ADMIN user, who has permission to all web routes
1695
1696var happn = require('happn-3');
1697happn.client.create({username:'_ADMIN', password:'testPWD'},function(e, instance) {
1698
1699 //the token can be derived from instance.session.token now
1700
1701 //here is an example of an http request using the token:
1702
1703 var http = require('http');
1704
1705 var options = {
1706 host: '127.0.0.1',
1707 port:55000,
1708 path:'/secure/route/test'
1709 }
1710
1711 if (use_query_string) options.path += '?happn_token=' + instance.session.token;
1712 else options.headers = {'Cookie': ['happn_token=' + instance.session.token]}
1713
1714 http.request(options, function(response){
1715 //response.statusCode should be 200;
1716 }).end();
1717});
1718
1719
1720```
1721
1722USING A BEARER TOKEN AUTHORIZATION HEADER
1723-----------------------------------------
1724
1725*A Bearer authorization token can also be used to do http requests with, as follows: *
1726
1727```javascript
1728
1729var happn = require('happn-3');
1730happn.client.create({username:'_ADMIN', password:'testPWD'},function(e, instance) {
1731
1732 var request = require('request');
1733
1734 var options = {
1735 url: 'http://127.0.0.1:55000/my/special/middleware',
1736 };
1737
1738 options.headers = {'Authorization': ['Bearer ' + instance.session.token]};
1739
1740 request(options, function (error, response, body) {
1741
1742 //response happens all should be ok if the token is correct and the account is able to access the middleware resource
1743 });
1744});
1745
1746```
1747
1748SECURITY OPTIONS
1749----------------
1750
1751__disableDefaultAdminNetworkConnections__ - this config setting prevents any logins of the default _ADMIN user via the network, thus only local (intra process) _ADMIN connections are allowed:
1752
1753```javascript
1754
1755var happn = require('happn-3');
1756
1757happn.service.create({
1758 secure: true,
1759 port:55002,
1760 disableDefaultAdminNetworkConnections:true //here is our switch
1761}, function(e, service){
1762
1763 happn_client.create({
1764 config: {
1765 username: '_ADMIN',
1766 password: 'happn',
1767 port:55002
1768 }
1769 }, function (e, instance) {
1770 //we will have an error here
1771 expect(e.toString()).to.be('AccessDenied: use of _ADMIN credentials over the network is disabled');
1772 });
1773});
1774
1775//only clients peeled off the local process will work:
1776
1777serviceInstanceLocked.services.session.localAdminClient(function(e, adminClient){
1778 if (e) return done(e);
1779 adminClient.get('/_SYSTEM/*', function(e, items){
1780
1781 expect(items.length > 0).to.be(true);//fetched system level data
1782 });
1783});
1784
1785//or
1786
1787serviceInstanceLocked.services.session.localClient({
1788 username:'_ADMIN',
1789 password:'happn'
1790}, function(e, adminClient){
1791
1792 adminClient.get('/_SYSTEM/*', function(e, items){
1793
1794 expect(items.length > 0).to.be(true);//fetched system level data
1795 });
1796});
1797
1798```
1799
1800UNCONFIGURED SESSION CLEANUP
1801-----------------------------------------
1802
1803*because we do not use client certificates to manage connections to a happn instance as part of the framework, sockets can be created that are not necessarily logged in, although they would be unable to do anything data-wise, these sockets could clog up allowed upgrade requests in a rate limited setup. The server, if secure, can be setup to disconnect sockets that have not logged in and have no user data attached to them within a specific period*
1804
1805```javascript
1806const serviceConfig = {
1807 secure: true,
1808 services: {
1809 session: {
1810 config: {
1811 unconfiguredSessionCleanup: {
1812 interval: 5e3, //check every N milliseconds for unconfigured sockets, default is every 5 seconds
1813 threshold: 30e3, //sessions are cleaned up if they remain unconfigured for this period since they were created, default is 30 seconds
1814 verbose: false //cleanup activitiies logged, default is false
1815 }
1816 }
1817 }
1818 }
1819};
1820
1821var happn = require('../lib/index')
1822var service = happn.service;
1823service.create(serviceConfig, function(e, happnInst) {
1824 //server created with unconfigured socket cleanup running
1825});
1826```
1827
1828NB: unconfigured socket removal can only be set up for secure servers
1829
1830HTTPS SERVER
1831-----------------------------
1832
1833*happn can also run in https mode, the config has a section called transport*
1834
1835```javascript
1836
1837//cert and key defined in config
1838
1839var config = {
1840 services: {
1841 transport:{
1842 config:{
1843 mode: 'https',
1844 cert: '-----BEGIN CERTIFICATE-----\nMIICpDCCAYwCCQDlN4Axwf2TVzANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwls\nb2NhbGhvc3QwHhcNMTYwMTAzMTE1NTIyWhcNMTcwMTAyMTE1NTIyWjAUMRIwEAYD\nVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDc\nSceGloyefFtWgy8vC7o8w6BTaoXMc2jsvMOxT1pUHPX3jJn2bUOUC8wf3vTM8o4a\n0HY+w7cEZm/BuyTAV0dmS5SU43x9XlCF877jj5L6+ycZDncgyqW3WUWztYyqpQEz\nsMu76XvNEHW+jMMv2EGtze6k1zIcv4FiehVZR9doNOm+SilmmfVpmTmQk+E5z0Bl\n8CSnBECfvtkaYb4YqsV9dZXZcAm5xWdid7BUbqBh5w5XHz9L4aC9WiUEyMMUtwcm\n4lXDnlMkei4ixyz8oGSeOfpAP6Lp4mBjXaMFT6FalwCDAKh9rH2T3Eo9fUm18Dof\nFg4q7KcLPwd6mttP+dqvAgMBAAEwDQYJKoZIhvcNAQELBQADggEBABf8DZ+zv1P7\n8NnDZzuhif+4PveSfAMQrGR+4BS+0eisciif1idyjlxsPF28i82eJOBya4xotRlW\netAqSIjw8Osqxy4boQw3aa+RBtEAZR6Y/h3dvb8eI/jmFqJzmFjdGZzEMO7YlE1B\nxZIbA86dGled9kI1uuxUbWOZkXEdMgoUzM3W4M1ZkWH5WNyzIROvOGSSD73c1fAq\nkeC9MkofvTh3TO5UXFkLCaaPLiETZGI9BpF34Xm3NHS90Y7SUVdiawCVCz9wSuki\nD98bUTZYXu8dZxG6AdgAUEFnMuuwfznpdWQTUpp0k7jbsX/QTbFIjbI9lCZpP9k7\np07A5npzFVo=\n-----END CERTIFICATE-----',
1845 key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA3EnHhpaMnnxbVoMvLwu6PMOgU2qFzHNo7LzDsU9aVBz194yZ\n9m1DlAvMH970zPKOGtB2PsO3BGZvwbskwFdHZkuUlON8fV5QhfO+44+S+vsnGQ53\nIMqlt1lFs7WMqqUBM7DLu+l7zRB1vozDL9hBrc3upNcyHL+BYnoVWUfXaDTpvkop\nZpn1aZk5kJPhOc9AZfAkpwRAn77ZGmG+GKrFfXWV2XAJucVnYnewVG6gYecOVx8/\nS+GgvVolBMjDFLcHJuJVw55TJHouIscs/KBknjn6QD+i6eJgY12jBU+hWpcAgwCo\nfax9k9xKPX1JtfA6HxYOKuynCz8HeprbT/narwIDAQABAoIBADoWFk+t6PxtbCQ+\nyTVNkVkueFsmjotfrz4ldDCP7RCa5lzVLU/mddhW2AdbYg+csc3uRA++ycaWQEfE\nUieJnCEkMtSju5LPSMpZgG8+z5Hwodmgj9cMuG/FUXTWnXXttohryP0Ozv8+pN2O\n/nTiQEdVMuUyfVtJQBO4f2KgZ/No6uuSGhYEGFRTRUgdM1E1f2yTu82HIfETAbnW\nMHpdhQORQKmHr7cE9/sr7E+BhJPSQxGZKmgi+/8tiHXAW5MoZ4K88EO9V/BnVHcL\n/1uVUJOvcyf2mEtsQ22WCeelPChoE8TH1lf0HHadqse5+eu9l3LQWb4Z96fZRK7G\nesk+WAkCgYEA/ZueKDbiyT2i9pS1VDop9BLDaxC3GWwYAEU8At/KzXAfuKdzcduj\nZuMBecS5SgU3wW/1hqBJ2lQF8ifUzQUuyh1tydSnolafurvHDqkWzgbo6EbjjFro\nAyyHHtYRxo/f1TWWs6RpNjJ3hDCc3OpghkwkZkN9v9wd4RMCW2kdA2MCgYEA3l20\nhxpSTJffbKKQTAxURW9v+YUxNQFb74892AMlCnMiCppvS0it8wt0gJwnlNPV92no\nUVLZ+gVXdo8E+kKca4GW/TDgceDPqw2EbkTF1ZCxxy/kwgPWR471ku3Zyg6xel3Z\nMU67EriKz1zJaMjm7JmSjoz3+u8PbLYIf+fpm0UCgYAnkU0GtzGA9lXjpOX5oy2C\ngB7vKGd41u2TtTmctS/eB51bYPzZCcyfs9E6H2BNVS0SyBYFkCKVpsBavK4t4p4f\nOKI1eDFDWcKIDt4KwoTlVhymiNDdyB0kyaC3Rez2DuJ8UGUX2BH2O797513B9etj\naKPRNLx836nlwOKAQpEdQwKBgQCvV7io6CqJVyDI6w9ZyEcTUaI8YbjBkUbLimo7\n0Y79xHfNYKXt+WuhQSEm4PudMcWBCTQ2HFzh+CBVzsUgCjKJ23ASSt5RLfLTcR9C\nTFyr4SMubCe4jYoEd0hSCdg4qolscmB3rxt40agzh3kSdYkSfK7CVYqdhrDlCk19\nfoQI+QKBgQD9PIEvhEnQO0R1k3EzchRo67IkWLyR4uX4hXa7IOXppde9dAwhbZqP\nUkw8tqj7Cg02hfXq+KdRn+xfh6pc708RMqqdqNqSfL2PYedAR65hDKV4rL8PVmL9\n0P4j3FT1nwa/sHW5jLuO5TcevPrlhEQ9xVbKw7I7IJivKMamukskUA==\n-----END RSA PRIVATE KEY-----'
1846 }
1847 }
1848 }
1849}
1850
1851// or cert and key file paths defined in config
1852// IF BOTH OF THESE FILES DONT EXIST, THEY ARE AUTOMATICALLY CREATED AS SELF SIGNED
1853
1854var config = {
1855 services:{
1856 transport:{
1857 config:{
1858 mode: 'https',
1859 certPath: __dirname + path.sep + 'b7_cert.pem',
1860 keyPath: __dirname + path.sep + 'b7_key.rsa'
1861 }
1862 }
1863 }
1864}
1865
1866// or have the system create a cert and key for you, in the home directory of the user that started the happn process - called .happn-https-cert and .happn-https-key
1867
1868var config = {
1869 services:{
1870 transport:{
1871 config:{
1872 mode: 'https'
1873 }
1874 }
1875 }
1876}
1877
1878var happn = require('../lib/index')
1879var service = happn.service;
1880var happnInstance; //this will be your server instance
1881
1882//create the service here - now in https mode - running over the default port 55000
1883
1884service.create(config ...
1885
1886```
1887
1888HTTPS CLIENT
1889------------
1890
1891*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*
1892
1893
1894```javascript
1895
1896var happn = require('happn-3');
1897
1898happn.client.create({protocol:'https', allowSelfSignedCerts:true},function(e, instance) {
1899...
1900
1901```
1902
1903HTTPS COOKIE, AND COOKIE NAME
1904-----------------------------
1905
1906*On the browser, by default the the token established in a successful authentication is put into a cookie, the cookie name is by default happn_token, but is configurable in the security service config. If the httpsCookie option is switched on, the browser will store the cookie name with _https appended to it if it is connecting to the server via https, and the server will know to check incoming https requests for the correct name. This is so that the client can switch between http and https and re-login in http mode without causing a failure when it tries to save over the Secure cookie it created in a previous https session.*
1907
1908```javascript
1909const serviceConfig = {
1910 secure:true,
1911 services: {}
1912};
1913//this is an https server
1914serviceConfig.services.transport = {
1915 config:{
1916 mode: 'https'
1917 }
1918}
1919
1920//we append _https to Secure cookies over https, and also use a custom cookieName myCookie
1921serviceConfig.services.security = {
1922 config:{
1923 cookieName: 'myCookie',//default is happn_token, but now the browser will store the cookie as myCookie_https
1924 //the server will know to look for myCookie_https in incoming requests
1925 httpsCookie: true
1926 }
1927}
1928
1929var happn = require('happn');
1930var service = happn.service;
1931service.create(serviceConfig, function(e, happnInst) {
1932 //server created with httpsCookie switched on
1933});
1934
1935```
1936
1937HTTP/S KEEPALIVES
1938-----------------------------
1939*socket keepalives are set to 2 minutes by default, but this can be configured as follows:*
1940```javascript
1941const serviceConfig = {
1942 services: {
1943 transport: {
1944 config: {
1945 keepAliveTimeout:180000 //3 minutes
1946 }
1947 }
1948 }
1949 };
1950
1951var happn = require('../lib/index')
1952var service = happn.service;
1953service.create(serviceConfig, function(e, happnInst) {
1954 //server created with 3 minute http/s socket keepalive
1955});
1956```
1957
1958WEBSOCKET COMPRESSION
1959---------------------
1960*primusOpts in the configuration can be adjusted to allow for per-message deflate compression for messages larger than 1024 bytes, clients will automatically compress messages when they reconnect*
1961
1962```javascript
1963const serviceConfig = {
1964 services: {
1965 session: {
1966 config: {
1967 primusOpts:{
1968 compression: true
1969 }
1970 }
1971 }
1972 }
1973};
1974
1975var happn = require('../lib/index')
1976var service = happn.service;
1977service.create(serviceConfig, function(e, happnInst) {
1978 //server created with compression switched on
1979});
1980```
1981
1982PAYLOAD ENCRYPTION
1983------------------
1984
1985*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](https://github.com/happner/happn-3/blob/master/test/integration/security/encryptedpayloads_sanity.js)*
1986
1987INBOUND AND OUTBOUND LAYERS (MIDDLEWARE)
1988-----------------------------------------
1989
1990*incoming and outgoing packets delivery can be intercepted on the server side, as demonstrated below:*
1991
1992```javascript
1993
1994
1995var layerLog1 = [];
1996var layerLog2 = [];
1997var layerLog3 = [];
1998var layerLog4 = [];
1999
2000var inboundLayers = [
2001 function(message, cb){
2002 layerLog3.push(message);
2003 return cb(null, message);
2004 },
2005 function(message, cb){
2006 layerLog4.push(message);
2007 return cb(null, message);
2008 }
2009];
2010
2011var outboundLayers = [
2012 function(message, cb){
2013 layerLog1.push(message);
2014 return cb(null, message);
2015 },
2016 function(message, cb){
2017 layerLog2.push(message);
2018 return cb(null, message);
2019 }
2020];
2021
2022var serviceConfig = {
2023 secure: true,
2024 services:{
2025 protocol:{
2026 config:{
2027 outboundLayers:outboundLayers,
2028 inboundLayers:inboundLayers
2029 }
2030 }
2031 }
2032};
2033
2034service.create(serviceConfig,
2035
2036 function (e, happnInst) {
2037
2038 if (e) return callback(e);
2039 var serviceInstance = happnInst;
2040
2041 happn_client.create(
2042 {
2043 username: '_ADMIN',
2044 password: 'happn'
2045 info:{
2046 from:'startup'
2047 }
2048 }, function (e, instance) {
2049
2050 if (e) return callback(e);
2051
2052 var clientInstance = instance;
2053
2054 clientInstance.on('/did/both', function(data){
2055
2056 expect(layerLog1.length > 0).to.be(true);
2057 expect(layerLog2.length > 0).to.be(true);
2058 expect(layerLog3.length > 0).to.be(true);
2059 expect(layerLog4.length > 0).to.be(true);
2060
2061 clientInstance.disconnect(function(){
2062
2063 serviceInstance.stop({reconnect:false}, callback);
2064 });
2065 }, function(e){
2066 if (e) return callback(e);
2067 clientInstance.set('/did/both', {'test':'data'}, function(e){
2068
2069 if (e) return callback(e);
2070 });
2071 });
2072 });
2073 }
2074);
2075
2076```
2077
2078CONSISTENCY (Quality of service)
2079--------------------------------
2080*set and remove operations can be done with an optional parameter called consistency, which changes the behaviour of the resulting publish, consistency values are numeric and follow:*
2081
20820 - QUEUED (spray and pray) - the publication is queued and the callback happens, when you are optimistic about the publish happening
2083
20841 - DEFERRED (asynchronous notification) - the publication is queued and the callback happens, but you are required to pass in the onPublish handler and will thus get a notification on how the publish went later
2085
20862 - TRANSACTIONAL (default) - the set callback only happens once all subscribers have been notified of the data change
2087
20883 - ACKNOWLEDGED - the publication is queued and the set/remove callback happens, each subscriber will receive the publication message and will answer with an ack message, the publication results come back with a new metric 'acknowledged'
2089
2090*an optional handler in the set/remove options, called onPublished will return with a log of how the resulting publication went*
2091
2092```javascript
2093
2094var CONSISTENCY = {
2095 DEFERRED: 1, //queues the publication, then calls back
2096 TRANSACTIONAL: 2, //waits until all recipients have been written to, then calls back
2097 ACKNOWLEDGED: 3 //waits until all recipients have acknowledged they have received the message
2098}
2099
2100clientInstance1.set('/test/path/acknowledged/1', {test: 'data'}, {
2101
2102 consistency: CONSISTENCY.ACKNOWLEDGED,
2103
2104 onPublished: function (e, results) {
2105
2106 //results look like this:
2107 expect(results).to.eql(
2108 {
2109 successful: 1,
2110 acknowledged:1,
2111 failed: 0,
2112 skipped: 0,
2113 queued:1
2114 }
2115 )
2116 }
2117}, function (e) {
2118
2119 //handle error here
2120})
2121
2122```
2123
2124STANDARDS COMPLIANCE
2125--------------------
2126- password hashes - pkdbf2, SHA512 (SHA1 previously or on node v0.*)
2127- asynchronous encryption (session secret teleportation) - ECIES (bitcore)
2128- synchronous encryption - AES-256
2129- signing and verifying - ECDSA (bitcore)
2130
2131####*NB: the strict bucket is not backwards compatible with happn-1*
2132
2133TESTING WITH KARMA
2134------------------
2135
2136testing payload encryption on the browser:
2137gulp --gulpfile test/browser/gulp-01.js
2138
2139
2140OTHER PLACES WHERE HAPPN-3 IS USED:
2141----------------------------------
2142HAPPNER - an experimental application engine that uses happn for its nervous system, see: www.github.com/happner/happner-2 - happner is now on version 2 so relatively mature.