@nats-io/transport-node
Version:
Node.js client for NATS, a lightweight, high-performance cloud native messaging system
461 lines • 15.1 kB
JavaScript
;
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() {
// ignored - this is not required, as there's no throttling
}
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;
async function nodeResolveHost(s) {
const a = (0, nats_base_client_1.deferred)();
const aaaa = (0, nats_base_client_1.deferred)();
node_dns_1.default.resolve4(s, (err, records) => {
if (err) {
a.resolve(err);
}
else {
a.resolve(records);
}
});
node_dns_1.default.resolve6(s, (err, records) => {
if (err) {
aaaa.resolve(err);
}
else {
aaaa.resolve(records);
}
});
const ips = [];
const da = await a;
if (Array.isArray(da)) {
ips.push(...da);
}
const daaaa = await aaaa;
if (Array.isArray(daaaa)) {
ips.push(...daaaa);
}
if (ips.length === 0) {
ips.push(s);
}
return ips;
}
//# sourceMappingURL=node_transport.js.map