blob: 08bf38d93d7d4ad99c99bf6becec8c142a06fc37 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/**
* @fileoveriew A router that handles incoming server rpcs.
* @private
*/
var Promise = require('../lib/promise');
var Stream = require('../proxy/stream');
var MessageType = require('../proxy/message-type');
var Incoming = MessageType.Incoming;
var Outgoing = MessageType.Outgoing;
var ErrorConversion = require('../vdl/error-conversion');
var vlog = require('./../lib/vlog');
var StreamHandler = require('../proxy/stream-handler');
var verror = require('../gen-vdl/v.io/v23/verror');
var createSecurityCall = require('../security/create-security-call');
var createServerCall = require('./create-server-call');
var vdl = require('../vdl');
var typeUtil = require('../vdl/type-util');
var Deferred = require('../lib/deferred');
var capitalize = require('../vdl/util').capitalize;
var namespaceUtil = require('../naming/util');
var naming = require('../gen-vdl/v.io/v23/naming');
var Glob = require('./glob');
var GlobStream = require('./glob-stream');
var ServerRpcReply =
require('../gen-vdl/v.io/x/ref/services/wspr/internal/lib').ServerRpcReply;
var serverVdl =
require('../gen-vdl/v.io/x/ref/services/wspr/internal/rpc/server');
var CaveatValidationResponse = serverVdl.CaveatValidationResponse;
var AuthReply = serverVdl.AuthReply;
var LookupReply = serverVdl.LookupReply;
var vtrace = require('../vtrace');
var lib =
require('../gen-vdl/v.io/x/ref/services/wspr/internal/lib');
var Blessings = require('../security/blessings');
var BlessingsId =
require('../gen-vdl/v.io/x/ref/services/wspr/internal/principal').BlessingsId;
var WireBlessings =
require('../gen-vdl/v.io/v23/security').WireBlessings;
var SharedContextKeys = require('../runtime/shared-context-keys');
var hexVom = require('../lib/hex-vom');
var vom = require('../vom');
var byteUtil = require('../vdl/byte-util');
var StreamCloseHandler = require('./stream-close-handler');
/**
* A router that handles routing incoming requests to the right
* server
* @constructor
* @private
*/
var Router = function(
proxy, appName, rootCtx, controller, caveatRegistry, blessingsCache) {
this._servers = {};
this._proxy = proxy;
this._streamMap = {};
this._contextMap = {};
this._appName = appName;
this._rootCtx = rootCtx;
this._caveatRegistry = caveatRegistry;
this._outstandingRequestForId = {};
this._controller = controller;
this._blessingsCache = blessingsCache;
this._typeEncoder = proxy.typeEncoder;
this._typeDecoder = proxy.typeDecoder;
proxy.addIncomingHandler(Incoming.INVOKE_REQUEST, this);
proxy.addIncomingHandler(Incoming.LOOKUP_REQUEST, this);
proxy.addIncomingHandler(Incoming.AUTHORIZATION_REQUEST, this);
proxy.addIncomingHandler(Incoming.CAVEAT_VALIDATION_REQUEST, this);
proxy.addIncomingHandler(Incoming.LOG_MESSAGE, this);
};
Router.prototype.handleRequest = function(messageId, type, request) {
switch (type) {
case Incoming.INVOKE_REQUEST:
return this.handleRPCRequest(messageId, request);
case Incoming.LOOKUP_REQUEST:
this.handleLookupRequest(messageId, request);
break;
case Incoming.AUTHORIZATION_REQUEST:
this.handleAuthorizationRequest(messageId, request);
break;
case Incoming.CAVEAT_VALIDATION_REQUEST:
this.handleCaveatValidationRequest(messageId, request);
break;
case Incoming.LOG_MESSAGE:
if (request.level === typeUtil.unwrap(lib.LogLevel.INFO)) {
vlog.logger.info(request.message);
} else if (request.level === typeUtil.unwrap(lib.LogLevel.ERROR)) {
vlog.logger.error(request.message);
} else {
vlog.logger.error('unknown log level ' + request.level);
}
break;
default:
vlog.logger.error('Unknown request type ' + type);
}
};
Router.prototype.handleAuthorizationRequest = function(messageId, request) {
try {
request = byteUtil.hex2Bytes(request);
} catch (e) {
var authReply = new AuthReply({
// TODO(bjornick): Use the real context
err: new verror.InternalError(this._rootCtx, 'Failed to decode ', e)
});
this._proxy.sendRequest(hexVom.encode(authReply, undefined,
this._typeEncoder),
Outgoing.AUTHORIZATION_RESPONSE, null, messageId);
return;
}
var router = this;
var decodedRequest;
vom.decode(request, false, this._typeDecoder).catch(function(e) {
return Promise.reject(new verror.InternalError(router._rootCtx,
'Failed to decode ', e));
}).then(function(req) {
decodedRequest = req;
var ctx = router._rootCtx.withValue(SharedContextKeys.LANG_KEY,
decodedRequest.context.language);
var server = router._servers[decodedRequest.serverId];
if (!server) {
var authReply = new AuthReply({
// TODO(bjornick): Use the real context
err: new verror.ExistsError(ctx, 'unknown server')
});
var bytes = hexVom.encode(authReply, undefined, router._typeEncoder);
router._proxy.sendRequest(bytes,
Outgoing.AUTHORIZATION_RESPONSE,
null, messageId);
return;
}
return createSecurityCall(decodedRequest.call, router._blessingsCache)
.then(function(call) {
return server._handleAuthorization(decodedRequest.handle, ctx, call);
});
}).then(function() {
var authReply = new AuthReply({});
router._proxy.sendRequest(hexVom.encode(authReply, undefined,
router._typeEncoder),
Outgoing.AUTHORIZATION_RESPONSE, null, messageId);
}).catch(function(e) {
var authReply = new AuthReply({
err: ErrorConversion.fromNativeValue(e, router._appName,
decodedRequest.call.method)
});
router._proxy.sendRequest(hexVom.encode(authReply, undefined,
router._typeEncoder),
Outgoing.AUTHORIZATION_RESPONSE, null,
messageId);
});
};
Router.prototype._validateChain = function(ctx, call, cavs) {
var router = this;
var promises = cavs.map(function(cav) {
var def = new Deferred();
router._caveatRegistry.validate(ctx, call, cav, function(err) {
if (err) {
return def.reject(err);
}
return def.resolve();
});
return def.promise;
});
return Promise.all(promises).then(function(results) {
return undefined;
}).catch(function(err) {
if (!(err instanceof Error)) {
err = new Error(
'Non-error value returned from caveat validator: ' +
err);
}
return ErrorConversion.fromNativeValue(err, router._appName,
'caveat validation');
});
};
Router.prototype.handleCaveatValidationRequest = function(messageId, request) {
var router = this;
createSecurityCall(request.call, this._blessingsCache)
.then(function(call) {
var ctx = router._rootCtx.withValue(SharedContextKeys.LANG_KEY,
request.context.language);
var resultPromises = request.cavs.map(function(cav) {
return router._validateChain(ctx, call, cav);
});
return Promise.all(resultPromises).then(function(results) {
var response = new CaveatValidationResponse({
results: results
});
var data = hexVom.encode(response, undefined, router._typeEncoder);
router._proxy.sendRequest(data, Outgoing.CAVEAT_VALIDATION_RESPONSE,
null, messageId);
});
}).catch(function(err) {
vlog.logger.error('Got err ' + err + ': ' + err.stack);
throw new Error('Unexpected error (all promises should resolve): ' + err);
});
};
Router.prototype.handleLookupRequest = function(messageId, request) {
var server = this._servers[request.serverId];
if (!server) {
// TODO(bjornick): Pass in context here so we can generate useful error
// messages.
var reply = new LookupReply({
err: new verror.NoExistError(this._rootCtx, 'unknown server')
});
this._proxy.sendRequest(hexVom.encode(reply, undefined, this._typeEncoder),
Outgoing.LOOKUP_RESPONSE,
null, messageId);
return;
}
var self = this;
return server._handleLookup(request.suffix).then(function(value) {
var signatureList = value.invoker.signature();
var hasAuthorizer = (typeof value.authorizer === 'function');
var hasGlobber = value.invoker.hasGlobber();
var reply = {
handle: value._handle,
signature: signatureList,
hasAuthorizer: hasAuthorizer,
hasGlobber: hasGlobber
};
self._proxy.sendRequest(hexVom.encode(reply, LookupReply.prototype._type,
self._typeEncoder),
Outgoing.LOOKUP_RESPONSE,
null, messageId);
}).catch(function(err) {
var reply = new LookupReply({
err: ErrorConversion.fromNativeValue(err, self._appName, '__Signature')
});
self._proxy.sendRequest(hexVom.encode(reply, undefined, self._typeEncoder),
Outgoing.LOOKUP_RESPONSE,
null, messageId);
});
};
Router.prototype.createRPCContext = function(request) {
var ctx = this._rootCtx;
// Setup the context passed in the context info passed in from wspr.
if (!request.call.deadline.noDeadline) {
var fromNow = request.call.deadline.fromNow;
var timeout = fromNow.seconds * 1000;
timeout += fromNow.nanos / 1000000;
ctx = ctx.withTimeout(timeout);
} else {
ctx = ctx.withCancel();
}
ctx = ctx.withValue(SharedContextKeys.LANG_KEY,
request.call.context.language);
// Plumb through the vtrace ids
var suffix = request.call.securityCall.suffix;
var spanName = '<jsserver>"' + suffix + '".' + request.method;
// TODO(mattr): We need to enforce some security on trace responses.
return vtrace.withContinuedTrace(ctx, spanName,
request.call.traceRequest);
};
function getMethodSignature(invoker, methodName) {
var methodSig;
// Find the method signature.
var signature = invoker.signature();
signature.forEach(function(ifaceSig) {
ifaceSig.methods.forEach(function(method) {
if (method.name === methodName) {
methodSig = method;
}
});
});
return methodSig;
}
Router.prototype._setupStream = function(messageId, ctx, methodSig) {
this._contextMap[messageId] = ctx;
if (methodIsStreaming(methodSig)) {
var readType = (methodSig.inStream ? methodSig.inStream.type : null);
var writeType = (methodSig.outStream ? methodSig.outStream.type : null);
var stream = new Stream(messageId, this._proxy.senderPromise, false,
readType, writeType, this._typeEncoder);
this._streamMap[messageId] = stream;
var rpc = new StreamHandler(ctx, stream, this._typeDecoder);
this._proxy.addIncomingStreamHandler(messageId, rpc);
} else {
this._proxy.addIncomingStreamHandler(messageId,
new StreamCloseHandler(ctx));
}
};
var globSig = {
inArgs: [],
outArgs: [],
outStream: {
type: naming.GlobReply.prototype._type
}
};
/**
* Handles the processing for reserved methods. If this request is not
* a reserved method call, this method does nothing.
*
* @private
* @param {module:vanadium.context.Context} ctx The context of the request
* @param {number} messageId The flow id
* @param {module:vanadium.rpc~Server} server The server instance that is
* handling the request.
* @param {Invoker} invoker The invoker for this request
* @param {string} methodName The name of the method.
* @param {object} request The request
* @returns Promise A promise that will be resolved when the method is
* dispatched or null if this is not a reserved method
*/
Router.prototype._maybeHandleReservedMethod = function(
ctx, messageId, server, invoker, methodName, request) {
var self = this;
function globCompletion() {
// There are no results to a glob method. Everything is sent back
// through the stream.
self.sendResult(messageId, methodName, null, undefined, 1);
}
if (request.method === 'Glob__') {
if (!invoker.hasGlobber()) {
var err = new Error('Glob is not implemented');
this.sendResult(messageId, 'Glob__', null, err);
return;
}
this._setupStream(messageId, ctx, globSig);
this._outstandingRequestForId[messageId] = 0;
this.incrementOutstandingRequestForId(messageId);
var globPattern = typeUtil.unwrap(request.args[0]);
return createServerCall(request, this._blessingsCache)
.then(function(call) {
self.handleGlobRequest(messageId, call.securityCall.suffix,
server, new Glob(globPattern), ctx, call, invoker,
globCompletion);
});
}
return null;
};
Router.prototype._unwrapArgs = function(args, methodSig) {
var self = this;
// Unwrap the RPC arguments sent to the JS server.
var unwrappedArgPromises = args.map(function(arg, i) {
// If an any type was expected, unwrapping is not needed.
if (methodSig.inArgs[i].type.kind === vdl.kind.ANY) {
return Promise.resolve(arg);
}
var unwrapped = typeUtil.unwrap(arg);
if (unwrapped instanceof BlessingsId) {
return self._blessingsCache.blessingsFromId(unwrapped);
}
return Promise.resolve(unwrapped);
});
return Promise.all(unwrappedArgPromises);
};
/**
* Performs the rpc request. Unlike handleRPCRequest, this function works on
* the decoded message.
* @private
* @param {number} messageId Message Id set by the server.
* @param {Object} request Request's structure is
* {
* serverId: number // the server id
* method: string // Name of the method on the service to call
* args: [] // Array of positional arguments to be passed into the method
* // Note: This array contains wrapped arguments!
* }
*/
Router.prototype._handleRPCRequestInternal = function(messageId, request) {
var methodName = capitalize(request.method);
var server = this._servers[request.serverId];
var err;
if (!server) {
// TODO(bprosnitz) What error type should this be.
err = new Error('Request for unknown server ' + request.serverId);
this.sendResult(messageId, methodName, null, err);
return;
}
var invoker = server._getInvokerForHandle(request.handle);
if (!invoker) {
vlog.logger.error('No invoker found: ', request);
err = new Error('No service found');
this.sendResult(messageId, methodName, null, err);
return;
}
var ctx = this.createRPCContext(request);
var reservedPromise = this._maybeHandleReservedMethod(
ctx, messageId, server, invoker, methodName, request);
if (reservedPromise) {
return;
}
var self = this;
var methodSig = getMethodSignature(invoker, methodName);
if (methodSig === undefined) {
err = new verror.NoExistError(
ctx, 'Requested method', methodName, 'not found on');
this.sendResult(messageId, methodName, null, err);
return;
}
this._setupStream(messageId, ctx, methodSig);
var args;
this._unwrapArgs(request.args, methodSig).then(function(unwrapped) {
args = unwrapped;
return createServerCall(request, self._blessingsCache);
}).then(function(call) {
var options = {
methodName: methodName,
args: args,
methodSig: methodSig,
ctx: ctx,
call: call,
stream: self._streamMap[messageId],
};
// Invoke the method;
self.invokeMethod(invoker, options).then(function(results) {
// Has results; associate the types of the outArgs.
var canonResults = results.map(function(result, i) {
var t = methodSig.outArgs[i].type;
if (t.equals(WireBlessings.prototype._type)) {
if (!(result instanceof Blessings)) {
vlog.logger.error(
'Encoding non-blessings value as wire blessings');
return null;
}
return result;
}
return vdl.canonicalize.fill(result, t);
});
self.sendResult(messageId, methodName, canonResults, undefined,
methodSig.outArgs.length);
}, function(err) {
var stackTrace;
if (err instanceof Error && err.stack !== undefined) {
stackTrace = err.stack;
}
vlog.logger.error('Requested method ' + methodName +
' threw an exception on invoke: ', err, stackTrace);
// The error case has no results; only send the error.
self.sendResult(messageId, methodName, undefined, err,
methodSig.outArgs.length);
});
});
};
/**
* Handles incoming requests from the server to invoke methods on registered
* services in JavaScript.
* @private
* @param {string} messageId Message Id set by the server.
* @param {string} vdlRequest VOM encoded request. Request's structure is
* {
* serverId: number // the server id
* method: string // Name of the method on the service to call
* args: [] // Array of positional arguments to be passed into the method
* // Note: This array contains wrapped arguments!
* }
*/
Router.prototype.handleRPCRequest = function(messageId, vdlRequest) {
var err;
var request;
var router = this;
try {
request = byteUtil.hex2Bytes(vdlRequest);
} catch (e) {
err = new Error('Failed to decode args: ' + e);
this.sendResult(messageId, '', null, err);
return;
}
return vom.decode(request, false, this._typeDecoder)
.then(function(request) {
return router._handleRPCRequestInternal(messageId, request);
}, function(e) {
vlog.logger.error('Failed to decode args : ' + e + ': ' + e.stack);
err = new Error('Failed to decode args: ' + e);
router.sendResult(messageId, '', null, err);
});
};
function methodIsStreaming(methodSig) {
return (typeof methodSig.inStream === 'object' &&
methodSig.inStream !== null) || (typeof methodSig.outStream === 'object' &&
methodSig.outStream !== null);
}
/**
* Invokes a method with a methodSig
*/
Router.prototype.invokeMethod = function(invoker, options) {
var methodName = options.methodName;
var args = options.args;
var ctx = options.ctx;
var call = options.call;
var injections = {
context: ctx,
call: call,
stream: options.stream
};
var def = new Deferred();
function InvocationFinishedCallback(err, results) {
if (err) {
return def.reject(err);
}
def.resolve(results);
}
invoker.invoke(methodName, args, injections, InvocationFinishedCallback);
return def.promise;
};
function createGlobReply(name) {
name = name || '';
return new naming.GlobReply({
'entry': new naming.MountEntry({
name: name
})
});
}
function createGlobErrorReply(name, err, appName) {
name = name || '';
var convertedError = ErrorConversion.fromNativeValue(err, appName, 'glob');
return new naming.GlobReply({
'error': new naming.GlobError({
name: name,
error: convertedError
})
});
}
Router.prototype.handleGlobRequest = function(messageId, name, server, glob,
context, call, invoker, cb) {
var self = this;
var options;
function invokeAndCleanup(invoker, options, method) {
self.invokeMethod(invoker, options).catch(function(err) {
var verr = new verror.InternalError(context,
method + '() failed', glob, err);
var errReply = createGlobErrorReply(name, verr, self._appName);
self._streamMap[messageId].write(errReply);
vlog.logger.info(verr);
}).then(function() {
// Always decrement the outstanding request counter.
self.decrementOutstandingRequestForId(messageId, cb);
});
}
if (invoker.hasMethod('__glob')) {
options = {
methodName: '__glob',
args: [glob.toString()],
methodSig: {
outArgs: []
},
ctx: context,
call: call,
// For the __glob method we just write the
// results directly out to the rpc stream.
stream: this._streamMap[messageId]
};
invokeAndCleanup(invoker, options, '__glob');
} else if (invoker.hasMethod('__globChildren')) {
if (glob.length() === 0) {
// This means we match the current object.
this._streamMap[messageId].write(createGlobReply(name));
}
if (glob.finished()) {
this.decrementOutstandingRequestForId(messageId, cb);
return;
}
// Create a GlobStream
var globStream = new GlobStream();
options = {
methodName: '__globChildren',
args: [],
methodSig: {
outArgs: []
},
ctx: context,
call: call,
stream: globStream
};
globStream.on('data', function(child) {
// TODO(bjornick): Allow for escaped slashes.
if (child.indexOf('/') !== -1) {
var verr = new verror.InternalError(context,
'__globChildren returned a bad child', child);
var errReply = createGlobErrorReply(name, verr, self._appName);
self._streamMap[messageId].write(errReply);
vlog.logger.info(verr);
return;
}
var suffix = namespaceUtil.join(name, child);
self.incrementOutstandingRequestForId(messageId);
var nextInvoker;
var subCall;
createServerCall(call, this._blessingsCache).then(function(servCall) {
subCall = servCall;
subCall.securityCall.suffix = suffix;
return server._handleLookup(suffix);
}).then(function(value) {
nextInvoker = value.invoker;
return server._handleAuthorization(value._handle, context,
subCall.securityCall);
}).then(function() {
var match = glob.matchInitialSegment(child);
if (match.match) {
self.handleGlobRequest(messageId, suffix, server, match.remainder,
context, subCall, nextInvoker, cb);
} else {
self.decrementOutstandingRequestForId(messageId, cb);
}
}).catch(function(e) {
var verr = new verror.NoServersError(context, suffix, e);
var errReply = createGlobErrorReply(suffix, verr, self._appName);
self._streamMap[messageId].write(errReply);
vlog.logger.info(errReply);
self.decrementOutstandingRequestForId(messageId, cb);
});
});
invokeAndCleanup(invoker, options, '__globChildren');
} else {
// This is a leaf of the globChildren call so we return this as
// a result.
this._streamMap[messageId].write(createGlobReply(name));
this.decrementOutstandingRequestForId(messageId, cb);
}
};
Router.prototype.incrementOutstandingRequestForId = function(id) {
this._outstandingRequestForId[id]++;
};
Router.prototype.decrementOutstandingRequestForId = function(id, cb) {
this._outstandingRequestForId[id]--;
if (this._outstandingRequestForId[id] === 0) {
cb();
delete this._outstandingRequestForId[id];
}
};
/**
* Sends the result of a requested invocation back to jspr
* @private
* @param {number} messageId Message id of the original invocation request
* @param {string} name Name of method
* @param {Object} results Result of the call
* @param {Error} err Error from the call
*/
Router.prototype.sendResult = function(messageId, name, results, err,
numOutArgs) {
if (!results) {
results = new Array(numOutArgs);
}
var errorStruct = null;
if (err !== undefined && err !== null) {
errorStruct = ErrorConversion.fromNativeValue(err, this._appName,
name);
}
// Clean up the context map.
var ctx = this._contextMap[messageId];
if (ctx) {
ctx.finish();
delete this._contextMap[messageId];
}
var traceResponse = vtrace.response(ctx);
// If this is a streaming request, queue up the final response after all
// the other stream requests are done.
var stream = this._streamMap[messageId];
if (stream && typeof stream.serverClose === 'function') {
// We should probably remove the stream from the dictionary, but it's
// not clear if there is still a reference being held elsewhere. If there
// isn't, then GC might prevent this final message from being sent out.
stream.serverClose(results, errorStruct, traceResponse);
this._proxy.dequeue(messageId);
} else {
var responseData = new ServerRpcReply({
results: results,
err: errorStruct,
traceResponse: traceResponse
});
this._proxy.sendRequest(hexVom.encode(responseData, undefined,
this._typeEncoder),
Outgoing.RESPONSE,
null, messageId);
}
};
/**
* Instructs WSPR to create a server and start listening for calls on
* behalf of the given JavaScript server.
* @private
* @param {string} name Name to serve under
* @param {Vanadium.Server} server The server who will handle the requests for
* this name.
* @param {function} [cb] If provided, the function will be called when
* serve completes. The first argument passed in is the error if there
* was any.
* @return {Promise} Promise to be called when serve completes or fails.
*/
Router.prototype.newServer = function(name, server, cb) {
vlog.logger.info('New server under the name: ', name);
this._servers[server.id] = server;
// If using a leaf dispatcher, set the IsLeaf ServerOption.
var isLeaf = server.dispatcher && server.dispatcher._isLeaf;
if (isLeaf) {
server.serverOption._opts.isLeaf = true;
}
var rpcOpts = server.serverOption._toRpcServerOption();
return this._controller.newServer(this._rootCtx, name, server.id,
rpcOpts, cb);
};
/**
* Sends an addName request to jspr.
* @private
* @param {string} name Name to publish
* @param {function} [cb] If provided, the function will be called on
* completion. The only argument is an error if there was one.
* @return {Promise} Promise to be called when operation completes or fails
*/
Router.prototype.addName = function(name, server, cb) {
return this._controller.addName(this._rootCtx, server.id, name, cb);
};
/**
* Sends an removeName request to jspr.
* @private
* @param {string} name Name to unpublish
* @param {function} [cb] If provided, the function will be called on
* completion. The only argument is an error if there was one.
* @return {Promise} Promise to be called when operation completes or fails
*/
Router.prototype.removeName = function(name, server, cb) {
// Delete our bind cache entry for that name
this._proxy.signatureCache.del(name);
return this._controller.removeName(this._rootCtx, server.id, name, cb);
};
/**
* Sends a stop server request to jspr.
* @private
* @param {Server} server Server object to stop.
* @param {function} [cb] If provided, the function will be called on
* completion. The only argument is an error if there was one.
* @return {Promise} Promise to be called when stop service completes or fails
*/
Router.prototype.stopServer = function(server, cb) {
var self = this;
return this._controller.stop(this._rootCtx, server.id)
.then(function() {
delete self._servers[server.id];
if (cb) {
cb(null);
}
}, function(err) {
if (cb) {
cb(err);
}
return Promise.reject(err);
});
};
/**
* Stops all servers managed by this router.
* @private
* @param {function} [cb] If provided, the function will be called on
* completion. The only argument is an error if there was one.
* @return {Promise} Promise to be called when all servers are stopped.
*/
Router.prototype.cleanup = function(cb) {
var promises = [];
var servers = this._servers;
for (var id in servers) {
if (servers.hasOwnProperty(id)) {
promises.push(this.stopServer(servers[id]));
}
}
return Promise.all(promises).then(function() {
if (cb) {
cb(null);
}
}, function(err) {
if (cb) {
cb(err);
}
});
};
module.exports = Router;