diff --git a/src/bucket-manager.ts b/src/bucket-manager.ts index 8ee7c1a..cef76f1 100644 --- a/src/bucket-manager.ts +++ b/src/bucket-manager.ts @@ -11,7 +11,8 @@ import * as zlib from "zlib" import * as compressible from "compressible" import express = require("express"); import {CommsController} from "./socket-api/comms-controller"; -import {EventType} from "./socket-api/socket-event-types"; +import {ClientInstructionType} from "./socket-api/socket-event-types"; +import {ClientInstruction} from "./socket-api/client-instruction"; import * as def from "webinate-users"; /** @@ -270,8 +271,8 @@ export class BucketManager var updateResult = await stats.updateOne({ user: user }, { $inc: { apiCallsUsed: 1 } }); // Send bucket added events to sockets - var fEvent: def.SocketEvents.IBucketAddedEvent = { eventType: EventType.BucketUploaded, bucket: bucketEntry, username: user, error : undefined }; - await CommsController.singleton.broadcastEventToAll(fEvent); + var token: def.SocketEvents.IBucketToken = { type: ClientInstructionType[ClientInstructionType.BucketUploaded], bucket: bucketEntry, username: user }; + await CommsController.singleton.processClientInstruction( new ClientInstruction(token, null, user)); return gBucket; } @@ -379,8 +380,8 @@ export class BucketManager var result = await stats.updateOne({ user: bucketEntry.user }, { $inc: { apiCallsUsed : 1 } }); // Send events to sockets - var fEvent: def.SocketEvents.IBucketRemovedEvent = { eventType: EventType.BucketRemoved, bucket: bucketEntry, error : undefined }; - await CommsController.singleton.broadcastEventToAll(fEvent); + var token: def.SocketEvents.IBucketToken = { type: ClientInstructionType[ClientInstructionType.BucketRemoved], bucket: bucketEntry, username : bucketEntry.user }; + await CommsController.singleton.processClientInstruction(new ClientInstruction(token, null, bucketEntry.user )); return bucketEntry; } @@ -433,8 +434,8 @@ export class BucketManager await stats.updateOne({ user: bucketEntry.user }, { $inc: { memoryUsed: -fileEntry.size, apiCallsUsed: 1 } }); // Update any listeners on the sockets - var fEvent: def.SocketEvents.IFileRemovedEvent = { eventType: EventType.FileRemoved, file: fileEntry, error : undefined }; - await CommsController.singleton.broadcastEventToAll(fEvent); + var token: def.SocketEvents.IFileToken = { type: ClientInstructionType[ClientInstructionType.FileRemoved], file: fileEntry, username : fileEntry.user }; + await CommsController.singleton.processClientInstruction(new ClientInstruction(token, null, fileEntry.user)); return fileEntry; } diff --git a/src/controllers/bucket-controller.ts b/src/controllers/bucket-controller.ts index 566cd68..a0cabe3 100644 --- a/src/controllers/bucket-controller.ts +++ b/src/controllers/bucket-controller.ts @@ -17,7 +17,8 @@ import * as compression from "compression"; import * as winston from "winston"; import * as gcloud from "gcloud"; import {CommsController} from "../socket-api/comms-controller"; -import {EventType} from "../socket-api/socket-event-types"; +import {ClientInstruction} from "../socket-api/client-instruction"; +import {ClientInstructionType} from "../socket-api/socket-event-types"; import * as def from "webinate-users"; import {okJson, errJson} from "../serializers"; @@ -810,8 +811,8 @@ export class BucketController extends Controller for (var i = 0, l = files.length; i < l; i++) { // Send file added events to sockets - var fEvent: def.SocketEvents.IFileAddedEvent = { username: user, eventType: EventType.FileUploaded, file: files[i], error : undefined }; - await CommsController.singleton.broadcastEventToAll(fEvent) + var token: def.SocketEvents.IFileToken = { username: user, type: ClientInstructionType[ClientInstructionType.FileUploaded], file: files[i] }; + await CommsController.singleton.processClientInstruction(new ClientInstruction(token, null, user)) } diff --git a/src/definitions/custom/definitions.d.ts b/src/definitions/custom/definitions.d.ts index a52a650..c57e546 100644 --- a/src/definitions/custom/definitions.d.ts +++ b/src/definitions/custom/definitions.d.ts @@ -10,27 +10,30 @@ */ export module SocketEvents { - /* - * The base interface for all socket events - */ - export interface IEvent - { - eventType: number; - - /* - * Will be null if no error, or a string if there is - */ - error: string; - } - - /* - * A very simple echo event. This simply pings the server with a message, which then returns with the same message - * either to the client or, if broadcast is true, to all clients. - */ - export interface IEchoEvent extends IEvent + export type ClientInstructionType = ( + 'Login' | + 'Logout' | + 'Activated' | + 'Removed' | + 'FileUploaded' | + 'FileRemoved' | + 'BucketUploaded' | + 'BucketRemoved' | + 'MetaRequest' + ); + + export type ServerInstructionType = ( + 'MetaRequest' + ); + + /** + * The base interface for all data that is serialized & sent to clients or server. + * The type property describes to the reciever what kind of data to expect. + */ + export interface IToken { - message: string; - broadcast?: boolean; + error? : string; + type: ClientInstructionType | ServerInstructionType | string; } /* @@ -38,17 +41,17 @@ * if you provide a property value, then only that specific meta property is edited. * If not provided, then the entire meta data is set. */ - export interface IMetaEvent extends IEvent + export interface IMetaToken extends IToken { username?: string; - property: string; - val: any; + property?: string; + val?: any; } /* * The socket user event */ - export interface IUserEvent extends IEvent + export interface IUserToken extends IToken { username: string; } @@ -56,36 +59,20 @@ /* * Interface for file added events */ - export interface IFileAddedEvent extends IEvent + export interface IFileToken extends IToken { username: string; file: IFileEntry; } - /* - * Interface for file removed events - */ - export interface IFileRemovedEvent extends IEvent - { - file: IFileEntry; - } - /* * Interface for a bucket being added */ - export interface IBucketAddedEvent extends IEvent + export interface IBucketToken extends IToken { username: string; bucket: IBucketEntry } - - /* - * Interface for a bucket being removed - */ - export interface IBucketRemovedEvent extends IEvent - { - bucket: IBucketEntry - } } /* @@ -181,6 +168,12 @@ */ export interface IWebsocket { + /** + * A key that must be provided in the headers of socket client connections. If the connection headers + * contain 'users-api-key', and it matches this key, then the connection is considered an authorized connection. + */ + socketApiKey: string; + /** * The port number to use for web socket communication. You can use this port to send and receive events or messages * to the server. diff --git a/src/dist-files/example-config.json b/src/dist-files/example-config.json index 58a5eb3..d3bb021 100644 --- a/src/dist-files/example-config.json +++ b/src/dist-files/example-config.json @@ -58,6 +58,7 @@ }, "websocket": { "port": 8063, + "socketApiKey" : "my-secret-key", "approvedSocketDomains": [ "localhost", "^ws://www.webinate.net:123$", diff --git a/src/socket-api/client-connection.ts b/src/socket-api/client-connection.ts index 066c2ab..e0247fc 100644 --- a/src/socket-api/client-connection.ts +++ b/src/socket-api/client-connection.ts @@ -10,8 +10,7 @@ import * as fs from "fs"; import * as winston from "winston"; import {UserManager, User} from "../users"; import {CommsController} from "./comms-controller"; -import {ClientEvent} from "./client-event"; -import {EventResponseType, EventType} from "./socket-event-types"; +import {ServerInstruction} from "./server-instruction"; import {SocketAPI} from "./socket-api"; /** @@ -19,16 +18,19 @@ import {SocketAPI} from "./socket-api"; */ export class ClientConnection { + public onDisconnected: (connection: ClientConnection) => void; public ws: ws; public user: User; public domain: string; + public authorizedThirdParty: boolean; private _controller: CommsController; - constructor(ws: ws, domain: string, controller : CommsController) + constructor(ws: ws, domain: string, controller : CommsController, authorizedThirdParty: boolean) { var that = this; this.domain = domain; this._controller = controller; + this.authorizedThirdParty = authorizedThirdParty; UserManager.get.loggedIn(ws.upgradeReq, null).then(function (user) { @@ -49,8 +51,8 @@ export class ClientConnection { winston.info(`Received message from client: '${message}'`, { process: process.pid } ); try { - var event : def.SocketEvents.IEvent = JSON.parse(message); - this._controller.alertMessage(new ClientEvent(event, this)); + var token : def.SocketEvents.IToken = JSON.parse(message); + this._controller.processServerInstruction(new ServerInstruction(token, this)); } catch(err) { winston.error(`Could not parse socket message: '${err}'`, { process: process.pid } ); @@ -62,6 +64,9 @@ export class ClientConnection */ private onClose() { + if (this.onDisconnected) + this.onDisconnected(this); + winston.info(`Websocket disconnected: ${this.domain}`, {process : process.pid}) this.ws.removeAllListeners("message"); diff --git a/src/socket-api/client-event.ts b/src/socket-api/client-event.ts deleted file mode 100644 index 0633b74..0000000 --- a/src/socket-api/client-event.ts +++ /dev/null @@ -1,41 +0,0 @@ -"use strict"; - -import * as def from "webinate-users"; -import {ClientConnection} from "./client-connection"; -import {EventResponseType, EventType} from "./socket-event-types"; - -/** - * An event class that is emitted to all listeners of the communications controller. - * This wraps data around events sent via the web socket to the users server. Optionally - * these events can respond to the client who initiated the event as well as to all listeners. - */ -export class ClientEvent -{ - /** The client who initiated the request */ - client: ClientConnection; - - /** The event sent from the client */ - clientEvent: T; - - /** An optional response event to be sent back to the client or all connected clients. This is dependent on the responseType */ - responseEvent: def.SocketEvents.IEvent; - - /** Describes how users should respond to a socket event. By default the response is EventResponseType.NoResponse. - * if EventResponseType.RespondClient then the responseEvent is sent back to the initiating client. - * if EventResponseType.ReBroadcast then the responseEvent is sent to all clients. - */ - responseType: EventResponseType; - - /** - * BY default the error is null, but if set, then an error response is given to the client - */ - error : Error; - - constructor(event: T, client: ClientConnection) - { - this.client = client; - this.error = null; - this.clientEvent = event; - this.responseType = EventResponseType.NoResponse; - } -} \ No newline at end of file diff --git a/src/socket-api/client-instruction.ts b/src/socket-api/client-instruction.ts new file mode 100644 index 0000000..b4d4a4a --- /dev/null +++ b/src/socket-api/client-instruction.ts @@ -0,0 +1,33 @@ +"use strict"; + +import * as def from "webinate-users"; +import {ClientConnection} from "./client-connection"; + +/** + * An instruction that is generated by the server and sent to relevant clients. + */ +export class ClientInstruction +{ + /** + * Specify a username that if set, will only send this instruction to authorized clients + * and/or the spefic user who may be connected + */ + username: string; + + /** + * An array of clients to send the instruction to. If null, then all clients will be considered + */ + recipients: ClientConnection[]; + + /** + * The event sent from the client + */ + token: T; + + constructor(event: T, client: ClientConnection[] = null, username : string = null ) + { + this.recipients = client; + this.token = event; + this.username = username; + } +} \ No newline at end of file diff --git a/src/socket-api/comms-controller.ts b/src/socket-api/comms-controller.ts index 8388913..7c1aa08 100644 --- a/src/socket-api/comms-controller.ts +++ b/src/socket-api/comms-controller.ts @@ -1,5 +1,6 @@ "use strict"; +import * as bcrypt from "bcryptjs"; import * as ws from "ws"; import * as events from "events"; import * as mongodb from "mongodb"; @@ -9,10 +10,11 @@ import * as http from "http"; import * as fs from "fs"; import * as winston from "winston"; import {UserManager, User} from "../users"; -import {EventResponseType, EventType} from "./socket-event-types"; +import {ClientInstructionType, ServerInstructionType} from "./socket-event-types"; import {SocketAPI} from "./socket-api"; import {ClientConnection} from "./client-connection"; -import {ClientEvent} from "./client-event"; +import {ClientInstruction} from "./client-instruction"; +import {ServerInstruction} from "./server-instruction"; /** * A controller that deals with any any IPC or web socket communications @@ -21,6 +23,9 @@ export class CommsController extends events.EventEmitter { public static singleton: CommsController; private _server: ws.Server; + private _connections: ClientConnection[]; + private _hashedApiKey: string; + private _cfg: def.IConfig; /** * Creates an instance of the Communication server @@ -29,173 +34,193 @@ export class CommsController extends events.EventEmitter constructor(cfg: def.IConfig) { super(); - var that = this; CommsController.singleton = this; + this._connections = []; + this._cfg = cfg; + this._hashedApiKey = bcrypt.hashSync(cfg.websocket.socketApiKey); - // dummy request processing - this is not actually called as its handed off to the socket api - var processRequest = function (req, res) { - res.writeHead(200); - res.end("All glory to WebSockets!\n"); - }; - - // Create the web socket server - if (cfg.ssl) - { - winston.info("Creating secure socket connection", { process: process.pid }); - var httpsServer: https.Server = null; - var caChain = [fs.readFileSync(cfg.sslIntermediate), fs.readFileSync(cfg.sslRoot)]; - var privkey = cfg.sslKey ? fs.readFileSync(cfg.sslKey) : null; - var theCert = cfg.sslCert ? fs.readFileSync(cfg.sslCert) : null; - winston.info(`Attempting to start Websocket server with SSL...`, { process: process.pid }); - httpsServer = https.createServer( { key: privkey, cert: theCert, passphrase: cfg.sslPassPhrase, ca: caChain }, processRequest); - httpsServer.listen(cfg.websocket.port); - this._server = new ws.Server({ server: httpsServer }); - } - else - { - winston.info("Creating regular socket connection", { process: process.pid }); - this._server = new ws.Server({ port: cfg.websocket.port }); - } - - - winston.info("Websockets attempting to listen on HTTP port " + cfg.websocket.port, { process: process.pid }); + } - // Handle errors - this._server.on('error', function connection(err) { - winston.error("Websocket error: " + err.toString()); - that._server.close(); + /** + * Checks the header api key against the hash generated from the config + */ + checkApiKey(key : string) : Promise { + return new Promise( (resolve, reject) => { + bcrypt.compare(key, this._hashedApiKey, function (err, same: boolean) + { + if (err) + return reject(err); + else + return resolve(same); + }); }); + } - // A client has connected to the server - this._server.on('connection', function connection(ws: ws) - { - var headers = (ws.upgradeReq).headers; + /** + * Sends an instruction to the relevant client connections + * @param {ClientInstruction} instruction The instruction from the server + */ + processClientInstruction( instruction: ClientInstruction ) + { + let recipients: ClientConnection[]; - if (cfg.debugMode) - winston.info(`Websocket connection origin: ${headers.origin}`, {process : process.pid}) + if (!instruction.recipients) + recipients = this._connections; + else + recipients = instruction.recipients; - var clientApproved = false; - for (var i = 0, l = cfg.websocket.approvedSocketDomains.length; i < l; i++) - { - if ((headers.origin && headers.origin.match(new RegExp(cfg.websocket.approvedSocketDomains[i])))) - { - new ClientConnection(ws, headers.origin, that); - clientApproved = true; - } - } + let username = instruction.username; - if (!clientApproved) - { - winston.error(`A connection was made by ${headers.origin} but it is not on the approved domain list. Make sure the host is on the approvedSocketDomains parameter in the config file.`); - ws.terminate(); - ws.close(); - } - }); - - // Setup the socket API - new SocketAPI(this); + if (!username) { + for ( let recipient of recipients ) + this.sendToken( recipient, instruction.token ); + } + else { + for ( let recipient of recipients ) + if ( recipient.authorizedThirdParty || ( recipient.user && recipient.user.dbEntry.username == username ) ) + this.sendToken( recipient, instruction.token ); + } } /** - * Sends an event to all connected clients of this server listening for a specific event - * @param {ClientEvent} event The event to alert the server of - */ - alertMessage( event: ClientEvent ) + * Processes an instruction sent from a client. Any listeners of the comms controller will listen & react to the + * instruction - and in some cases might resond to the client with a ClientInstruction. + * @param {ServerInstruction} instruction The instruction from the client + */ + processServerInstruction( instruction: ServerInstruction ) { - if (!event.clientEvent) - return winston.error(`Websocket alert error: No ClientEvent set`, { process: process.pid } ); - - this.emit( EventType[event.clientEvent.eventType], event ); + if (!instruction.token) + return winston.error(`Websocket error: An instruction was sent from '${instruction.from.domain}' without a token`, { process: process.pid } ); - if (event.responseType != EventResponseType.NoResponse && !event.responseEvent) - return winston.error(`Websocket alert error: The response type is expecting a responseEvent but none exist`, { process: process.pid } ); + if (!ServerInstructionType[instruction.token.type]) + return winston.error(`Websocket error: An instruction was sent from '${instruction.from.domain}' with a type that is not recognised`, { process: process.pid } ); - if ( event.responseType == EventResponseType.RespondClient ) - this.broadcastEventToClient(event.responseEvent, event.client); - else if ( event.responseType == EventResponseType.ReBroadcast ) - this.broadcastEventToAll(event.responseEvent); + this.emit( instruction.token.type, instruction ); } /** - * Sends an event to the client specified - * @param {IEvent} event The event to broadcast - */ - broadcastEventToClient(event: def.SocketEvents.IEvent, client : ClientConnection ): Promise + * Attempts to send a token to a specific client + */ + private sendToken(connection : ClientConnection, token : def.SocketEvents.IToken) : Promise { - var that = this; - return new Promise(function (resolve, reject) - { - client.ws.send(JSON.stringify(event), undefined, function (error: Error) + return new Promise(function(resolve, reject) { + let serializedData: string; + + try { + serializedData = JSON.stringify(token) + } + catch (err) { + return reject(err); + } + + connection.ws.send( serializedData, undefined, function (error: Error) { - if (error) - { + if (error) { winston.error(`Websocket broadcase error: '${error}'`, { process: process.pid } ); - return reject(); + reject(error); } return resolve(); }); - }); + }) } /** - * Sends an event to all connected clients of this server listening for a specific event - * @param {IEvent} event The event to broadcast - */ - broadcastEventToAll(event: def.SocketEvents.IEvent): Promise + * Called whenever a new client connection is made to the WS server + * @param {ws} ws + */ + async onWsConnection(ws : ws) : Promise { - var that = this; - return new Promise(function (resolve, reject) + let headers = ws.upgradeReq.headers; + + if (this._cfg.debugMode) + winston.info(`Websocket client connected: ${headers.origin}`, {process : process.pid}) + + let clientApproved = false; + for ( let domain of this._cfg.websocket.approvedSocketDomains ) { - var numResponded = 0, - errorOccurred = false, - releventClients: Array = []; + // Check if the connecting client is an authorized third party (more privileges) + let authorizedThirdParty = false; + if ( headers['users-api-key'] && this._hashedApiKey ) { + winston.info("Checking socket API key"); + authorizedThirdParty = await this.checkApiKey(headers['users-api-key']); + } - // First find all listening clients that need to be notified when this event happens - for (var i = 0, l = that._server.clients.length; i < l; i++) + if ( authorizedThirdParty || (headers.origin && headers.origin.match(new RegExp(domain)))) { - var client = that._server.clients[i]; - releventClients.push(client); + let clientConnection = new ClientConnection(ws, headers.origin || 'AUTHORIZED-ACCESS', this, authorizedThirdParty); + + // Remove the client when its disconnected + clientConnection.onDisconnected = (connection: ClientConnection) => { + this._connections.splice(this._connections.indexOf(connection), 1); + } + + this._connections.push(clientConnection); + clientApproved = true; + break; } + } - // Now go through each client and let them know about the event - var clientLength = releventClients.length; - for (var i = 0; i < clientLength; i++) - { - var client = releventClients[i]; - client.send(JSON.stringify(event), undefined, function (error: Error) - { - if (errorOccurred) - return; - - if (error) - { - winston.error(`Websocket broadcase error: '${error}'`, { process: process.pid } ); - errorOccurred = true; - return reject(); - } - - numResponded++; - if (numResponded >= clientLength) - return resolve(); - }); - }; - - // No active listeners - if (clientLength == 0) - return resolve(); - }); + // The client was not approved - so kill the connection + if (!clientApproved) + { + winston.error(`A connection was made by ${headers.origin} but it is not on the approved domain list. Make sure the host is on the approvedSocketDomains parameter in the config file.`); + ws.terminate(); + ws.close(); + } } /** - * Called to initialize this controller and its related database objects - * @returns {Promise} - */ - initialize(db: mongodb.Db): Promise + * Initializes the comms controller + * @returns {Promise} + */ + async initialize(): Promise { - return Promise.resolve(null); + let cfg = this._cfg; + + // dummy request processing - this is not actually called as its handed off to the socket api + var processRequest = function (req, res) { + res.writeHead(200); + res.end("All glory to WebSockets!\n"); + }; + + // Create the web socket server + if (cfg.ssl) + { + winston.info("Creating secure socket connection", { process: process.pid }); + var httpsServer: https.Server = null; + var caChain = [fs.readFileSync(cfg.sslIntermediate), fs.readFileSync(cfg.sslRoot)]; + var privkey = cfg.sslKey ? fs.readFileSync(cfg.sslKey) : null; + var theCert = cfg.sslCert ? fs.readFileSync(cfg.sslCert) : null; + + winston.info(`Attempting to start Websocket server with SSL...`, { process: process.pid }); + httpsServer = https.createServer( { key: privkey, cert: theCert, passphrase: cfg.sslPassPhrase, ca: caChain }, processRequest); + httpsServer.listen(cfg.websocket.port); + this._server = new ws.Server({ server: httpsServer }); + } + else + { + winston.info("Creating regular socket connection", { process: process.pid }); + this._server = new ws.Server({ port: cfg.websocket.port }); + } + + winston.info("Websockets attempting to listen on HTTP port " + this._cfg.websocket.port, { process: process.pid }); + + // Handle errors + this._server.on('error', (err) => { + winston.error("Websocket error: " + err.toString()); + this._server.close(); + }); + + // A client has connected to the server + this._server.on('connection', (ws: ws) => { + this.onWsConnection(ws); + }); + + // Setup the socket API + new SocketAPI(this); } } \ No newline at end of file diff --git a/src/socket-api/server-instruction.ts b/src/socket-api/server-instruction.ts new file mode 100644 index 0000000..3545f70 --- /dev/null +++ b/src/socket-api/server-instruction.ts @@ -0,0 +1,26 @@ +"use strict"; + +import * as def from "webinate-users"; +import {ClientConnection} from "./client-connection"; + +/** + * An instruction that is generated by clients and sent to the server to react to + */ +export class ServerInstruction +{ + /** + * The client connection who initiated the request + */ + from: ClientConnection; + + /** + * The token sent from the client + */ + token: T; + + constructor(event: T, from: ClientConnection) + { + this.from = from; + this.token = event; + } +} \ No newline at end of file diff --git a/src/socket-api/socket-api.ts b/src/socket-api/socket-api.ts index d125ee4..700371a 100644 --- a/src/socket-api/socket-api.ts +++ b/src/socket-api/socket-api.ts @@ -1,9 +1,11 @@ "use strict"; +import * as winston from "winston"; import {UserManager, User, UserPrivileges} from "../users"; -import {ClientEvent} from "./client-event"; +import {ServerInstruction} from "./server-instruction"; +import {ClientInstruction} from "./client-instruction"; import {CommsController} from "./comms-controller"; -import {EventType, EventResponseType} from "./socket-event-types"; +import {ClientInstructionType, ServerInstructionType} from "./socket-event-types"; import * as def from "webinate-users"; /** @@ -18,60 +20,57 @@ export class SocketAPI this._comms = comms; // Setup all socket API listeners - comms.on( EventType[EventType.Echo], this.onEcho.bind(this) ); - comms.on( EventType[EventType.MetaRequest], this.onMeta.bind(this) ); + comms.on( ServerInstructionType[ServerInstructionType.MetaRequest], this.onMeta.bind(this) ); } /** * Responds to a meta request from a client * @param {SocketEvents.IMetaEvent} e */ - private onMeta( e: ClientEvent ) + private onMeta( e: ServerInstruction ) { var comms = this._comms; if (!UserManager.get) return; - UserManager.get.getUser(e.clientEvent.username).then(function(user) { + UserManager.get.getUser(e.token.username).then(function(user) { if ( !user ) - return Promise.reject("Could not find user " + e.clientEvent.username ); - if ( e.clientEvent.property && e.clientEvent.val !== undefined ) - return UserManager.get.setMetaVal(user.dbEntry, e.clientEvent.property, e.clientEvent.val ); - else if ( e.clientEvent.property ) - return UserManager.get.getMetaVal(user.dbEntry, e.clientEvent.property ); - else if ( e.clientEvent.val ) - return UserManager.get.setMeta(user.dbEntry, e.clientEvent.val ); + return Promise.reject(new Error("Could not find user " + e.token.username )); + + // Make sure the client is authorized to make this request + if ( !e.from.authorizedThirdParty ) + return Promise.reject( new Error("You do not have permission to make this request")); + + if ( e.token.property && e.token.val !== undefined ) + return UserManager.get.setMetaVal(user.dbEntry, e.token.property, e.token.val ); + else if ( e.token.property ) + return UserManager.get.getMetaVal(user.dbEntry, e.token.property ); + else if ( e.token.val ) + return UserManager.get.setMeta(user.dbEntry, e.token.val ); else return UserManager.get.getMetaData( user.dbEntry ); }).then(function( metaVal ) { - comms.broadcastEventToClient( { - error : undefined, - eventType : e.clientEvent.eventType, - val: metaVal - }, e.client ); + let responseToken : def.SocketEvents.IMetaToken = { + type : ClientInstructionType[ClientInstructionType.MetaRequest], + val: metaVal, + property: e.token.property, + username: e.token.username + }; + + comms.processClientInstruction(new ClientInstruction( responseToken, [e.from] )); }).catch(function( err: Error ) { - comms.broadcastEventToClient( { error : err.toString(), eventType : e.clientEvent.eventType }, e.client ); - }); - } - /** - * Responds to a echo request from a client - */ - private onEcho( e: ClientEvent ) - { - e.responseEvent = { - eventType: EventType.Echo, - message : e.clientEvent.message - }; - - if ( e.clientEvent.broadcast ) - e.responseType = EventResponseType.ReBroadcast; - else - e.responseType = EventResponseType.RespondClient; + let responseToken : def.SocketEvents.IMetaToken = { + type : ClientInstructionType[ClientInstructionType.MetaRequest], + error: err.message + }; + + comms.processClientInstruction(new ClientInstruction( responseToken, [e.from] )); + }); } } \ No newline at end of file diff --git a/src/socket-api/socket-event-types.ts b/src/socket-api/socket-event-types.ts index 23a59b3..a67eb52 100644 --- a/src/socket-api/socket-event-types.ts +++ b/src/socket-api/socket-event-types.ts @@ -1,81 +1,73 @@ "use strict"; /** -* Describes the event being sent to connected clients -*/ -export enum EventType + * Describes the type of token data being sent to connected clients + */ +export enum ClientInstructionType { /** * Event sent to clients whenever a user logs in. - * Event type: IUserEvent + * Event type: IUserToken */ Login = 1, /** * Event sent to clients whenever a user logs out. - * Event type: IUserEvent + * Event type: IUserToken */ Logout = 2, /** * Event sent to clients whenever a user's account is activated. - * Event type: IUserEvent + * Event type: IUserToken */ Activated = 3, /** * Event sent to clients whenever a user's account is removed. - * Event type: IUserEvent + * Event type: IUserToken */ Removed = 4, /** * Event sent to clients whenever a user uploads a new file. - * Event type: IFileAddedEvent + * Event type: IFileToken */ FileUploaded = 5, /** * Event sent to clients whenever a user file is removed. - * Event type: IFileRemovedEvent + * Event type: IFileToken */ FileRemoved = 6, /** * Event sent to clients whenever a user creates a new bucket - * Event type: IBucketAddedEvent + * Event type: IBucketToken */ BucketUploaded = 7, /** * Event sent to clients whenever a user removes a bucket - * Event type: IBucketRemovedEvent + * Event type: IBucketToken */ BucketRemoved = 8, /** * Event both sent to the server as well as optionally to clients. Gets or sets user meta data. - * Event type: IMetaEvent + * Event type: IMetaToken */ - MetaRequest = 9, - - /** - * Event both sent to the server as well as to clients. The echo simply echoes a message. - * Event type: IEchoEvent - */ - Echo = 10 + MetaRequest = 9 } -/** Describes how users should respond to a socket events +/** + * Describes the type of token data being sent to connected clients */ -export enum EventResponseType +export enum ServerInstructionType { - /** The default the response is EventResponseType.NoResponse. */ - NoResponse, - - /** A response event is sent back to the initiating client. */ - RespondClient, - - /** A response event is sent to all connected clients. */ - ReBroadcast + /** + * Event both sent to the server as well as optionally to clients. Gets or sets user meta data. + * Event type: IMetaToken + */ + MetaRequest = 9 } \ No newline at end of file diff --git a/src/startup.ts b/src/startup.ts index 3427682..e9e2be8 100644 --- a/src/startup.ts +++ b/src/startup.ts @@ -80,7 +80,7 @@ openDB(config).then(function (db) { winston.info(`Initializing controllers...`, { process: process.pid }); return Promise.all([ - new CommsController(config).initialize(db), + new CommsController(config).initialize(), new CORSController(app, config).initialize(db), new BucketController(app, config).initialize(db), new UserController(app, config).initialize(db), diff --git a/src/users.ts b/src/users.ts index a7d85b6..5a32d3a 100644 --- a/src/users.ts +++ b/src/users.ts @@ -10,7 +10,8 @@ import * as winston from "winston"; import * as https from "https"; import {CommsController} from "./socket-api/comms-controller"; -import {EventType} from "./socket-api/socket-event-types"; +import {ClientInstruction} from "./socket-api/client-instruction"; +import {ClientInstructionType} from "./socket-api/socket-event-types"; import * as def from "webinate-users"; import {SessionManager, Session} from "./session"; import {BucketManager} from "./bucket-manager"; @@ -166,8 +167,8 @@ export class UserManager if (useEntry) { // Send logged out event to socket - var sEvent: def.SocketEvents.IUserEvent = { username: useEntry.username, eventType: EventType.Logout, error : undefined }; - await CommsController.singleton.broadcastEventToAll(sEvent); + var token: def.SocketEvents.IUserToken = { username: useEntry.username, type: ClientInstructionType[ClientInstructionType.Logout] }; + await CommsController.singleton.processClientInstruction( new ClientInstruction(token, null, useEntry.username)); winston.info(`User '${useEntry.username}' has logged out`, { process: process.pid }); } @@ -325,8 +326,8 @@ export class UserManager var result = await this._userCollection.updateOne({ _id: user.dbEntry._id }, { $set: { registerKey: "" } }); // Send activated event - var sEvent: def.SocketEvents.IUserEvent = { username: username, eventType: EventType.Activated, error : undefined }; - await CommsController.singleton.broadcastEventToAll(sEvent); + var token: def.SocketEvents.IUserToken = { username: username, type: ClientInstructionType[ClientInstructionType.Activated] }; + await CommsController.singleton.processClientInstruction(new ClientInstruction(token, null, username)); winston.info(`User '${username}' has been activated`, { process: process.pid }); return; @@ -538,8 +539,8 @@ export class UserManager await this._userCollection.updateOne({ _id: user.dbEntry._id }, { $set: { registerKey: "" } }); // Send activated event - var sEvent: def.SocketEvents.IUserEvent = { username: username, eventType: EventType.Activated, error : undefined }; - await CommsController.singleton.broadcastEventToAll(sEvent); + var token: def.SocketEvents.IUserToken = { username: username, type: ClientInstructionType[ClientInstructionType.Activated] }; + await CommsController.singleton.processClientInstruction(new ClientInstruction(token, null, username)); winston.info(`User '${username}' has been activated`, { process: process.pid }); return true; @@ -681,10 +682,10 @@ export class UserManager throw new Error("Could not remove the user from the database"); // Send event to sockets - var sEvent: def.SocketEvents.IUserEvent = { username: username, eventType: EventType.Removed, error : undefined }; - CommsController.singleton.broadcastEventToAll(sEvent).then(function() { - winston.info(`User '${username}' has been removed`, { process: process.pid }); - }); + var token: def.SocketEvents.IUserToken = { username: username, type: ClientInstructionType[ClientInstructionType.Removed] }; + CommsController.singleton.processClientInstruction(new ClientInstruction(token, null, username)); + + winston.info(`User '${username}' has been removed`, { process: process.pid }); return; } @@ -765,8 +766,8 @@ export class UserManager throw new Error("Could not find the user in the database, please make sure its setup correctly"); // Send logged in event to socket - var sEvent: def.SocketEvents.IUserEvent = { username: username, eventType: EventType.Login, error : undefined }; - await CommsController.singleton.broadcastEventToAll(sEvent); + var token: def.SocketEvents.IUserToken = { username: username, type: ClientInstructionType[ClientInstructionType.Login] }; + await CommsController.singleton.processClientInstruction(new ClientInstruction(token, null, username)); return user; } diff --git a/test/tests.js b/test/tests.js index a8bba37..108b773 100644 --- a/test/tests.js +++ b/test/tests.js @@ -26,7 +26,6 @@ var activation = ""; var fileId = ""; var publicURL = ""; var wsClient; -var wsClient2; // A map of all web socket events var socketEvents = { @@ -58,47 +57,47 @@ var numWSCalls = { */ function onWsEvent(data) { - var event = JSON.parse(data); + var token = JSON.parse(data); - if (!event.eventType) - throw new Error("eventType does not exist on socket event"); + if (!token.type) + throw new Error("type does not exist on socket token"); - switch (event.eventType) + switch (token.type) { - case 1: // Login - socketEvents.login = event; + case 'Login': + socketEvents.login = token; numWSCalls.login++; break; - case 2: // Logout - socketEvents.logout = event; + case 'Logout': + socketEvents.logout = token; numWSCalls.logout++; break; - case 3: // Activated - socketEvents.activated = event; + case 'Activated': + socketEvents.activated = token; numWSCalls.activated++; break; - case 4: // Removed - socketEvents.removed = event; + case 'Removed': + socketEvents.removed = token; numWSCalls.removed++; break; - case 5: // FileUploaded - socketEvents.fileUploaded = event; + case 'FileUploaded': + socketEvents.fileUploaded = token; numWSCalls.fileUploaded++; break; - case 6: // FileRemoved - socketEvents.fileRemoved = event; + case 'FileRemoved': + socketEvents.fileRemoved = token; numWSCalls.fileRemoved++; break; - case 7: // BucketUploaded - socketEvents.bucketUploaded = event; + case 'BucketUploaded': + socketEvents.bucketUploaded = token; numWSCalls.bucketUploaded++; break; - case 8: // BucketRemoved - socketEvents.bucketRemoved = event; + case 'BucketRemoved': + socketEvents.bucketRemoved = token; numWSCalls.bucketRemoved++; break; - case 9: // MetaRequest - socketEvents.metaRequest = event; + case 'MetaRequest': + socketEvents.metaRequest = token; numWSCalls.metaRequest++; break; } @@ -127,7 +126,10 @@ describe('Testing WS connectivity', function() { it('connected to the users socket API', function(done) { var socketUrl = "ws://localhost:" + config.websocket.port; - wsClient = new ws( socketUrl, { headers: { origin: "localhost" } }); + var options = { headers: { origin: "localhost" } }; + options.headers['users-api-key'] = config.websocket.socketApiKey; + + wsClient = new ws( socketUrl, options); // Opens a stream to the users socket events wsClient.on('open', function () { @@ -140,71 +142,6 @@ describe('Testing WS connectivity', function() { return done(err); }); }) - - it('connected a separate WS connection', function(done) { - - var socketUrl = "ws://localhost:" + config.websocket.port; - wsClient2 = new ws( socketUrl, { headers: { origin: "localhost" } }); - - // Opens a stream to the users socket events - wsClient2.on('open', function () { - wsClient2.on( 'message', onSocketMessage ); - return done(); - }); - - // Report if there are any errors - wsClient2.on('error', function (err) { - return done(err); - }); - }) - - it('recieved an echo test to sender', function(done) { - - var onMessge = function(data) { - var response = JSON.parse(data); - wsClient.removeListener( 'message', onMessge ); - wsClient2.removeListener( 'message', onMessge ); - - test.string(response.message).is("Echo worked!") - test.number(response.eventType).is(10) - done(); - } - - wsClient.on( 'message', onMessge ); - wsClient2.on( 'message', onMessge ); - - wsClient.send( JSON.stringify({ eventType: 10, message : "Echo worked!" }), function(err) { - wsClient.removeListener( 'message', onMessge ); - wsClient2.removeListener( 'message', onMessge ); - done(err); - }); - }); - - it('recieved a broadcast echo test', function(done) { - - var echoCount = 0; - - var onMessge = function(data) { - var response = JSON.parse(data); - wsClient.removeListener( 'message', onMessge ); - wsClient2.removeListener( 'message', onMessge ); - echoCount++; - - test.string(response.message).is("Echo worked!") - test.number(response.eventType).is(10) - if (echoCount == 2) - done(); - } - - wsClient.on( 'message', onMessge ); - wsClient2.on( 'message', onMessge ); - - wsClient.send( JSON.stringify({ eventType: 10, message : "Echo worked!", broadcast: true }), function(err) { - wsClient.removeListener( 'message', onMessge ); - wsClient2.removeListener( 'message', onMessge ); - done(err); - }); - }); }) @@ -1158,61 +1095,57 @@ describe('Testing WS API calls', function(){ } wsClient.on( 'message', onMessge ); - wsClient.send( JSON.stringify({ eventType: 9, val : { sister : "sam", brother: "mat" }, username : "george3" })); + wsClient.send( JSON.stringify({ type: "MetaRequest", val : { sister : "sam", brother: "mat" }, username : "george3" })); }); it('Can set meta data for user george', function(done) { var onMessge = function(data) { var response = JSON.parse(data); wsClient.removeListener( 'message', onMessge ); - test.value(response.error).isUndefined() test.string(response.val.sister).is("sam") test.string(response.val.brother).is("mat") done(); } wsClient.on( 'message', onMessge ); - wsClient.send( JSON.stringify({ eventType: 9, val : { sister : "sam", brother: "mat" }, username : "george" })); + wsClient.send( JSON.stringify({ type: "MetaRequest", val : { sister : "sam", brother: "mat" }, username : "george" })); }); it('Can get meta data for user george', function(done) { var onMessge = function(data) { var response = JSON.parse(data); wsClient.removeListener( 'message', onMessge ); - test.value(response.error).isUndefined() test.string(response.val.sister).is("sam") test.string(response.val.brother).is("mat") done(); } wsClient.on( 'message', onMessge ); - wsClient.send( JSON.stringify({ eventType: 9, username : "george" })); + wsClient.send( JSON.stringify({ type: "MetaRequest", username : "george" })); }); it('Can set the meta property "brother" for user george', function(done) { var onMessge = function(data) { var response = JSON.parse(data); wsClient.removeListener( 'message', onMessge ); - test.value(response.error).isUndefined() test.string(response.val).is("George's brother") done(); } wsClient.on( 'message', onMessge ); - wsClient.send( JSON.stringify({ eventType: 9, property: "brother", val : "George's brother", username : "george" })); + wsClient.send( JSON.stringify({ type: "MetaRequest", property: "brother", val : "George's brother", username : "george" })); }); it('Can get the meta property "brother" for user george', function(done) { var onMessge = function(data) { var response = JSON.parse(data); wsClient.removeListener( 'message', onMessge ); - test.value(response.error).isUndefined() test.string(response.val).is("George's brother") done(); } wsClient.on( 'message', onMessge ); - wsClient.send( JSON.stringify({ eventType: 9, property: "brother", username : "george" })); + wsClient.send( JSON.stringify({ type: "MetaRequest", property: "brother", username : "george" })); }); }) @@ -2062,10 +1995,8 @@ describe('Cleaning up socket', function(){ if ( wsClient ) { - wsClient2.removeListener( 'message', onSocketMessage ); wsClient.removeListener( 'message', onSocketMessage ); wsClient.close(); - wsClient2.close(); wsClient = null; wsClient2 = null; } diff --git a/tsconfig.json b/tsconfig.json index a5d9697..424801b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,8 @@ "src/mailers/mailgun.ts", "src/permission-controller.ts", "src/session.ts", - "src/socket-api/client-event.ts", + "src/socket-api/client-instruction.ts", + "src/socket-api/server-instruction.ts", "src/socket-api/socket-event-types.ts", "src/socket-api/client-connection.ts", "src/socket-api/comms-controller.ts",