amqplib
Version:
An AMQP 0-9-1 (e.g., RabbitMQ) library and client.
194 lines (168 loc) • 5.79 kB
JavaScript
const { URL } = require('node:url');
const QS = require('node:querystring');
const net = require('node:net');
const tls = require('node:tls');
const Connection = require('./connection').Connection;
const fmt = require('node:util').format;
const credentials = require('./credentials');
function copyInto(obj, target) {
const keys = Object.keys(obj);
let i = keys.length;
while (i--) {
const k = keys[i];
target[k] = obj[k];
}
return target;
}
// Adapted from util._extend, which is too fringe to use.
function clone(obj) {
return copyInto(obj, {});
}
const CLIENT_PROPERTIES = {
product: 'amqplib',
version: require('../package.json').version,
platform: fmt('Node.JS %s', process.version),
information: 'https://amqp-node.github.io/amqplib/',
capabilities: {
publisher_confirms: true,
exchange_exchange_bindings: true,
'basic.nack': true,
consumer_cancel_notify: true,
'connection.blocked': true,
authentication_failure_close: true,
},
};
// Construct the main frames used in the opening handshake
function openFrames(vhost, query, credentials, extraClientProperties) {
if (!vhost) vhost = '/';
else vhost = QS.unescape(vhost);
query = query || {};
function intOrDefault(val, def) {
return val === undefined ? def : parseInt(val, 10);
}
const clientProperties = Object.create(CLIENT_PROPERTIES);
return {
// start-ok
clientProperties: copyInto(extraClientProperties, clientProperties),
mechanism: credentials.mechanism,
response: credentials.response(),
locale: query.locale || 'en_US',
// tune-ok
channelMax: intOrDefault(query.channelMax, 0),
frameMax: intOrDefault(query.frameMax, 131072),
heartbeat: intOrDefault(query.heartbeat, 0),
// open
virtualHost: vhost,
capabilities: '',
insist: 0,
};
}
// Decide on credentials based on what we're supplied.
function credentialsFromUrl(parts) {
let user = 'guest';
let passwd = 'guest';
if (parts.username !== '' || parts.password !== '') {
user = parts.username ? unescape(parts.username) : '';
passwd = parts.password ? unescape(parts.password) : '';
}
return credentials.plain(user, passwd);
}
function connect(url, socketOptions, openCallback) {
// tls.connect uses `util._extend()` on the options given it, which
// copies only properties mentioned in `Object.keys()`, when
// processing the options. So I have to make copies too, rather
// than using `Object.create()`.
const sockopts = clone(socketOptions || {});
url = url || 'amqp://localhost';
const noDelay = !!sockopts.noDelay;
const timeout = sockopts.timeout;
const keepAlive = !!sockopts.keepAlive;
// 0 is default for node
const keepAliveDelay = sockopts.keepAliveDelay || 0;
const extraClientProperties = sockopts.clientProperties || {};
let protocol;
let fields;
if (typeof url === 'object') {
protocol = `${url.protocol || 'amqp'}:`;
sockopts.host = url.hostname;
if (!sockopts.servername && !net.isIP(url.hostname)) sockopts.servername = url.hostname;
sockopts.port = url.port || (protocol === 'amqp:' ? 5672 : 5671);
let user;
let pass;
// Only default if both are missing, to have the same behaviour as
// the stringly URL.
if (url.username === undefined && url.password === undefined) {
user = 'guest';
pass = 'guest';
} else {
user = url.username || '';
pass = url.password || '';
}
const config = {
locale: url.locale,
channelMax: url.channelMax,
frameMax: url.frameMax,
heartbeat: url.heartbeat,
};
fields = openFrames(url.vhost, config, sockopts.credentials || credentials.plain(user, pass), extraClientProperties);
} else {
let parts;
try {
parts = new URL(url);
} catch (_e) {
parts = {};
}
const host = (parts.hostname || '').replace(/^\[|\]$/g, '');
protocol = parts.protocol || '';
sockopts.host = host;
if (!sockopts.servername && !net.isIP(host)) sockopts.servername = host;
sockopts.port = parseInt(parts.port, 10) || (protocol === 'amqp:' ? 5672 : 5671);
const vhost = parts.pathname ? parts.pathname.substr(1) : null;
const query = QS.parse(parts.search ? parts.search.slice(1) : '');
// QS.parse returns arrays for duplicate keys; match url-parse's first-value-wins behavior.
for (const key in query) {
if (Array.isArray(query[key])) query[key] = query[key][0];
}
fields = openFrames(vhost, query, sockopts.credentials || credentialsFromUrl(parts), extraClientProperties);
}
let sockok = false;
let sock;
function onConnect() {
sockok = true;
sock.setNoDelay(noDelay);
if (keepAlive) sock.setKeepAlive(keepAlive, keepAliveDelay);
const c = new Connection(sock);
c.open(fields, (err, _ok) => {
// disable timeout once the connection is open, we don't want
// it fouling things
if (timeout) sock.setTimeout(0);
if (err === null) {
openCallback(null, c);
} else {
// The connection isn't closed by the server on e.g. wrong password
sock.end();
sock.destroy();
openCallback(err);
}
});
}
if (protocol === 'amqp:') {
sock = net.connect(sockopts, onConnect);
} else if (protocol === 'amqps:') {
sock = tls.connect(sockopts, onConnect);
} else {
throw new Error(`Expected amqp: or amqps: as the protocol; got ${protocol}`);
}
if (timeout) {
sock.setTimeout(timeout, () => {
sock.end();
sock.destroy();
openCallback(new Error('connect ETIMEDOUT'));
});
}
sock.once('error', (err) => {
if (!sockok) openCallback(err);
});
}
module.exports.connect = connect;
module.exports.credentialsFromUrl = credentialsFromUrl;