happn-3
Version:
pub/sub api as a service using primus and mongo & redis or nedb, can work as cluster, single process or embedded using nedb
405 lines (346 loc) • 13.7 kB
JavaScript
var util = require('util'),
EventEmitter = require('events').EventEmitter,
async = require('async'),
CONSTANTS = require('../..').constants;
module.exports = ProtocolService;
function ProtocolService(opts) {
this.log = opts.logger.createLogger('Protocol');
this.log.$$TRACE('construct(%j)', opts);
}
// Enable subscription to key lifecycle events
util.inherits(ProtocolService, EventEmitter);
ProtocolService.prototype.initialize = initialize;
ProtocolService.prototype.stats = stats;
ProtocolService.prototype.processSystem = processSystem;
ProtocolService.prototype.processInboundStack = processInboundStack;
ProtocolService.prototype.processLayers = processLayers;
ProtocolService.prototype.processMessageIn = processMessageIn;
ProtocolService.prototype.processMessageOut = processMessageOut;
ProtocolService.prototype.processMessageInLayers = processMessageInLayers;
ProtocolService.prototype.processMessageOutLayers = processMessageOutLayers;
ProtocolService.prototype.processSystemOut = processSystemOut;
ProtocolService.prototype.__cleanseRequestForLogs = __cleanseRequestForLogs;
ProtocolService.prototype.__handleProtocolError = __handleProtocolError;
ProtocolService.prototype.__getProtocol = __getProtocol;
ProtocolService.prototype.__disconnectUnauthorizedSession = __disconnectUnauthorizedSession;
function stats() {
return {
protocols: Object.keys(this.config.protocols)
};
}
//takes inbound system requests and transforms them into ones the client understands
function processSystem(message, respond) {
var protocol = this.__getProtocol(message);
if (!protocol)
return respond(
this.happn.services.error.SystemError(
'unknown system protocol: ' + message.session.protocol,
'protocol'
)
);
respond(null, protocol.transformSystem(message));
}
//takes outbound system requests and transforms them into ones the client understands
function processSystemOut(message, callback) {
try {
var protocol = this.__getProtocol(message);
if (!protocol)
return callback(
this.happn.services.error.SystemError(
'unknown system protocol: ' + message.session.protocol,
'protocol'
)
);
var systemEvent = protocol.transformSystem(message);
if (systemEvent.__suppress) return callback();
protocol.emit(systemEvent, message.session);
callback();
} catch (e) {
this.happn.services.error.handleSystem(e, 'ProtocolService', CONSTANTS.ERROR_SEVERITY.MEDIUM);
}
}
function processInboundStack(message, protocol, callback) {
var transformed;
try {
transformed = protocol.transformIn(message);
if (transformed.__suppress) return callback(null, message);
} catch (e) {
return callback(e);
}
this.happn.services.security.processAuthorize(transformed, (e, authorized) => {
if (e) return callback(e);
if (authorized.request.action === 'set') {
return this.happn.services.data.processStore(authorized, (e, publication) => {
if (e) return callback(e);
if (publication.request.options && publication.request.options.noPublish)
return callback(null, publication);
this.happn.services.publisher.processPublish(publication, function(e, result) {
if (e) return callback(e);
callback(null, result);
});
});
}
if (authorized.request.action === 'remove')
return this.happn.services.data.processRemove(authorized, (e, publication) => {
if (e) return callback(e);
if (publication.request.options && publication.request.options.noPublish)
return callback(null, publication);
this.happn.services.publisher.processPublish(publication, (e, result) => {
if (e) return callback(e);
callback(null, result);
});
});
if (authorized.request.action === 'get') {
return this.happn.services.data.processGet(authorized, (e, result) => {
if (e) return callback(e);
callback(null, result);
});
}
if (authorized.request.action === 'count') {
return this.happn.services.data.processCount(authorized, (e, result) => {
if (e) return callback(e);
callback(null, result);
});
}
if (authorized.request.action === 'on') {
return this.happn.services.subscription.processSubscribe(
this.happn.services.subscription.prepareSubscribeMessage(authorized),
function(e, result) {
if (e) return callback(e);
callback(null, result);
}
);
}
if (authorized.request.action === 'off')
return this.happn.services.subscription.processUnsubscribe(
this.happn.services.subscription.prepareSubscribeMessage(authorized),
function(e, result) {
if (e) return callback(e);
callback(null, result);
}
);
if (authorized.request.action === 'request-nonce')
return this.happn.services.security.processNonceRequest(authorized, function(e, result) {
if (e) return callback(e);
callback(null, result);
});
if (authorized.request.action === 'describe')
return callback(null, this.happn.services.system.processMessage(authorized));
if (authorized.request.action === 'configure-session') {
this.happn.services.session.processConfigureSession(authorized);
return callback(null, authorized);
}
if (authorized.request.action === 'login') {
if (this.config.secure)
return this.happn.services.security.processLogin(authorized, (e, result) => {
if (e) return callback(e);
callback(null, result);
});
return this.happn.services.security.processUnsecureLogin(authorized, (e, result) => {
if (e) return callback(e);
callback(null, result);
});
}
if (authorized.request.action === 'ack')
return this.happn.services.publisher.processAcknowledge(authorized, (e, result) => {
if (e) return callback(e);
callback(null, result);
});
if (authorized.request.action === 'revoke-token')
return this.happn.services.session.processRevokeSessionToken(authorized, 'CLIENT', function(
e,
result
) {
callback(e, result);
});
if (authorized.request.action === 'disconnect-child-sessions')
return this.happn.services.session.disconnectSessions(
authorized.session.id,
{
reason: CONSTANTS.SECURITY_DIRECTORY_EVENTS.TOKEN_REVOKED
},
e => {
callback(e, authorized);
},
false
);
});
}
function __cleanseRequestForLogs(request, session) {
const cleansed = {};
if (session && session.user) cleansed.username = session.user.username;
if (request) {
if (request.action) cleansed.action = request.action;
if (request.path) cleansed.path = request.path;
if (request.sessionId) cleansed.sessionId = request.sessionId;
}
return JSON.stringify(cleansed);
}
function __disconnectUnauthorizedSession(message, errorMessage, cleansedRequest) {
const logMessage = `AccessDenied: ${errorMessage}, request: ${cleansedRequest}`;
let reason;
switch (errorMessage) {
case CONSTANTS.UNAUTHORISED_REASONS.EXPIRED_TOKEN:
reason = 'token-expired';
break;
case CONSTANTS.UNAUTHORISED_REASONS.INACTIVITY_THRESHOLD_REACHED:
reason = 'inactivity-threshold';
break;
case CONSTANTS.UNAUTHORISED_REASONS.SESSION_USAGE:
reason = 'usage-limit';
break;
case CONSTANTS.UNAUTHORISED_REASONS.NO_POLICY_SESSION:
reason = 'no-session-policy';
break;
case CONSTANTS.UNAUTHORISED_REASONS.NO_POLICY_SESSION_TYPE:
reason = 'no-session-type-policy';
break;
default:
reason = 'unauthorized-request';
}
this.happn.services.session.disconnectSession(
message.session.id,
e => {
if (e)
return this.happn.services.log.warn(
`unable to disconnect session due to ${errorMessage}: ${e.message}`,
'',
'SessionService'
);
},
{ reason }
);
return this.happn.services.log.warn(logMessage, '', 'SecurityService');
}
function __handleProtocolError(protocol, message, e, callback) {
message.error = e.cause ? e.cause : e;
callback(null, protocol.fail(message));
let logMessage;
const cleansedRequest = this.__cleanseRequestForLogs(message.request, message.session);
const errorMessage = e.toString();
if (CONSTANTS.UNAUTHORISED_REASONS_COLLECTION.indexOf(e.message) > -1)
return this.__disconnectUnauthorizedSession(message, e.message, cleansedRequest);
//access denieds are not system errors, and should be logged as warnings
if (errorMessage === 'AccessDenied: unauthorized' && message.request && message.request.path) {
logMessage = 'AccessDenied: unauthorized, path: ' + message.request.path;
logMessage += ', action: ' + message.request.action;
if (message.session && message.session.user && message.session.user.username)
logMessage += ', username: ' + message.session.user.username;
if (e.reason) {
logMessage += ', reason: ' + e.reason;
}
return this.happn.services.log.warn(logMessage, '', 'SecurityService');
}
if (errorMessage.indexOf('AccessDenied:') === 0) {
logMessage = `${errorMessage}, request: ${cleansedRequest}`;
return this.happn.services.log.warn(logMessage, '', 'SecurityService');
}
if (errorMessage.indexOf('InvalidCredentials:') === 0) {
logMessage = `${errorMessage}, request: ${cleansedRequest}`;
return this.happn.services.log.warn(logMessage, '', 'SecurityService');
}
this.happn.services.error.handleSystem(e, 'ProtocolService', CONSTANTS.ERROR_SEVERITY.MEDIUM);
}
function __getProtocol(message) {
var protocol = this.config.protocols[message.session.protocol];
if (!protocol) {
//backward compatibility happn_1.3.0
if (message.session.protocol.indexOf('happn') === 0) protocol = this.config.protocols.happn_1;
}
return protocol;
}
function processMessageIn(message, callback) {
var protocol = this.__getProtocol(message);
if (!protocol)
return callback(
this.happn.services.error.SystemError(
'unknown inbound protocol: ' + message.session.protocol,
'protocol'
)
);
this.processInboundStack(message, protocol, (e, processed) => {
if (e) return this.__handleProtocolError(protocol, message, e, callback);
callback(null, protocol.success(processed));
});
}
function processLayers(message, layers, callback) {
if (layers == null || layers.length === 0) return callback(null, message);
async.waterfall(
[
function(layerCallback) {
layerCallback(null, message);
}
].concat(layers),
callback
);
}
function processMessageInLayers(message, callback) {
var protocol = this.__getProtocol(message);
if (!protocol)
return callback(
this.happn.services.error.SystemError(
'unknown inbound protocol: ' + message.session.protocol,
'protocol'
)
);
this.processLayers(message, this.config.inboundLayers, (e, inboundTransformed) => {
if (e) return this.__handleProtocolError(protocol, message, e, callback);
this.processInboundStack(inboundTransformed, protocol, (e, processed) => {
if (e) return this.__handleProtocolError(protocol, message, e, callback);
this.processLayers(processed, this.config.outboundLayers, (e, outboundTransformed) => {
if (e) return this.__handleProtocolError(protocol, message, e, callback);
callback(null, protocol.success(outboundTransformed));
});
});
});
}
function processMessageOutLayers(message, callback) {
var protocol = this.__getProtocol(message);
//dont need to check if the protocol exists because it always should
this.processLayers(
protocol.transformOut(message),
this.config.outboundLayers,
(e, transformedPublication) => {
if (e) return this.__handleProtocolError(protocol, message, e, callback);
try {
protocol.emit(transformedPublication, message.session);
callback(null, transformedPublication);
} catch (protocolError) {
this.__handleProtocolError(protocol, message, protocolError, callback);
}
}
);
}
function processMessageOut(message, callback) {
try {
var protocol = this.config.protocols[message.session.protocol];
var publication = protocol.transformOut(message);
protocol.emit(publication, message.session);
callback(null, publication);
} catch (e) {
callback(e);
}
}
function initialize(config, callback) {
if (!config) config = {};
if (!config.protocols) config.protocols = {};
var packageProtocol = require('../../../package.json').protocol;
this.__latest = require('./happn_' + packageProtocol).create();
this.__earliest = require('./happn_1').create();
config.protocols['happn_' + packageProtocol] = this.__latest;
//the earliest clients have no protocol version, so we use the earliest by default
config.protocols.happn = this.__earliest;
for (var i = 1; i < packageProtocol; i++)
if (!config.protocols['happn_' + i])
config.protocols['happn_' + i] = require('./happn_' + i).create();
for (var protocolKey in config.protocols) {
var protocol = config.protocols[protocolKey];
protocol.happn = this.happn;
protocol.protocolVersion = protocolKey;
protocol.initialize();
}
this.config = config;
if (this.config.outboundLayers) this.processMessageOut = this.processMessageOutLayers;
if (this.config.inboundLayers) this.processMessageIn = this.processMessageInLayers;
return callback();
}