You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
736 lines
22 KiB
736 lines
22 KiB
3 years ago
|
/**
|
||
|
* Copyright (c) 2013 Yahoo! Inc. All rights reserved.
|
||
|
*
|
||
|
* Copyrights licensed under the MIT License. See the accompanying LICENSE file
|
||
|
* for terms.
|
||
|
*/
|
||
|
|
||
|
var net = require('net');
|
||
|
var utils = require('util');
|
||
|
var events = require('events');
|
||
|
|
||
|
var jute = require('./jute');
|
||
|
var ConnectionStringParser = require('./ConnectionStringParser.js');
|
||
|
var WatcherManager = require('./WatcherManager.js');
|
||
|
var PacketQueue = require('./PacketQueue.js');
|
||
|
var Exception = require('./Exception.js');
|
||
|
|
||
|
/**
|
||
|
* This class manages the connection between the client and the ensemble.
|
||
|
*
|
||
|
* @module node-zookeeper-client
|
||
|
*/
|
||
|
|
||
|
// Constants.
|
||
|
var STATES = { // Connection States.
|
||
|
DISCONNECTED : 0,
|
||
|
CONNECTING : 1,
|
||
|
CONNECTED : 2,
|
||
|
CONNECTED_READ_ONLY : 3,
|
||
|
CLOSING : -1,
|
||
|
CLOSED : -2,
|
||
|
SESSION_EXPIRED : -3,
|
||
|
AUTHENTICATION_FAILED : -4
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Construct a new ConnectionManager instance.
|
||
|
*
|
||
|
* @class ConnectionStringParser
|
||
|
* @constructor
|
||
|
* @param connectionString {String} ZooKeeper server ensemble string.
|
||
|
* @param options {Object} Client options.
|
||
|
* @param stateListener {Object} Listener for state changes.
|
||
|
*/
|
||
|
function ConnectionManager(connectionString, options, stateListener) {
|
||
|
events.EventEmitter.call(this);
|
||
|
|
||
|
this.watcherManager = new WatcherManager();
|
||
|
this.connectionStringParser = new ConnectionStringParser(connectionString);
|
||
|
|
||
|
this.servers = this.connectionStringParser.getServers();
|
||
|
this.chrootPath = this.connectionStringParser.getChrootPath();
|
||
|
this.nextServerIndex = 0;
|
||
|
this.serverAttempts = 0;
|
||
|
|
||
|
this.state = STATES.DISCONNECTED;
|
||
|
|
||
|
this.options = options;
|
||
|
this.spinDelay = options.spinDelay;
|
||
|
|
||
|
this.updateTimeout(options.sessionTimeout);
|
||
|
this.connectTimeoutHandler = null;
|
||
|
|
||
|
this.xid = 0;
|
||
|
|
||
|
this.sessionId = new Buffer(8);
|
||
|
if (Buffer.isBuffer(options.sessionId)) {
|
||
|
options.sessionId.copy(this.sessionId);
|
||
|
} else {
|
||
|
this.sessionId.fill(0);
|
||
|
}
|
||
|
|
||
|
this.sessionPassword = new Buffer(16);
|
||
|
if (Buffer.isBuffer(options.sessionPassword)) {
|
||
|
options.sessionPassword.copy(this.sessionPassword);
|
||
|
} else {
|
||
|
this.sessionPassword.fill(0);
|
||
|
}
|
||
|
|
||
|
// scheme:auth pairs
|
||
|
this.credentials = [];
|
||
|
|
||
|
// Last seen zxid.
|
||
|
this.zxid = new Buffer(8);
|
||
|
this.zxid.fill(0);
|
||
|
|
||
|
|
||
|
this.pendingBuffer = null;
|
||
|
|
||
|
this.packetQueue = new PacketQueue();
|
||
|
this.packetQueue.on('readable', this.onPacketQueueReadable.bind(this));
|
||
|
this.pendingQueue = [];
|
||
|
|
||
|
this.on('state', stateListener);
|
||
|
}
|
||
|
|
||
|
utils.inherits(ConnectionManager, events.EventEmitter);
|
||
|
|
||
|
/**
|
||
|
* Update the session timeout and related timeout variables.
|
||
|
*
|
||
|
* @method updateTimeout
|
||
|
* @private
|
||
|
* @param sessionTimeout {Number} Milliseconds of the timeout value.
|
||
|
*/
|
||
|
ConnectionManager.prototype.updateTimeout = function (sessionTimeout) {
|
||
|
this.sessionTimeout = sessionTimeout;
|
||
|
|
||
|
// Designed to have time to try all the servers.
|
||
|
this.connectTimeout = Math.floor(sessionTimeout / this.servers.length);
|
||
|
|
||
|
// We at least send out one ping one third of the session timeout, so
|
||
|
// the read timeout is two third of the session timeout.
|
||
|
this.pingTimeout = Math.floor(this.sessionTimeout / 3);
|
||
|
// this.readTimeout = Math.floor(sessionTimeout * 2 / 3);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Find the next available server to connect. If all server has been tried,
|
||
|
* it will wait for a random time between 0 to spin delay before call back
|
||
|
* with the next server.
|
||
|
*
|
||
|
* callback prototype:
|
||
|
* callback(server);
|
||
|
*
|
||
|
* @method findNextServer
|
||
|
* @param callback {Function} callback function.
|
||
|
*
|
||
|
*/
|
||
|
ConnectionManager.prototype.findNextServer = function (callback) {
|
||
|
var self = this;
|
||
|
|
||
|
self.nextServerIndex %= self.servers.length;
|
||
|
|
||
|
if (self.serverAttempts === self.servers.length) {
|
||
|
setTimeout(function () {
|
||
|
callback(self.servers[self.nextServerIndex]);
|
||
|
self.nextServerIndex += 1;
|
||
|
|
||
|
// reset attempts since we already waited for enough time.
|
||
|
self.serverAttempts = 0;
|
||
|
}, Math.random() * self.spinDelay);
|
||
|
} else {
|
||
|
self.serverAttempts += 1;
|
||
|
|
||
|
process.nextTick(function () {
|
||
|
callback(self.servers[self.nextServerIndex]);
|
||
|
self.nextServerIndex += 1;
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Change the current state to the given state if the given state is different
|
||
|
* from current state. Emit the state change event with the changed state.
|
||
|
*
|
||
|
* @method setState
|
||
|
* @param state {Number} The state to be set.
|
||
|
*/
|
||
|
ConnectionManager.prototype.setState = function (state) {
|
||
|
if (typeof state !== 'number') {
|
||
|
throw new Error('state must be a valid number.');
|
||
|
}
|
||
|
|
||
|
if (this.state !== state) {
|
||
|
this.state = state;
|
||
|
this.emit('state', this.state);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.registerDataWatcher = function (path, watcher) {
|
||
|
this.watcherManager.registerDataWatcher(path, watcher);
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.registerChildWatcher = function (path, watcher) {
|
||
|
this.watcherManager.registerChildWatcher(path, watcher);
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.registerExistenceWatcher = function (path, watcher) {
|
||
|
this.watcherManager.registerExistenceWatcher(path, watcher);
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.cleanupPendingQueue = function (errorCode) {
|
||
|
var pendingPacket = this.pendingQueue.shift();
|
||
|
|
||
|
while (pendingPacket) {
|
||
|
if (pendingPacket.callback) {
|
||
|
pendingPacket.callback(Exception.create(errorCode));
|
||
|
}
|
||
|
|
||
|
pendingPacket = this.pendingQueue.shift();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.getSessionId = function () {
|
||
|
var result = new Buffer(8);
|
||
|
|
||
|
this.sessionId.copy(result);
|
||
|
return result;
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.getSessionPassword = function () {
|
||
|
var result = new Buffer(16);
|
||
|
|
||
|
this.sessionPassword.copy(result);
|
||
|
return result;
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.getSessionTimeout = function () {
|
||
|
return this.sessionTimeout;
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.connect = function () {
|
||
|
var self = this;
|
||
|
|
||
|
self.setState(STATES.CONNECTING);
|
||
|
|
||
|
self.findNextServer(function (server) {
|
||
|
self.socket = net.connect(server);
|
||
|
|
||
|
self.connectTimeoutHandler = setTimeout(
|
||
|
self.onSocketConnectTimeout.bind(self),
|
||
|
self.connectTimeout
|
||
|
);
|
||
|
|
||
|
// Disable the Nagle algorithm.
|
||
|
self.socket.setNoDelay();
|
||
|
|
||
|
self.socket.on('connect', self.onSocketConnected.bind(self));
|
||
|
self.socket.on('data', self.onSocketData.bind(self));
|
||
|
self.socket.on('drain', self.onSocketDrain.bind(self));
|
||
|
self.socket.on('close', self.onSocketClosed.bind(self));
|
||
|
self.socket.on('error', self.onSocketError.bind(self));
|
||
|
});
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.close = function () {
|
||
|
var self = this,
|
||
|
header = new jute.protocol.RequestHeader(),
|
||
|
request;
|
||
|
|
||
|
self.setState(STATES.CLOSING);
|
||
|
|
||
|
header.type = jute.OP_CODES.CLOSE_SESSION;
|
||
|
request = new jute.Request(header, null);
|
||
|
|
||
|
self.queue(request);
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.onSocketClosed = function (hasError) {
|
||
|
var retry = false,
|
||
|
errorCode,
|
||
|
pendingPacket;
|
||
|
|
||
|
switch (this.state) {
|
||
|
case STATES.CLOSING:
|
||
|
errorCode = Exception.CONNECTION_LOSS;
|
||
|
retry = false;
|
||
|
break;
|
||
|
case STATES.SESSION_EXPIRED:
|
||
|
errorCode = Exception.SESSION_EXPIRED;
|
||
|
retry = false;
|
||
|
break;
|
||
|
case STATES.AUTHENTICATION_FAILED:
|
||
|
errorCode = Exception.AUTH_FAILED;
|
||
|
retry = false;
|
||
|
break;
|
||
|
default:
|
||
|
errorCode = Exception.CONNECTION_LOSS;
|
||
|
retry = true;
|
||
|
}
|
||
|
|
||
|
this.cleanupPendingQueue(errorCode);
|
||
|
this.setState(STATES.DISCONNECTED);
|
||
|
|
||
|
if (retry) {
|
||
|
this.connect();
|
||
|
} else {
|
||
|
this.setState(STATES.CLOSED);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.onSocketError = function (error) {
|
||
|
if (this.connectTimeoutHandler) {
|
||
|
clearTimeout(this.connectTimeoutHandler);
|
||
|
}
|
||
|
|
||
|
// After socket error, the socket closed event will be triggered,
|
||
|
// we will retry connect in that listener function.
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.onSocketConnectTimeout = function () {
|
||
|
// Destroy the current socket so the socket closed event
|
||
|
// will be trigger.
|
||
|
this.socket.destroy();
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.onSocketConnected = function () {
|
||
|
var connectRequest,
|
||
|
authRequest,
|
||
|
setWatchesRequest,
|
||
|
header,
|
||
|
payload;
|
||
|
|
||
|
if (this.connectTimeoutHandler) {
|
||
|
clearTimeout(this.connectTimeoutHandler);
|
||
|
}
|
||
|
|
||
|
connectRequest = new jute.Request(null, new jute.protocol.ConnectRequest(
|
||
|
jute.PROTOCOL_VERSION,
|
||
|
this.zxid,
|
||
|
this.sessionTimeout,
|
||
|
this.sessionId,
|
||
|
this.sessionPassword
|
||
|
));
|
||
|
|
||
|
// XXX No read only support yet.
|
||
|
this.socket.write(connectRequest.toBuffer());
|
||
|
|
||
|
// Set auth info
|
||
|
if (this.credentials.length > 0) {
|
||
|
this.credentials.forEach(function (credential) {
|
||
|
header = new jute.protocol.RequestHeader();
|
||
|
payload = new jute.protocol.AuthPacket();
|
||
|
|
||
|
header.xid = jute.XID_AUTHENTICATION;
|
||
|
header.type = jute.OP_CODES.AUTH;
|
||
|
|
||
|
payload.type = 0;
|
||
|
payload.scheme = credential.scheme;
|
||
|
payload.auth = credential.auth;
|
||
|
|
||
|
authRequest = new jute.Request(header, payload);
|
||
|
this.queue(authRequest);
|
||
|
|
||
|
}, this);
|
||
|
}
|
||
|
|
||
|
// Reset the watchers if we have any.
|
||
|
if (!this.watcherManager.isEmpty()) {
|
||
|
header = new jute.protocol.RequestHeader();
|
||
|
payload = new jute.protocol.SetWatches();
|
||
|
|
||
|
header.type = jute.OP_CODES.SET_WATCHES;
|
||
|
header.xid = jute.XID_SET_WATCHES;
|
||
|
|
||
|
payload.setChrootPath(this.chrootPath);
|
||
|
payload.relativeZxid = this.zxid;
|
||
|
payload.dataWatches = this.watcherManager.getDataWatcherPaths();
|
||
|
payload.existWatches = this.watcherManager.getExistenceWatcherPaths();
|
||
|
payload.childWatches = this.watcherManager.getChildWatcherPaths();
|
||
|
|
||
|
setWatchesRequest = new jute.Request(header, payload);
|
||
|
this.queue(setWatchesRequest);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.onSocketTimeout = function () {
|
||
|
var header,
|
||
|
request;
|
||
|
|
||
|
if (this.socket &&
|
||
|
(this.state === STATES.CONNECTED ||
|
||
|
this.state === STATES.CONNECTED_READ_ONLY)) {
|
||
|
header = new jute.protocol.RequestHeader(
|
||
|
jute.XID_PING,
|
||
|
jute.OP_CODES.PING
|
||
|
);
|
||
|
|
||
|
request = new jute.Request(header, null);
|
||
|
this.queue(request);
|
||
|
|
||
|
// Re-register the timeout handler since it only fired once.
|
||
|
this.socket.setTimeout(
|
||
|
this.pingTimeout,
|
||
|
this.onSocketTimeout.bind(this)
|
||
|
);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/* eslint-disable complexity,max-depth */
|
||
|
ConnectionManager.prototype.onSocketData = function (buffer) {
|
||
|
var self = this,
|
||
|
offset = 0,
|
||
|
size = 0,
|
||
|
connectResponse,
|
||
|
pendingPacket,
|
||
|
responseHeader,
|
||
|
responsePayload,
|
||
|
response,
|
||
|
event;
|
||
|
|
||
|
// Combine the pending buffer with the new buffer.
|
||
|
if (self.pendingBuffer) {
|
||
|
buffer = Buffer.concat(
|
||
|
[self.pendingBuffer, buffer],
|
||
|
self.pendingBuffer.length + buffer.length
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// We need at least 4 bytes
|
||
|
if (buffer.length < 4) {
|
||
|
self.pendingBuffer = buffer;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
size = buffer.readInt32BE(offset);
|
||
|
offset += 4;
|
||
|
|
||
|
if (buffer.length < size + 4) {
|
||
|
// More data are coming.
|
||
|
self.pendingBuffer = buffer;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (buffer.length === size + 4) {
|
||
|
// The size is perfect.
|
||
|
self.pendingBuffer = null;
|
||
|
} else {
|
||
|
// We have extra bytes, splice them out as pending buffer.
|
||
|
self.pendingBuffer = buffer.slice(size + 4);
|
||
|
buffer = buffer.slice(0, size + 4);
|
||
|
}
|
||
|
|
||
|
if (self.state === STATES.CONNECTING) {
|
||
|
// Handle connect response.
|
||
|
connectResponse = new jute.protocol.ConnectResponse();
|
||
|
offset += connectResponse.deserialize(buffer, offset);
|
||
|
|
||
|
|
||
|
if (connectResponse.timeOut <= 0) {
|
||
|
self.setState(STATES.SESSION_EXPIRED);
|
||
|
|
||
|
} else {
|
||
|
// Reset the server connection attempts since we connected now.
|
||
|
self.serverAttempts = 0;
|
||
|
|
||
|
self.sessionId = connectResponse.sessionId;
|
||
|
self.sessionPassword = connectResponse.passwd;
|
||
|
self.updateTimeout(connectResponse.timeOut);
|
||
|
|
||
|
self.setState(STATES.CONNECTED);
|
||
|
|
||
|
// Check if we have anything to send out just in case.
|
||
|
self.onPacketQueueReadable();
|
||
|
|
||
|
self.socket.setTimeout(
|
||
|
self.pingTimeout,
|
||
|
self.onSocketTimeout.bind(self)
|
||
|
);
|
||
|
|
||
|
}
|
||
|
} else {
|
||
|
// Handle all other repsonses.
|
||
|
responseHeader = new jute.protocol.ReplyHeader();
|
||
|
offset += responseHeader.deserialize(buffer, offset);
|
||
|
|
||
|
// TODO BETTTER LOGGING
|
||
|
switch (responseHeader.xid) {
|
||
|
case jute.XID_PING:
|
||
|
break;
|
||
|
case jute.XID_AUTHENTICATION:
|
||
|
if (responseHeader.err === Exception.AUTH_FAILED) {
|
||
|
self.setState(STATES.AUTHENTICATION_FAILED);
|
||
|
}
|
||
|
break;
|
||
|
case jute.XID_NOTIFICATION:
|
||
|
event = new jute.protocol.WatcherEvent();
|
||
|
|
||
|
if (self.chrootPath) {
|
||
|
event.setChrootPath(self.chrootPath);
|
||
|
}
|
||
|
|
||
|
offset += event.deserialize(buffer, offset);
|
||
|
self.watcherManager.emit(event);
|
||
|
break;
|
||
|
default:
|
||
|
pendingPacket = self.pendingQueue.shift();
|
||
|
|
||
|
if (!pendingPacket) {
|
||
|
// TODO, better error handling and logging need to be done.
|
||
|
// Need to clean up and do a reconnect.
|
||
|
// throw new Error(
|
||
|
// 'Nothing in pending queue but got data from server.'
|
||
|
// );
|
||
|
self.socket.destroy(); // this will trigger reconnect
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (pendingPacket.request.header.xid !== responseHeader.xid) {
|
||
|
// TODO, better error handling/logging need to bee done here.
|
||
|
// Need to clean up and do a reconnect.
|
||
|
// throw new Error(
|
||
|
// 'Xid out of order. Got xid: ' +
|
||
|
// responseHeader.xid + ' with error code: ' +
|
||
|
// responseHeader.err + ', expected xid: ' +
|
||
|
// pendingPacket.request.header.xid + '.'
|
||
|
// );
|
||
|
self.socket.destroy(); // this will trigger reconnect
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (responseHeader.zxid) {
|
||
|
// TODO, In Java implementation, the condition is to
|
||
|
// check whether the long zxid is greater than 0, here
|
||
|
// use buffer so we simplify.
|
||
|
// Need to figure out side effect.
|
||
|
self.zxid = responseHeader.zxid;
|
||
|
}
|
||
|
|
||
|
if (responseHeader.err === 0) {
|
||
|
switch (pendingPacket.request.header.type) {
|
||
|
case jute.OP_CODES.CREATE:
|
||
|
responsePayload = new jute.protocol.CreateResponse();
|
||
|
break;
|
||
|
case jute.OP_CODES.DELETE:
|
||
|
responsePayload = null;
|
||
|
break;
|
||
|
case jute.OP_CODES.GET_CHILDREN2:
|
||
|
responsePayload = new jute.protocol.GetChildren2Response();
|
||
|
break;
|
||
|
case jute.OP_CODES.EXISTS:
|
||
|
responsePayload = new jute.protocol.ExistsResponse();
|
||
|
break;
|
||
|
case jute.OP_CODES.SET_DATA:
|
||
|
responsePayload = new jute.protocol.SetDataResponse();
|
||
|
break;
|
||
|
case jute.OP_CODES.GET_DATA:
|
||
|
responsePayload = new jute.protocol.GetDataResponse();
|
||
|
break;
|
||
|
case jute.OP_CODES.SET_ACL:
|
||
|
responsePayload = new jute.protocol.SetACLResponse();
|
||
|
break;
|
||
|
case jute.OP_CODES.GET_ACL:
|
||
|
responsePayload = new jute.protocol.GetACLResponse();
|
||
|
break;
|
||
|
case jute.OP_CODES.SET_WATCHES:
|
||
|
responsePayload = null;
|
||
|
break;
|
||
|
case jute.OP_CODES.CLOSE_SESSION:
|
||
|
responsePayload = null;
|
||
|
break;
|
||
|
case jute.OP_CODES.MULTI:
|
||
|
responsePayload = new jute.TransactionResponse();
|
||
|
break;
|
||
|
default:
|
||
|
// throw new Error('Unknown request OP_CODE: ' +
|
||
|
// pendingPacket.request.header.type);
|
||
|
self.socket.destroy(); // this will trigger reconnect
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (responsePayload) {
|
||
|
if (self.chrootPath) {
|
||
|
responsePayload.setChrootPath(self.chrootPath);
|
||
|
}
|
||
|
|
||
|
offset += responsePayload.deserialize(buffer, offset);
|
||
|
}
|
||
|
|
||
|
if (pendingPacket.callback) {
|
||
|
pendingPacket.callback(
|
||
|
null,
|
||
|
new jute.Response(responseHeader, responsePayload)
|
||
|
);
|
||
|
}
|
||
|
} else if (pendingPacket.callback) {
|
||
|
pendingPacket.callback(
|
||
|
Exception.create(responseHeader.err),
|
||
|
new jute.Response(responseHeader, null)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// We have more data to process, need to recursively process it.
|
||
|
if (self.pendingBuffer) {
|
||
|
self.onSocketData(new Buffer(0));
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/* eslint-enable complexity,max-depth */
|
||
|
|
||
|
ConnectionManager.prototype.onSocketDrain = function () {
|
||
|
// Trigger write on socket.
|
||
|
this.onPacketQueueReadable();
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.onPacketQueueReadable = function () {
|
||
|
var packet,
|
||
|
header;
|
||
|
|
||
|
switch (this.state) {
|
||
|
case STATES.CONNECTED:
|
||
|
case STATES.CONNECTED_READ_ONLY:
|
||
|
case STATES.CLOSING:
|
||
|
// Continue
|
||
|
break;
|
||
|
case STATES.DISCONNECTED:
|
||
|
case STATES.CONNECTING:
|
||
|
case STATES.CLOSED:
|
||
|
case STATES.SESSION_EXPIRED:
|
||
|
case STATES.AUTHENTICATION_FAILED:
|
||
|
// Skip since we can not send traffic out
|
||
|
return;
|
||
|
default:
|
||
|
throw new Error('Unknown state: ' + this.state);
|
||
|
}
|
||
|
|
||
|
while ((packet = this.packetQueue.shift()) !== undefined) {
|
||
|
header = packet.request.header;
|
||
|
if (header !== null &&
|
||
|
header.type !== jute.OP_CODES.PING &&
|
||
|
header.type !== jute.OP_CODES.AUTH) {
|
||
|
|
||
|
header.xid = this.xid;
|
||
|
this.xid += 1;
|
||
|
|
||
|
// Only put requests that are not connect, ping and auth into
|
||
|
// the pending queue.
|
||
|
this.pendingQueue.push(packet);
|
||
|
}
|
||
|
|
||
|
if (!this.socket.write(packet.request.toBuffer())) {
|
||
|
// Back pressure is handled here, when the socket emit
|
||
|
// drain event, this method will be invoked again.
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (header.type === jute.OP_CODES.CLOSE_SESSION) {
|
||
|
// The close session should be the final packet sent to the
|
||
|
// server.
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.addAuthInfo = function (scheme, auth) {
|
||
|
if (!scheme || typeof scheme !== 'string') {
|
||
|
throw new Error('scheme must be a non-empty string.');
|
||
|
}
|
||
|
|
||
|
if (!Buffer.isBuffer(auth)) {
|
||
|
throw new Error('auth must be a valid instance of Buffer');
|
||
|
}
|
||
|
|
||
|
var header,
|
||
|
payload,
|
||
|
request;
|
||
|
|
||
|
this.credentials.push({
|
||
|
scheme : scheme,
|
||
|
auth : auth
|
||
|
});
|
||
|
|
||
|
switch (this.state) {
|
||
|
case STATES.CONNECTED:
|
||
|
case STATES.CONNECTED_READ_ONLY:
|
||
|
// Only queue the auth request when connected.
|
||
|
header = new jute.protocol.RequestHeader();
|
||
|
payload = new jute.protocol.AuthPacket();
|
||
|
|
||
|
header.xid = jute.XID_AUTHENTICATION;
|
||
|
header.type = jute.OP_CODES.AUTH;
|
||
|
|
||
|
payload.type = 0;
|
||
|
payload.scheme = scheme;
|
||
|
payload.auth = auth;
|
||
|
|
||
|
this.queue(new jute.Request(header, payload));
|
||
|
break;
|
||
|
case STATES.DISCONNECTED:
|
||
|
case STATES.CONNECTING:
|
||
|
case STATES.CLOSING:
|
||
|
case STATES.CLOSED:
|
||
|
case STATES.SESSION_EXPIRED:
|
||
|
case STATES.AUTHENTICATION_FAILED:
|
||
|
// Skip when we are not in a live state.
|
||
|
return;
|
||
|
default:
|
||
|
throw new Error('Unknown state: ' + this.state);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ConnectionManager.prototype.queue = function (request, callback) {
|
||
|
if (typeof request !== 'object') {
|
||
|
throw new Error('request must be a valid instance of jute.Request.');
|
||
|
}
|
||
|
|
||
|
if (this.chrootPath && request.payload) {
|
||
|
request.payload.setChrootPath(this.chrootPath);
|
||
|
}
|
||
|
|
||
|
|
||
|
callback = callback || function () {};
|
||
|
|
||
|
switch (this.state) {
|
||
|
case STATES.DISCONNECTED:
|
||
|
case STATES.CONNECTING:
|
||
|
case STATES.CONNECTED:
|
||
|
case STATES.CONNECTED_READ_ONLY:
|
||
|
// queue the packet
|
||
|
this.packetQueue.push({
|
||
|
request : request,
|
||
|
callback : callback
|
||
|
});
|
||
|
break;
|
||
|
case STATES.CLOSING:
|
||
|
if (request.header &&
|
||
|
request.header.type === jute.OP_CODES.CLOSE_SESSION) {
|
||
|
this.packetQueue.push({
|
||
|
request : request,
|
||
|
callback : callback
|
||
|
});
|
||
|
} else {
|
||
|
callback(Exception.create(Exception.CONNECTION_LOSS));
|
||
|
}
|
||
|
break;
|
||
|
case STATES.CLOSED:
|
||
|
callback(Exception.create(Exception.CONNECTION_LOSS));
|
||
|
return;
|
||
|
case STATES.SESSION_EXPIRED:
|
||
|
callback(Exception.create(Exception.SESSION_EXPIRED));
|
||
|
return;
|
||
|
case STATES.AUTHENTICATION_FAILED:
|
||
|
callback(Exception.create(Exception.AUTH_FAILED));
|
||
|
return;
|
||
|
default:
|
||
|
throw new Error('Unknown state: ' + this.state);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
module.exports = ConnectionManager;
|
||
|
module.exports.STATES = STATES;
|