diff --git a/.nvmrc b/.nvmrc index 9da0a09..c0bcaeb 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -8.4.0 \ No newline at end of file +8.7.0 \ No newline at end of file diff --git a/README.md b/README.md index d066444..da6170c 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ $ yarn add @hapiness/core rxjs ```javascript "dependencies": { - "@hapiness/core": "^1.0.0", + "@hapiness/core": "^1.1.0", //... } //... @@ -95,7 +95,7 @@ $ yarn add @hapiness/core rxjs ### Use Hapiness API -Actually, we're in : **v1.0.0** +Actually, we're in : **v1.1.0** See [API](https://github.com/hapinessjs/hapiness/blob/master/API.md) Reference to know what's already implemented. @@ -115,6 +115,9 @@ To set up your development environment: [Back to top](#table-of-contents) ## Change History +* v1.1.0 (2017-10-16) + * `Websocket` Server: Secure configuration + * Documentation * v1.0.0 (2017-10-05) * Publish all features of API * First stable version diff --git a/package-lock.json b/package-lock.json index 99e72e3..9de7037 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@hapiness/core", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -23,7 +23,7 @@ "integrity": "sha512-Id1hnmfd+7G9K+jWz2syfMcpymx2mj6B1y4C72vAoYQzxOA79UhY/kNvOCyb9yYR1SoSaHyhwcYtWKKqUiLTZA==", "dev": true, "requires": { - "@types/node": "8.0.32" + "@types/node": "8.0.34" } }, "@types/hapi": { @@ -35,7 +35,7 @@ "@types/catbox": "7.1.3", "@types/joi": "10.4.3", "@types/mimos": "3.0.1", - "@types/node": "8.0.32", + "@types/node": "8.0.34", "@types/podium": "1.0.0", "@types/shot": "3.4.0" } @@ -64,9 +64,9 @@ } }, "@types/node": { - "version": "8.0.32", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.32.tgz", - "integrity": "sha512-n1zzgeQehndikZc/8N4rGSZc99cO6Tb3OInKzvWYniJsT/jet3m57buaBFa5cMeVNFosN4PKZ2LM1y16CFD7Rg==" + "version": "8.0.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.34.tgz", + "integrity": "sha512-Jnmm57+nHqvJUPwUzt1CLoLzFtF2B2vgG7cWFut+a4nqTp9/L6pL0N+o0Jt3V7AQnCKMsPEqQpLFZYleBCdq3w==" }, "@types/podium": { "version": "1.0.0", @@ -78,7 +78,7 @@ "resolved": "https://registry.npmjs.org/@types/shot/-/shot-3.4.0.tgz", "integrity": "sha1-RZR3xRh9Pr0wNmCrCZ5+ng87ZW8=", "requires": { - "@types/node": "8.0.32" + "@types/node": "8.0.34" } }, "@types/websocket": { @@ -86,7 +86,7 @@ "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-0.0.34.tgz", "integrity": "sha512-cvN32rSoC4aXpXHG2Gf8/f8/gtyA7PmE38E3ENsMymFPgoG8xD/kH/KAVhTINCZTPqmgaWN86c1MitMEsuHjnQ==", "requires": { - "@types/node": "8.0.32" + "@types/node": "8.0.34" } }, "abbrev": { @@ -853,7 +853,7 @@ "hoek": "4.2.0", "iron": "4.0.5", "items": "2.1.1", - "joi": "11.1.1", + "joi": "11.4.0", "mimos": "3.0.3", "podium": "1.3.0", "shot": "3.4.2", @@ -1228,9 +1228,9 @@ "integrity": "sha1-i9FtnIOxlSneWuoyGsqtp4NkoZg=" }, "joi": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-11.1.1.tgz", - "integrity": "sha512-hffQzH42mYLvUCqhUPZZGegiiIjVvHcOV8mrxXPci8qZFOp2sHK4778GPyI3ZlvqTOHs8qZN6DovDnBR1slO4g==", + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-11.4.0.tgz", + "integrity": "sha512-O7Uw+w/zEWgbL6OcHbyACKSj0PkQeUgmehdoXVSxt92QFCq4+1390Rwh5moI2K/OgC7D8RHRZqHZxT2husMJHA==", "requires": { "hoek": "4.2.0", "isemail": "3.0.0", @@ -1615,9 +1615,9 @@ } }, "mocha-typescript": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/mocha-typescript/-/mocha-typescript-1.1.9.tgz", - "integrity": "sha512-q+J7oN6iWRgNkxMzaoqKPTp3mqlw5bj+TB2E4yRpvze7Yh8ES5W0aczn81RsXhr1iw6tPkpz+rGZC0WaX5dd3Q==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/mocha-typescript/-/mocha-typescript-1.1.11.tgz", + "integrity": "sha512-esJXD5hwbpH1+DBjx7WFXH6b+YIdTFShARUhc9hSW+DTuiWy907cnpaZtTmL+Gojhz/4ftE8gEuf5TSTos3t+A==", "dev": true, "requires": { "chalk": "1.1.3", @@ -2495,9 +2495,9 @@ } }, "tslib": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.7.1.tgz", - "integrity": "sha1-vIAEFkaRkjp5/oN4u+s9ogF1OOw=", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.8.0.tgz", + "integrity": "sha512-ymKWWZJST0/CkgduC2qkzjMOWr4bouhuURNXCn/inEX0L57BnRG6FhX76o7FOnsjHazCjfU2LKeSrlS2sIKQJg==", "dev": true }, "tslint": { @@ -2514,17 +2514,17 @@ "minimatch": "3.0.4", "resolve": "1.4.0", "semver": "5.4.1", - "tslib": "1.7.1", - "tsutils": "2.11.2" + "tslib": "1.8.0", + "tsutils": "2.12.1" } }, "tsutils": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.11.2.tgz", - "integrity": "sha1-YBNgHjb6FP+VhBPlQdQn+4xqw0E=", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.12.1.tgz", + "integrity": "sha1-9Nlc4zkciXHkblTEzw7bCiHdWyQ=", "dev": true, "requires": { - "tslib": "1.7.1" + "tslib": "1.8.0" } }, "tunnel-agent": { diff --git a/package.json b/package.json index 412bfa0..532e448 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hapiness/core", - "version": "1.0.0", + "version": "1.1.0", "description": "Project to have a HapiJS (https://hapijs.com/) based framework to create easier NodeJS back-end with some awesome features", "main": "index.js", "types": "index.d.ts", @@ -76,7 +76,7 @@ "@types/hapi": "^16.1.10", "@types/hoek": "^4.1.3", "@types/joi": "^10.4.3", - "@types/node": "^8.0.32", + "@types/node": "^8.0.34", "@types/websocket": "^0.0.34", "debug": "^3.1.0", "hapi": "^16.6.2", @@ -88,12 +88,12 @@ "coveralls": "^3.0.0", "fs-extra": "^4.0.2", "istanbul": "^1.1.0-alpha.1", - "mocha": "^3.5.0", - "mocha-typescript": "^1.1.9", + "mocha": "^3.5.3", + "mocha-typescript": "^1.1.11", "rimraf": "^2.6.2", "rxjs": "^5.4.3", "ts-node": "^3.3.0", - "tslint": "^5.5.0", + "tslint": "^5.7.0", "typescript": "^2.5.3", "unit.js": "^2.0.0" }, diff --git a/src/extensions/socket-server/README.md b/src/extensions/socket-server/README.md index 2f34a50..e6e3206 100644 --- a/src/extensions/socket-server/README.md +++ b/src/extensions/socket-server/README.md @@ -30,10 +30,18 @@ Configuration: ``` Allow to get the hapi server instance +## Extension service +```javascript + ... + constructor(private server: SocketServerService) {} + ... +``` + ## Socket Socket object provided when a new connection comes in the ServerSocket - methods + - `on$` - Listen new event with an Observable - `on` - Add listener on a new event coming from the socket - `onBytes` - Add listener on a new binary data coming from the socket - `emit` - Send data into the socket @@ -47,8 +55,11 @@ Socket object provided when a new connection comes in the ServerSocket WebSocket server - methods - - `onRequest` - Callback called at each new socket connection - - arguments: ((socket: Socket) => void) + - `configure` - Allow to configure a secure callback to accept new request + - arguments: request => Observable\ + - return: connections() + - `connections` - Observable providing accepted requests + - return: Subject\ - `getSockets` - Return all active sockets - return: Socket[] - `broadcast` - Broadcast data to all active sockets @@ -60,17 +71,24 @@ WebSocket server ```javascript @HapinessModule({ - version: 'x.x.x' + version: 'x.x.x', + providers: [ SocketServerService ] }) class SocketServerModule implements OnStart { - constructor(@Inject(SocketServerExt) private server: WebSocketServer) {} + constructor(private server: SocketServerService) {} onStart() { - this.server.onRequest((socket: Socket) => { - socket.on('message', _ => console.log(_)); - socket.emit('message', 'Hello World!'); - this.server.broadcast('join', 'Hello World!'); + this + .server + .configure(req => Observable.of(true)) // accept requests + .subscribe( + socket => { + socket.on('message', _ => console.log(_)); + socket.emit('message', 'Hello World!'); + this.server.broadcast('join', 'Hello World!') + } + ); }); } diff --git a/src/extensions/socket-server/server.ts b/src/extensions/socket-server/server.ts index 9250e19..d221621 100644 --- a/src/extensions/socket-server/server.ts +++ b/src/extensions/socket-server/server.ts @@ -1,3 +1,4 @@ +import { Observable, Subject } from 'rxjs/Rx'; import { server, request } from 'websocket'; import { Socket } from './socket'; import { SocketConfig } from './extension'; @@ -7,10 +8,11 @@ import { WebSocketRooms } from './rooms'; export class WebSocketServer { private server: server; - private subscribers: Array<(socket: Socket) => void>; + private connections$ = new Subject(); private sockets: Socket[]; private httpServer: http.Server | https.Server; private rooms: WebSocketRooms; + private secure: ((request: request) => Observable) = () => Observable.of(true); constructor(config: SocketConfig) { /* istanbul ignore next */ @@ -28,9 +30,14 @@ export class WebSocketServer { const _config = Object.assign({ httpServer: this.httpServer }, config); this.server = new server(_config); this.sockets = []; - this.subscribers = []; this.server.on('request', _request => { - this.onRequestHandler(_request); + this + .secure(_request) + .subscribe( + _ => !!_ ? + this.onRequestHandler(_request) : + _request.reject(403, 'Forbidden') + ); }); this.rooms = new WebSocketRooms(); } @@ -41,23 +48,35 @@ export class WebSocketServer { * * @param {request} _request */ - private onRequestHandler(_request: request) { + private onRequestHandler(_request: request): void { const connection = _request.accept(null, _request.origin); const socket = new Socket(_request, connection, this.rooms); const index = this.sockets.push(socket) - 1; - this.subscribers.forEach(sub => sub.apply(this, [socket])); connection.on('close', conn => { this.sockets.splice(index, 1); }); + this.connections$.next(socket); } /** - * Subscribe to new socket connections + * Configure a secure callback + * to accept/reject requests * - * @param {(socket:Socket)=>void} callback + * @param {(request:request)=>Observable} secure + * @returns Subject */ - public onRequest(callback: (socket: Socket) => void) { - this.subscribers.push(callback); + public configure(secure: (request: request) => Observable): Subject { + this.secure = (!!secure ? secure : this.secure); + return this.connections$; + } + + /** + * Get connections Subject + * + * @returns Subject + */ + public connections(): Subject { + return this.connections$; } /** diff --git a/src/extensions/socket-server/socket.ts b/src/extensions/socket-server/socket.ts index 0e2a1b4..ec5ec39 100644 --- a/src/extensions/socket-server/socket.ts +++ b/src/extensions/socket-server/socket.ts @@ -1,8 +1,25 @@ import { connection, request } from 'websocket'; import { WebSocketRooms } from './rooms'; +import { Subject, Observable } from 'rxjs/Rx'; + +interface Message { + event: string; + data: any; +} export class Socket { - constructor(_request: request, private _connection: connection, private _rooms: WebSocketRooms) {} + + private data$ = new Subject(); + + constructor( + _request: request, + private _connection: connection, + private _rooms: WebSocketRooms + ) { + this.on('close', data => this.data$.complete()); + this.on('error', err => this.data$.error(err)); + this.on('*', data => this.data$.next(this.getJSON(data.utf8Data))) + } /** * Listen events @@ -24,7 +41,7 @@ export class Socket { default: this._connection.on('message', message => { if (message.type === 'utf8') { - const parsed = this.getJSON(message.utf8Data); + const parsed = this.getJSON(message.utf8Data); if (parsed.event === event) { callback(parsed.data); } @@ -33,6 +50,20 @@ export class Socket { } } + /** + * Listen data filtered by event + * in a Observable + * + * @param {string} event + * @returns Observable + */ + on$(event: string): Observable { + return this + .data$ + .filter(_ => _ && _.event === event) + .map(_ => _.data); + } + /** * Listen to binary data * @@ -98,7 +129,7 @@ export class Socket { return JSON.parse(data); } catch (e) { /* istanbul ignore next */ - return {}; + return data; } } } diff --git a/test/integration/socket-server-rooms.test.ts b/test/integration/socket-server-rooms.test.ts index 6cc2bb4..d3e3689 100644 --- a/test/integration/socket-server-rooms.test.ts +++ b/test/integration/socket-server-rooms.test.ts @@ -18,25 +18,31 @@ export class SocketServerIntegration { constructor(private server: SocketServerService) {} onStart() { - this.server.instance().onRequest(socket => { - unit.array(this.server.instance().getSockets()) - .hasLength(1); - socket - .join('room1') - .join('room2'); - socket.on('close', data => {}); - socket.on('error', data => {}); - socket.on('tata', data => {}); - socket.on('*', data => { - unit.string(data.utf8Data).is('received'); - socket.close(); - this - .server - .stop() - .subscribe(_ => done()); - }); - this.server.instance().to('room1', 'message', { foo: 'bar' }); - }); + this + .server + .instance() + .connections() + .subscribe( + socket => { + unit.array(this.server.instance().getSockets()) + .hasLength(1); + socket + .join('room1') + .join('room2'); + socket.on('close', data => {}); + socket.on('error', data => {}); + socket.on('tata', data => {}); + socket.on('*', data => { + unit.string(data.utf8Data).is('received'); + socket.close(); + this + .server + .stop() + .subscribe(_ => done()); + }); + this.server.instance().to('room1', 'message', { foo: 'bar' }); + } + ); const W3CWebSocket = require('websocket').w3cwebsocket; const client = new W3CWebSocket('ws://localhost:2223/'); diff --git a/test/integration/socket-server.test.ts b/test/integration/socket-server.test.ts index efa3657..cc2d46c 100644 --- a/test/integration/socket-server.test.ts +++ b/test/integration/socket-server.test.ts @@ -1,3 +1,4 @@ +import { Observable } from 'rxjs/Rx'; import { suite, test } from 'mocha-typescript'; import * as unit from 'unit.js'; import { Hapiness, HapinessModule, OnStart } from '../../src/core'; @@ -18,31 +19,45 @@ export class SocketServerIntegration { constructor(private server: SocketServerService) {} onStart() { - this.server.instance().onRequest(socket => { - unit.array(this.server.instance().getSockets()) - .hasLength(1); - socket.emit('toto', 'test'); - socket.on('close', data => {}); - socket.on('error', data => {}); - socket.on('tata', data => {}); - socket.on('ev', data => { - this.server.instance().broadcast('test', 'test'); - }); - socket.on('*', data => { - if (data.utf8Data === '123') { - unit.string(data.utf8Data) - .is('123'); - socket.emitBytes(new Buffer('test')); + // this.server.instance().onRequest(socket => { + this + .server + .instance() + .configure(_ => Observable.of(true)) + .subscribe( + socket => { + unit.array(this.server.instance().getSockets()) + .hasLength(1); + socket.emit('toto', 'test'); + socket.on('close', data => {}); + socket.on('error', data => {}); + socket.on('tata', data => {}); + socket.on('ev', data => { + this.server.instance().broadcast('test', 'test'); + }); + socket.on('*', data => { + if (data.utf8Data === '123') { + unit.string(data.utf8Data) + .is('123'); + socket.emitBytes(new Buffer('test')); + } + }); + socket.onBytes(data => { + unit.string(data.toString()) + .is('test'); + socket.emit('obs', 'obs'); + }); + socket + .on$('FINAL') + .subscribe( + _ => { + unit.string(_).is('final'); + socket.close(); + this.server.stop().subscribe(() => done()); + } + ); } - }); - socket.onBytes(data => { - unit.string(data.toString()) - .is('test'); - socket.close(); - this.server.stop() - .subscribe(_ => done()); - }); - }); + ); const W3CWebSocket = require('websocket').w3cwebsocket; const client = new W3CWebSocket('ws://localhost:2222/'); @@ -51,8 +66,10 @@ export class SocketServerIntegration { client.send('{"event":"ev","data":"abc"}'); } else if (e.data instanceof ArrayBuffer) { client.send(e.data); - } else { + } else if (e.data === JSON.stringify({ event: 'test', data: 'test' })) { client.send('123'); + } else { + client.send('{"event":"FINAL","data":"final"}'); } }; } diff --git a/test/unit/extensions/socket-server/rooms.test.ts b/test/unit/extensions/socket-server/rooms.test.ts index 92d6f28..b510a2b 100644 --- a/test/unit/extensions/socket-server/rooms.test.ts +++ b/test/unit/extensions/socket-server/rooms.test.ts @@ -20,8 +20,8 @@ export class WebSocketRoomsTestSuite { @test('Should test all functions') testFunctions() { - const connection1 = { sendUTF() {} }; - const connection2 = { sendUTF() {} }; + const connection1 = { sendUTF() {}, on() {} }; + const connection2 = { sendUTF() {}, on() {} }; unit.stub(connection1, 'sendUTF'); unit.stub(connection2, 'sendUTF'); const instance = new WebSocketRooms(); diff --git a/yarn.lock b/yarn.lock index b123818..1cde96c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -48,9 +48,9 @@ dependencies: "@types/mime-db" "*" -"@types/node@*", "@types/node@^8.0.32": - version "8.0.32" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.32.tgz#869a716538b6eec65ab3893f183d557be3cda206" +"@types/node@*", "@types/node@^8.0.34": + version "8.0.34" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.34.tgz#55f801fa2ddb2a40dd6dfc15ecfe1dde9c129fe9" "@types/podium@*": version "1.0.0" @@ -504,8 +504,8 @@ diff@3.2.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" diff@^3.1.0, diff@^3.2.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" + version "3.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" ecc-jsbn@~0.1.1: version "0.1.1" @@ -931,8 +931,8 @@ joi@10.x.x: topo "2.x.x" joi@^11.1.0: - version "11.1.1" - resolved "https://registry.yarnpkg.com/joi/-/joi-11.1.1.tgz#08194537d3b9c796bac8666deb3a825c1a5d83c1" + version "11.4.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-11.4.0.tgz#f674897537b625e9ac3d0b7e1604c828ad913ccb" dependencies: hoek "4.x.x" isemail "3.x.x" @@ -1162,14 +1162,14 @@ mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.1: dependencies: minimist "0.0.8" -mocha-typescript@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/mocha-typescript/-/mocha-typescript-1.1.9.tgz#0aa43e30fdaf3116c2d7ba846a8f5643e9bb667b" +mocha-typescript@^1.1.11: + version "1.1.11" + resolved "https://registry.yarnpkg.com/mocha-typescript/-/mocha-typescript-1.1.11.tgz#8fc357b72968ee66a2d86cc88c3fc24d06ed4198" dependencies: chalk "^1.1.3" yargs "^6.5.0" -mocha@^3.5.0: +mocha@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d" dependencies: @@ -1705,10 +1705,10 @@ tsconfig@^6.0.0: strip-json-comments "^2.0.0" tslib@^1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.7.1.tgz#bc8004164691923a79fe8378bbeb3da2017538ec" + version "1.8.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.8.0.tgz#dc604ebad64bcbf696d613da6c954aa0e7ea1eb6" -tslint@^5.5.0: +tslint@^5.7.0: version "5.7.0" resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.7.0.tgz#c25e0d0c92fa1201c2bc30e844e08e682b4f3552" dependencies: @@ -1724,8 +1724,8 @@ tslint@^5.5.0: tsutils "^2.8.1" tsutils@^2.8.1: - version "2.11.2" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.11.2.tgz#6013601e36fa14ff958413e541d427fb8c6ac341" + version "2.12.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.12.1.tgz#f4d95ce3391c8971e46e54c4cf0edb0a21dd5b24" dependencies: tslib "^1.7.1"