UNPKG

@nats-io/transport-node

Version:

Node.js client for NATS, a lightweight, high-performance cloud native messaging system

457 lines 15 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NodeTransport = exports.VERSION = void 0; exports.nodeResolveHost = nodeResolveHost; const nats_base_client_1 = require("./nats-base-client"); const node_net_1 = require("node:net"); const node_tls_1 = require("node:tls"); const node_path_1 = require("node:path"); const node_fs_1 = require("node:fs"); const node_dns_1 = __importDefault(require("node:dns")); const version_1 = require("./version"); exports.VERSION = version_1.version; const LANG = "nats.js"; class NodeTransport { socket; version; lang; yields = []; signal = (0, nats_base_client_1.deferred)(); closedNotification = (0, nats_base_client_1.deferred)(); options; connected = false; tlsName = ""; done = false; closeError; constructor() { this.lang = LANG; this.version = exports.VERSION; } async connect(hp, options) { this.tlsName = hp.tlsName; this.options = options; const { tls } = this.options; const { handshakeFirst } = tls || {}; try { if (handshakeFirst === true) { this.socket = await this.tlsFirst(hp); } else { this.socket = await this.dial(hp); } // protocol could have terminated if (this.done) { this.socket?.destroy(); } const info = await this.peekInfo(); (0, nats_base_client_1.checkOptions)(info, options); const { tls_required: tlsRequired, tls_available: tlsAvailable } = info; const desired = tlsAvailable === true && options.tls !== null; if (!handshakeFirst && (tlsRequired || desired)) { this.socket = await this.startTLS(); } if (this.done) { this.socket?.destroy(); } //@ts-ignore: this is possibly a TlsSocket if (tlsRequired && this.socket.encrypted !== true) { throw nats_base_client_1.errors.InvalidArgumentError.format("tls", "is not available on this server"); } this.connected = true; this.setupHandlers(); this.signal.resolve(); return Promise.resolve(); } catch (ex) { let err = ex; if (!err) { // this seems to be possible in Kubernetes // where an error is thrown, but it is undefined // when something like istio-init is booting up err = new nats_base_client_1.errors.ConnectionError("error connecting - node provided an undefined error"); } // @ts-ignore: node error const { code } = err; const perr = code === "ECONNREFUSED" ? new nats_base_client_1.errors.ConnectionError("connection refused", { cause: err }) : err; this.socket?.destroy(); throw perr; } } dial(hp) { const d = (0, nats_base_client_1.deferred)(); let dialError; const socket = (0, node_net_1.createConnection)(hp.port, hp.hostname, () => { d.resolve(socket); socket.removeAllListeners(); }); socket.on("error", (err) => { dialError = err; }); socket.on("close", () => { socket.removeAllListeners(); d.reject(dialError); }); socket.setNoDelay(true); return d; } get isClosed() { return this.done; } close(err) { return this._closed(err, false); } peekInfo() { const d = (0, nats_base_client_1.deferred)(); let peekError; this.socket.on("data", (frame) => { this.yields.push(frame); const t = nats_base_client_1.DataBuffer.concat(...this.yields); const pm = (0, nats_base_client_1.extractProtocolMessage)(t); if (pm !== "") { try { const m = nats_base_client_1.INFO.exec(pm); if (!m) { throw new Error("unexpected response from server"); } const info = JSON.parse(m[1]); d.resolve(info); } catch (err) { d.reject(err); } finally { this.socket.removeAllListeners(); } } }); this.socket.on("error", (err) => { peekError = err; }); this.socket.on("close", () => { this.socket.removeAllListeners(); d.reject(peekError); }); return d; } loadFile(fn) { if (!fn) { return Promise.resolve(); } const d = (0, nats_base_client_1.deferred)(); try { fn = (0, node_path_1.resolve)(fn); if (!(0, node_fs_1.existsSync)(fn)) { d.reject(new Error(`${fn} doesn't exist`)); } (0, node_fs_1.readFile)(fn, (err, data) => { if (err) { return d.reject(err); } d.resolve(data); }); } catch (err) { d.reject(err); } return d; } async loadClientCerts() { const tlsOpts = {}; const { certFile, cert, caFile, ca, keyFile, key } = this.options.tls; try { if (certFile) { const data = await this.loadFile(certFile); if (data) { tlsOpts.cert = data; } } else if (cert) { tlsOpts.cert = cert; } if (keyFile) { const data = await this.loadFile(keyFile); if (data) { tlsOpts.key = data; } } else if (key) { tlsOpts.key = key; } if (caFile) { const data = await this.loadFile(caFile); if (data) { tlsOpts.ca = [data]; } } else if (ca) { tlsOpts.ca = ca; } return Promise.resolve(tlsOpts); } catch (err) { return Promise.reject(err); } } async tlsFirst(hp) { let tlsError; let tlsOpts = { servername: this.tlsName, rejectUnauthorized: true, }; if (this.socket) { tlsOpts.socket = this.socket; } if (typeof this.options.tls === "object") { try { const certOpts = await this.loadClientCerts() || {}; tlsOpts = (0, nats_base_client_1.extend)(tlsOpts, this.options.tls, certOpts); } catch (err) { return Promise.reject(new nats_base_client_1.errors.ConnectionError(err.message, { cause: err })); } } const d = (0, nats_base_client_1.deferred)(); try { const tlsSocket = (0, node_tls_1.connect)(hp.port, hp.hostname, tlsOpts, () => { tlsSocket.removeAllListeners(); d.resolve(tlsSocket); }); tlsSocket.on("error", (err) => { tlsError = err; }); tlsSocket.on("secureConnect", () => { // socket won't be authorized, if the user disabled it if (tlsOpts.rejectUnauthorized === false) { return; } if (!tlsSocket.authorized) { throw tlsSocket.authorizationError; } }); tlsSocket.on("close", () => { d.reject(tlsError); tlsSocket.removeAllListeners(); }); tlsSocket.setNoDelay(true); } catch (err) { // tls throws errors on bad certs see nats.js#310 d.reject(new nats_base_client_1.errors.ConnectionError(err.message, { cause: err })); } return d; } async startTLS() { let tlsError; let tlsOpts = { socket: this.socket, servername: this.tlsName, rejectUnauthorized: true, }; if (typeof this.options.tls === "object") { try { const certOpts = await this.loadClientCerts() || {}; tlsOpts = (0, nats_base_client_1.extend)(tlsOpts, this.options.tls, certOpts); } catch (err) { return Promise.reject(new nats_base_client_1.errors.ConnectionError(err.message, { cause: err, })); } } const d = (0, nats_base_client_1.deferred)(); try { const tlsSocket = (0, node_tls_1.connect)(tlsOpts, () => { tlsSocket.removeAllListeners(); d.resolve(tlsSocket); }); tlsSocket.on("error", (err) => { tlsError = err; }); tlsSocket.on("secureConnect", () => { // socket won't be authorized, if the user disabled it if (tlsOpts.rejectUnauthorized === false) { return; } if (!tlsSocket.authorized) { throw tlsSocket.authorizationError; } }); tlsSocket.on("close", () => { d.reject(tlsError); tlsSocket.removeAllListeners(); }); } catch (err) { // tls throws errors on bad certs see nats.js#310 d.reject(new nats_base_client_1.errors.ConnectionError(err.message, { cause: err })); } return d; } setupHandlers() { let connError; this.socket.on("data", (frame) => { this.yields.push(frame); return this.signal.resolve(); }); this.socket.on("error", (err) => { connError = err; }); this.socket.on("end", () => { if (this.socket?.destroyed) { return; } this.socket?.write(new Uint8Array(0), () => { this.socket?.end(); }); }); this.socket.on("close", () => { this._closed(connError, false); }); } [Symbol.asyncIterator]() { return this.iterate(); } async *iterate() { while (true) { if (this.yields.length === 0) { await this.signal; } const yields = this.yields; this.yields = []; for (let i = 0; i < yields.length; i++) { if (this.options.debug) { console.info(`> ${(0, nats_base_client_1.render)(yields[i])}`); } yield yields[i]; } // yielding could have paused and microtask // could have added messages. Prevent allocations // if possible if (this.done) { break; } else if (this.yields.length === 0) { yields.length = 0; this.yields = yields; this.signal = (0, nats_base_client_1.deferred)(); } } } discard() { this.done = true; if (this.socket) { try { this.socket.removeAllListeners(); this.socket.destroy(); } catch { // We tried, just ignore it } } } disconnect() { this._closed(undefined, true).then().catch(); } isEncrypted() { return this.socket instanceof node_tls_1.TLSSocket; } _send(frame) { if (this.isClosed || this.socket === undefined) { return Promise.resolve(); } if (this.options.debug) { console.info(`< ${(0, nats_base_client_1.render)(frame)}`); } const d = (0, nats_base_client_1.deferred)(); try { this.socket.write(frame, (err) => { if (err) { if (this.options.debug) { console.error(`!!! ${(0, nats_base_client_1.render)(frame)}: ${err}`); } return d.reject(err); } return d.resolve(); }); } catch (err) { if (this.options.debug) { console.error(`!!! ${(0, nats_base_client_1.render)(frame)}: ${err}`); } d.reject(err); } return d; } send(frame) { const p = this._send(frame); p.catch((_err) => { // we ignore write errors because client will // fail on a read or when the heartbeat timer // detects a stale connection }); } async _closed(err, internal = true) { // if this connection didn't succeed, then ignore it. if (!this.connected) return; if (this.done) { this.socket?.destroy(); return; } this.closeError = err; // only try to flush the outbound buffer if we got no error and // the close is internal, if the transport closed, we are done. if (!err && this.socket && internal) { try { await this._send(new TextEncoder().encode("")); } catch (err) { if (this.options.debug) { console.log("transport close terminated with an error", err); } } } try { if (this.socket) { this.socket.removeAllListeners(); this.socket?.destroy(); this.socket = undefined; } } catch (err) { console.log(err); } this.done = true; this.closedNotification.resolve(this.closeError); } closed() { return this.closedNotification; } } exports.NodeTransport = NodeTransport; function nodeResolveHost(s) { const ips = (0, nats_base_client_1.deferred)(); node_dns_1.default.lookup(s, { all: true }, //@ts-ignore: callback changes shape when all is true (err, address) => { if (err) { ips.reject(err); return; } const buf = []; for (const r of address) { buf.push(r.address); } if (buf.length === 0) { buf.push(s); } ips.resolve(buf); }); return ips; } //# sourceMappingURL=node_transport.js.map