oncrpc
Version:
Library for creating ONC RPC Node.js servers and clients
367 lines (291 loc) • 9.34 kB
JavaScript
// Copyright 2013 Joyent, Inc. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
var domain = require('domain');
var net = require('net');
var util = require('util');
var assert = require('assert-plus');
var clone = require('clone');
var once = require('once');
var dtrace = require('./dtrace');
var errors = require('./errors');
var RpcCall = require('./call').RpcCall;
var RpcParser = require('./parser').RpcParser;
var RpcReply = require('./reply').RpcReply;
///--- Globals
var slice = Function.prototype.call.bind(Array.prototype.slice);
var sprintf = util.format;
///--- Private Methods
// "this" is bound to an RpcServer
function ifError(n) {
function _ifError(err) {
if (err) {
err._rpc_next = n;
throw err;
}
}
return (_ifError);
}
function onConnection(c) {
var log = this.log;
var parser = new RpcParser({
log: log
});
var self = this;
// Clients can have many requests running in parallel
c.setMaxListeners(256);
c.on('error', function (err) {
log.error(err, 'connection error occurred');
});
parser.on('message', function onRpcMessage(msg) {
assert.ok(msg, 'parsed rpc object is null');
var err;
function return_error() {
err.xid = msg.xid;
c.write(err.toBuffer());
}
log.trace({
rpc_call: msg
}, 'call received');
if (msg.rpcvers !== 2) {
err = new errors.RpcMismatchError();
return_error();
} else if (msg.prog !== self.program) {
err = new errors.RpcProgramUnavailableError();
return_error();
} else if (self.version.indexOf(msg.vers) === -1) {
err = new errors.RpcProgramMismatchError();
err.setVersion(self.version[self.version.length - 1]);
return_error();
} else if (!self.rpc_table[msg.proc]) {
if (msg.proc === 0) { // NULL RPC handler
msg.incoming = false;
var _null = new RpcReply(msg);
_null.pipe(c, {end: false});
_null.writeHead();
_null.end();
msg.incoming = true;
} else {
err = new errors.RpcProcedureUnavailableError();
return_error();
}
} else {
var cfg = self.rpc_table[msg.proc];
msg.incoming = false;
var reply = new cfg.reply(msg);
reply.pipe(c, {end: false});
msg.incoming = true;
var call;
if (cfg.call) {
call = new cfg.call(msg);
msg.pipe(call);
} else {
call = msg;
}
call.connection = c;
process.nextTick(function () {
self.emit(cfg.name, call, reply);
});
}
});
c.pipe(parser);
}
///--- API
// An RPC service is identified by its RPC program number, version
// number, and the transport address where it may be reached. The
// transport address, in turn, consists of a network address and a
// transport selector. In the case of a service available over TCP/IP
// or UDP/IP, the network address will be an IP address, and the
// transport selector will be a TCP or UDP port number.
function RpcServer(opts) {
assert.object(opts, 'options');
assert.object(opts.log, 'options.log');
assert.number(opts.program, 'options.program');
var v = opts.version;
if (typeof (v) === 'number')
v = [v];
assert.arrayOfNumber(v, 'options.version');
net.Server.call(this, opts);
this.log = opts.log.child({
component: 'RpcServer'
}, true);
this.name = opts.name || 'RpcServer';
this.program = opts.program;
this.rpc_table = {};
this.saved_handlers = [];
this.version = v.slice();
this.on('connection', onConnection.bind(this));
}
util.inherits(RpcServer, net.Server);
RpcServer.prototype.use = function use(chain) {
if (!Array.isArray(chain))
chain = [chain];
assert.arrayOfFunc(chain);
var self = this;
chain.forEach(function (c) {
self.saved_handlers.push(c);
Object.keys(self.rpc_table).forEach(function (k) {
self.rpc_table[k].push(c);
});
});
};
RpcServer.prototype._mount = function _mount(cfg, chain) {
assert.object(cfg, 'config');
if (Array.isArray(chain) && chain.length) {
if (Array.isArray(chain[0]))
chain = chain[0];
}
assert.arrayOfFunc(chain);
chain.unshift(cfg);
this._rpc.apply(this, chain);
};
/**
* Runs a series of handlers given an RPC procedure.
*
* var cfg = {
* procedure: 4, // What RPC procedure
* reply: PortmapDumpReply // What Type of Reply instance to create
* };
* server._rpc(cfg, function (req, res, next) {
* res.addMapping({...}); // procedure specific
* res.writeHead();
* res.end();
* next();
* });
*/
RpcServer.prototype._rpc = function _rpc(opts) {
assert.object(opts, 'options');
assert.string(opts.name, 'options.name');
assert.number(opts.procedure, 'options.procedure');
assert.optionalFunc(opts.call, 'options.call');
assert.func(opts.reply, 'options.reply');
var args = this.saved_handlers.concat(slice(arguments, 1));
assert.arrayOfFunc(args, 'handler chain');
var self = this;
this.rpc_table[opts.procedure] = {
name: opts.name,
procedure: opts.procedure,
call: opts.call,
reply: opts.reply,
chain: args
};
this.on(opts.name, function onRpcCall(call, reply) {
var log = self.log.child({
procedure: opts.name,
xid: call.xid
}, true);
call.log = log;
reply.log = log;
call._rpc_proc_name = opts.name;
self._run(call, reply, self.rpc_table[opts.procedure].chain);
});
};
RpcServer.prototype._run = function _run(req, res, chain) {
var d;
var err;
var i = -1;
var id = req.xid;
var log = this.log;
var self = this;
function fire_rpc() {
return ([
self.name,
req._rpc_proc_name,
id
]);
}
function fire_handler() {
return ([
self.name,
req._rpc_proc_name,
id
]);
}
function next(arg) {
var done = false;
if (arg) {
if (arg instanceof Error) {
log.trace(arg, 'next(err=%s)', (arg.name || 'Error'));
arg.xid = req.xid;
err = arg;
req.connection.write(err.toBuffer());
done = true;
}
}
if (arg === false)
done = true;
var h_name;
// Fire DTrace done for the previous handler.
if ((i + 1) > 0 && chain[i] && !chain[i]._skip) {
h_name = chain[i].name || ('handler-' + i);
dtrace._rpc_probes['handler-done'].fire(function () {
return ([
self.name,
req._rpc_proc_name,
h_name,
id
]);
});
}
// Run the next handler up
if (!done && chain[++i]) {
if (chain[i]._skip) {
next();
return;
}
h_name = chain[i].name || ('handler-' + i);
dtrace._rpc_probes['handler-start'].fire(function () {
return ([
self.name,
req._rpc_proc_name,
h_name,
id
]);
});
var n = once(next);
n.ifError = ifError(n);
if (log.trace()) {
log.trace({
route_name: n,
rpc_call: req,
rpc_reply: res
}, '%s: running handler %s', self.name, h_name);
}
chain[i].call(self, req, res, n);
return;
}
dtrace._rpc_probes['rpc-done'].fire(fire_rpc);
self.emit('after', req._rpc_proc_name, req, res, err);
}
var n1 = once(next);
n1.ifError = ifError(n1);
dtrace._rpc_probes['rpc-start'].fire(fire_rpc);
d = domain.create();
d.add(req);
d.add(res);
d.on('error', function onError(error) {
if (error._rpc_next) {
error._rpc_next(error);
} else if (self.listeners('uncaughtException').length) {
self.emit('uncaughtException', req, res, error);
} else {
req.connection.destroy();
self.emit('after', req._rpc_proc_name, req, res, error);
}
});
d.run(n1);
};
RpcServer.prototype.toString = function toString() {
var fmt = '[object RpcServer <program=%d, version=%d>]';
return (sprintf(fmt, this.program, this.version));
};
function createServer(opts) {
return (new RpcServer(opts));
}
///--- Exports
module.exports = {
RpcServer: RpcServer,
createServer: createServer
};