UNPKG

amqplib

Version:

An AMQP 0-9-1 (e.g., RabbitMQ) library and client.

194 lines (168 loc) 5.79 kB
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;