UNPKG

oracledb

Version:

A Node.js module for Oracle Database access from JavaScript and TypeScript

728 lines (657 loc) 25.3 kB
// Copyright (c) 2022, 2025, Oracle and/or its affiliates. //----------------------------------------------------------------------------- // // This software is dual-licensed to you under the Universal Permissive License // (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License // 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose // either license. // // If you elect to accept the software under the Apache License, Version 2.0, // the following applies: // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //----------------------------------------------------------------------------- 'use strict'; const zlib = require('zlib'); const Packet = require("./packet.js"); const NTTCP = require("./ntTcp.js"); const SessionAtts = require("./sessionAtts.js"); const constants = require("./constants.js"); const { createNode } = require('./connStrategy.js'); const errors = require("../../errors.js"); const { findNVPairRecurse, createNVPair, findValue } = require("./nvStrToNvPair.js"); const { Buffer } = require('buffer'); const EzConnect = require("./ezConnectResolver.js"); const { NLParamParser, tnsnamesFilePath } = require("./paramParser.js"); const process = require('process'); const downHostInstance = require("./connStrategy.js").SOLE_INST_DHCACHE; const {ANO} = require("./ANO.js"); /** * * @param {string} userConfig * @returns serverinfo */ async function getConnectionInfo(userConfig) { const connStr = await resolveConnectStr(userConfig.connectString, userConfig.configDir); const addressNode = await createNode(connStr, userConfig); let nvpair; if (typeof connStr === 'string') nvpair = createNVPair(connStr); else nvpair = connStr;//Already a NVPair const serverVal = findValue(nvpair, ["DESCRIPTION", "CONNECT_DATA", "SERVER"]); const connClass = findValue(nvpair, ["DESCRIPTION", "CONNECT_DATA", "POOL_CONNECTION_CLASS"]); const svcname = findValue(nvpair, ["DESCRIPTION", "CONNECT_DATA", "SERVICE_NAME"]); const sid = findValue(nvpair, ["DESCRIPTION", "CONNECT_DATA", "SID"]); const poolPurity = findValue(nvpair, ["DESCRIPTION", "CONNECT_DATA", "POOL_PURITY"]); return [serverVal, connClass, svcname, poolPurity, sid, addressNode]; } /** * Resolve the connect string to a NV format address * @param {String} connStr Connect string * @returns AddressNode */ const nlParamParser = new NLParamParser; async function resolveConnectStr(connectString, configDir) { const connStr = connectString.trim(); let resolvedVal = connStr; if ((connStr.indexOf(')') == -1) || (connStr.indexOf('(') != 0)) { if ((connStr.indexOf(':') != -1) || (connStr.indexOf('/') != -1)) { const ezcnObj = new EzConnect(connStr); resolvedVal = ezcnObj.getResolvedUrl(); return resolvedVal; } else { //try tns alias const namesFilePath = tnsnamesFilePath(configDir); const p = await nlParamParser.initializeNlpa(namesFilePath); resolvedVal = p.get(connStr.toUpperCase()); if (!resolvedVal) errors.throwErr(errors.ERR_TNS_ENTRY_NOT_FOUND, connStr, configDir ? configDir + '/tnsnames.ora' : process.env.TNS_ADMIN + '/tnsnames.ora'); if (resolvedVal.rhsType == 1) { const rString = resolvedVal.atom; if ((rString.indexOf(':') != -1) || (rString.indexOf('/') != -1)) { return new EzConnect(rString).getResolvedUrl(); } } resolvedVal = resolvedVal.getListElement(0); } } return resolvedVal; } async function resolveAddress(connStr, userConfig) { const connstr = await resolveConnectStr(connStr, userConfig.configDir); return createNode(connstr, userConfig); } /** * Timeout function * @param {Promise} asyncPromise input promise * @param {int} timeVal timeout value * @returns resolved value of input promise */ function timeout(asyncPromise, timeVal, oper, address, connID) { let timer; const timeoutPromise = new Promise((resolve, reject) => { // max possible value for 32-bit integer if (timeVal > 2147483647) timeVal = 2147483647; timer = setTimeout(() => reject(errors.getErr(errors.ERR_CONNECTION_TIMEDOUT, address.host, address.port, oper, timeVal / 1000, connID)), timeVal); }); return Promise.race([asyncPromise, timeoutPromise]).then((result) => { clearTimeout(timer); return result; }).catch((err) => { clearTimeout(timer); throw err; }); } /** * Network Session. This will be used for communication with the server. * @param {object} userConfig Connection options */ class NetworkSession { constructor() { this.connected = false; this.isBreak = false; this.isReset = false; this.breakPosted = false; this.compressionEnabled = false; } async getAddress(addressNode, userConfig) { /* Get the next address */ const address = await addressNode.execute(userConfig); /* Prepare connection attributes */ const uuid = this.sAtts ? this.sAtts.uuid : null; this.sAtts = new SessionAtts(uuid); this.sAtts.setFrom(userConfig); this.sAtts.setFrom(address.desc.params); /* Resolve attributes from Connect String */ await this.sAtts.prepare(address.protocol, userConfig); /* Insert Connection ID */ const rootNVPair = createNVPair(address.CNdata.join("")); const cdataNVPair = findNVPairRecurse(rootNVPair, "CONNECT_DATA"); const connidStr = `(CONNECTION_ID=${this.sAtts.connectionId})`; const childNVPair = createNVPair(connidStr); cdataNVPair.addListElement(childNVPair); this.cData = Buffer.from(rootNVPair.toString(), 'ascii'); this.cDataNVPair = rootNVPair; this.sAtts.nt.cDataNVPair = rootNVPair; return (address); } /** * Make the transport level connection */ async transportConnect(address) { if (address.protocol.toUpperCase() == 'TCP' && address.httpsProxy) { errors.throwErr(errors.ERR_INVALID_CONNECT_STRING_PARAMETERS, 'https proxy requires protocol as', 'tcps '); } if (address.protocol && (address.protocol.toUpperCase() == 'TCP' || address.protocol.toUpperCase() == 'TCPS')) { this.ntAdapter = new NTTCP(this.sAtts.nt); } else { errors.throwErr(errors.ERR_INVALID_CONNECT_STRING_PARAMETERS, address.protocol + " protocol not supported"); } await this.ntAdapter.connect(address); this.ntAdapter.startRead(); this.sAtts.ntCha = this.ntAdapter.cha; this.sndDatapkt = new Packet.DataPacket(this.sAtts.largeSDU); this.rcvDatapkt = new Packet.DataPacket(this.sAtts.largeSDU); } /** * Send the NSPTCN(connect) packet * @param {object} connectPkt Connect Packet */ _sendConnect(connectPkt) { this.sendPacket(connectPkt.buf); if (connectPkt.overflow) { this._send(connectPkt.connectData, 0, connectPkt.connectDataLen); this.flush(); } } /** * Establish network session. Make transport level connection, send * NSPTCN(connect packet) and read the response. @returns NetError. * (connection successfully established(NetError.CONNECTED) * or reason for failure) */ async connect2(address, userConfig) { /* Sanitise SDU */ if (this.sAtts.sdu) { if (this.sAtts.sdu > constants.NSPABSSDULN) { this.sAtts.sdu = constants.NSPABSSDULN; } else if (this.sAtts.sdu < constants.NSPMNSDULN) { this.sAtts.sdu = constants.NSPMNSDULN; } } else { this.sAtts.sdu = constants.NSPDFSDULN; } /* Transport connect */ if (this.sAtts.transportConnectTimeout) { const asyncPromise = this.transportConnect(address); await timeout(asyncPromise, this.sAtts.transportConnectTimeout, "transportConnectTimeout", address, this.sAtts.connectionId); } else { await this.transportConnect(address); } /* Send the connect packet */ let connectPkt = new Packet.ConnectPacket(this.cData, this.sAtts); this._sendConnect(connectPkt); /* Read the response */ while (true) { // eslint-disable-line const packet = await this._recvPacket(); if (packet.type === constants.NSPTAC) /* ACCEPT */ break; if (packet.type === constants.NSPTRF) { /* REFUSE */ if (this.refusePkt.overflow) { await this._recvPacket(); this.refusePkt.dataBuf = this.rcvDatapkt.buf.subarray(this.rcvDatapkt.offset, this.rcvDatapkt.len).toString(); } const nvpair = createNVPair(this.refusePkt.dataBuf); this.refusePkt = null; const err = findValue(nvpair, ["DESCRIPTION", "ERR"]); if (err == "12514") { errors.throwErr(errors.ERR_INVALID_SERVICE_NAME, this.getOption(constants.SVCNAME), address.host, address.port, this.sAtts.connectionId); } else if (err == "12505") { errors.throwErr(errors.ERR_INVALID_SID, this.getOption(constants.SID), address.host, address.port, this.sAtts.connectionId); } else if (err) { errors.throwErr(errors.ERR_CONNECTION_REFUSED, address.host, address.port, this.sAtts.connectionId, "ORA-" + err); } else { errors.throwErr(errors.ERR_CONNECTION_REFUSED, address.host, address.port, this.sAtts.connectionId, "refused"); } } else if (packet.type === constants.NSPTRS) { /* RESEND */ if ((packet.flags & constants.NSPFSRN) == constants.NSPFSRN) { await this.ntAdapter.renegTLS(); this.ntAdapter.startRead(); } this._sendConnect(connectPkt); } else if (packet.type === constants.NSPTRD) { /* REDIRECT */ let adrLen, adrStr, redirConnData; /* Read and connect to Redirect address */ if (this.redirectPkt.overflow) { await this._recvPacket(); this.redirectPkt.dataBuf = this.rcvDatapkt.buf.subarray(this.rcvDatapkt.offset, this.rcvDatapkt.len); } if (this.redirectPkt.flags & constants.NSPFRDS) { adrLen = this.redirectPkt.dataBuf.indexOf('\0', 0, 'ascii'); adrStr = this.redirectPkt.dataBuf.toString('ascii', 0, adrLen); redirConnData = this.redirectPkt.dataBuf.subarray(adrLen + 1, this.redirectPkt.dataLen); } else { adrStr = this.redirectPkt.dataBuf.toString('ascii'); redirConnData = this.cData; } const redirAddressNode = await resolveAddress(adrStr, userConfig); const host = address.hostname; address = await redirAddressNode.execute(); if (address.desc) this.sAtts.setFrom(address.desc.params); /* Add on attributes from redirect connect String */ address.originHost = host; this.redirectPkt = null; this.ntAdapter.disconnect(constants.NSFIMM); if (this.sAtts.transportConnectTimeout) { const asyncPromise = this.transportConnect(address); await timeout(asyncPromise, this.sAtts.transportConnectTimeout, "transportConnectTimeout", address, this.sAtts.connectionId); } else { await this.transportConnect(address); } connectPkt = new Packet.ConnectPacket(redirConnData, this.sAtts, constants.NSPFRDR); this.sndDatapkt = new Packet.DataPacket(this.sAtts.largeSDU); this._sendConnect(connectPkt); } } /* Accepted */ this.connected = true; this.cData = null; this.sndDatapkt = new Packet.DataPacket(this.sAtts.largeSDU); this.markerPkt = new Packet.MarkerPacket(this.sAtts.largeSDU); this.controlPkt = new Packet.ControlPacket(); this.ntAdapter.largeSDU = this.sAtts.largeSDU; this.sAtts.clearWallet(); this.sAtts.nt.walletPassword = null; if (!this.sAtts.noNA) { const NAContext = new ANO(this.sAtts); const buf = NAContext.sendPacket(); this._send(buf, 0, buf.length); this.flush(); const packet = await this._recvPacket(); NAContext.processPacket(packet.buf); } this.sndDatapkt.createPacket(constants.NSPDADAT); //Currently only used for disconnect this.sndDatapkt.offset = this.sndDatapkt.dataPtr; this.sndDatapkt.len = this.sndDatapkt.bufLen; return (true); } /** * Try all available addresses for connection establishment */ async connect1(address, addressNode, userConfig) { let connected, savedErr; do { try { if (this.sAtts.connectTimeout) { const asyncPromise = this.connect2(address, userConfig); connected = await timeout(asyncPromise, this.sAtts.connectTimeout, "connectTimeout", address, this.sAtts.connectionId); } else { connected = await this.connect2(address, userConfig); } } catch (err) { if (err.message.startsWith('NJS-510') && !this.ntAdapter.connected) { downHostInstance.markDownHost(address.host, Date.now()); // mark the host as down this.ntAdapter.connected = true; // Pretend as connected } if (this.ntAdapter) { this.ntAdapter.disconnect(constants.NSFIMM); this.ntAdapter = null; } this.sAtts.clearWallet(); connected = false; savedErr = err; try { address = await this.getAddress(addressNode, userConfig); } catch (err) { break; } } } while (!connected); if (connected) { return; } else { throw (savedErr); } } /** * Process packet (Internal) */ _processPacket(packet) { switch (packet.type) { case constants.NSPTDA: { /* DATA packet */ const size = packet.buf.length; const dataFlags = packet.buf.readUInt16BE(constants.NSPDAFLG); if ((dataFlags & constants.NSPDAFCMP) != 0) { // compression enabled const packetHeader = packet.buf.subarray(0, constants.NSPDADAT); const buffertoDeCompress = packet.buf.subarray(constants.NSPDADAT, size); let deCompressedDataBuffer; try { if (this.sAtts.firstCompressedPacket) { deCompressedDataBuffer = zlib.inflateSync(buffertoDeCompress, {finishFlush: zlib.constants.Z_SYNC_FLUSH}); this.sAtts.firstCompressedPacket = 0; } else { deCompressedDataBuffer = zlib.inflateRawSync(buffertoDeCompress, {finishFlush: zlib.constants.Z_SYNC_FLUSH}); } } catch (err) { errors.throwErr(errors.ERR_DATA_COMPRESSION, err.message); } const resultLength = deCompressedDataBuffer.length; //concatenate packet header with decompressed data packet.buf = Buffer.concat([packetHeader, deCompressedDataBuffer]); const length = resultLength + constants.NSPDADAT; if (this.sAtts.largeSDU) { packet.buf.writeUInt32BE(length, constants.NSPHDLEN); } else { packet.buf.writeUInt16BE(length, constants.NSPHDLEN); } } this.rcvDatapkt.fromPacket(packet); break; } case constants.NSPTMK: /* MARKER packet */ this.markerPkt.fromPacket(packet, this); break; case constants.NSPTCNL: /* CONTROL packet */ this.controlPkt.fromPacket(packet); break; case constants.NSPTAC: /* ACCEPT */ Packet.AcceptPacket(packet, this.sAtts); if (this.sAtts.version >= constants.TNS_VERSION_MIN_END_OF_RESPONSE && (packet.flags & constants.TNS_ACCEPT_FLAG_HAS_END_OF_REQUEST)) { this.endOfRequestSupport = true; } if (packet.flags & constants.TNS_ACCEPT_FLAG_FAST_AUTH) { this.supportsFastAuth = true; } break; case constants.NSPTRF: /* REFUSE */ this.refusePkt = new Packet.RefusePacket(packet); break; case constants.NSPTRS: /* RESEND */ break; case constants.NSPTRD: /* REDIRECT */ this.redirectPkt = new Packet.RedirectPacket(packet); break; default: errors.throwErr(errors.ERR_CONNECTION_INVALID_PACKET); } } /** * Receive packet (Internal) * Control packets are consumed internally and discarded */ async _recvPacket() { while (true) { // eslint-disable-line const packet = await this.ntAdapter.receive(); if (!packet) break; this._processPacket(packet); if (packet.type !== constants.NSPTCNL) { return packet; } } } /** * Send data (Internal) */ sendPacket(buf) { const packetType = buf.readUInt8(constants.NSPHDTYP); // only data packets need to be compressed if (packetType == constants.NSPTDA) { const size = buf.length; let dataFlags = buf.readUInt16BE(constants.NSPDAFLG); if (this.sAtts.networkCompressionEnabled && size > this.sAtts.networkCompressionThreshold) { this.compressionEnabled = true; const buffertoCompress = buf.subarray(constants.NSPDADAT, size); const bufferHeader = buf.subarray(0, constants.NSPDADAT); let compressedDataBuffer; try { compressedDataBuffer = zlib.deflateSync(buffertoCompress, {finishFlush: zlib.constants.Z_SYNC_FLUSH}); } catch (err) { errors.throwErr(errors.ERR_DATA_COMPRESSION, err.message); } const resultLength = compressedDataBuffer.length; if (resultLength < size - constants.NSPDADAT) { dataFlags |= constants.NSPDAFCMP; // concatenate buffer header with the compressed data buf = Buffer.concat([bufferHeader, compressedDataBuffer]); buf.writeUInt16BE(dataFlags, constants.NSPDAFLG); const pktOffset = resultLength + constants.NSPDADAT; if (this.sAtts.largeSDU) { buf.writeUInt32BE(pktOffset, constants.NSPHDLEN); } else { buf.writeUInt16BE(pktOffset, constants.NSPHDLEN); } } } } this.ntAdapter.send(buf); } /** * Break ongoing operation */ sendBreak() { if (this.isBreak) return; /* Already in a break */ if (!this.connected) { this.isBreak = true; /* Not yet connected. Post the break */ this.breakPosted = true; return; } this.isBreak = true; this.markerPkt.prepare(constants.NSPMKTD1, constants.NIQIMARK); this.sendPacket(this.markerPkt.buf); } /** * Reset the connection */ async reset() { /* If posted send Break */ if (this.breakPosted) { this.markerPkt.prepare(constants.NSPMKTD1, constants.NIQBMARK); this.sendPacket(this.markerPkt.buf); this.breakPosted = false; } /* Send Reset */ this.markerPkt.prepare(constants.NSPMKTD1, constants.NIQRMARK); this.sendPacket(this.markerPkt.buf); /* Await Reset */ while (!this.isReset) { await this._recvPacket(); } /* reset packet buffers */ this.sndDatapkt.dataPtr = this.sndDatapkt.dataLen = constants.NSPDADAT; this.sndDatapkt.offset = this.sndDatapkt.dataPtr; this.sndDatapkt.len = this.sndDatapkt.bufLen; this.isBreak = this.isReset = false; } /** * Receive packet */ async recvPacket() { return await this._recvPacket(); } syncRecvPacket() { while (this.ntAdapter.packets.length > 0) { const packet = this.ntAdapter.syncReceive(); if (!packet) break; this._processPacket(packet); if (packet.type !== constants.NSPTCNL) return packet; } } /** * Send data * @param {Buffer} userBuf User provided buffer * @param {*} offset from which to send data * @param {*} len number of bytes to send */ _send(userBuf, offset, len) { if (this.isBreak) { return; } let bytesCopied = 0; this.sndDatapkt.dataLen = this.sndDatapkt.offset; if (this.sndDatapkt.dataLen < this.sndDatapkt.bufLen || !this.sndDatapkt.bufLen) { bytesCopied = this.sndDatapkt.fillBuf(userBuf, offset, len); len -= bytesCopied; offset += bytesCopied; this.sndDatapkt.offset = this.sndDatapkt.dataLen; } while (len) { this.sendPacket(this.sndDatapkt.dataBuf); /* If break throw error now */ if (this.isBreak) { return; } this.sndDatapkt.dataLen = this.sndDatapkt.dataPtr; this.sndDatapkt.offset = this.sndDatapkt.dataPtr; bytesCopied = this.sndDatapkt.fillBuf(userBuf, offset, len); len -= bytesCopied; offset += bytesCopied; this.sndDatapkt.offset = this.sndDatapkt.dataLen; } } /** * Flush send buffers */ flush() { if (this.isBreak) { return; } this.sndDatapkt.dataLen = this.sndDatapkt.offset; this.sndDatapkt.prepare2Send(); this.sendPacket(Buffer.from(this.sndDatapkt.dataBuf)); this.sndDatapkt.dataLen = this.sndDatapkt.dataPtr; this.sndDatapkt.offset = this.sndDatapkt.dataPtr; } /** * Establish network connection */ async connect(userConfig) { const connStr = userConfig.connectString ? userConfig.connectString : userConfig.connectionString; let addressNode; if (userConfig._connInfo) { addressNode = userConfig._connInfo[5]; addressNode.reset(); } else { addressNode = await resolveAddress(connStr, userConfig); } let address; try { address = await this.getAddress(addressNode, userConfig); } catch (err) { if (err.message == "All options tried") /* Not even one valid address */ errors.throwErr(errors.ERR_HOST_NOT_FOUND); else errors.throwErr(errors.ERR_INVALID_CONNECT_STRING_PARAMETERS, err.message); } await this.connect1(address, addressNode, userConfig); } /** * Force Disconnect the stream, primarily used * to disconnect dead/hung connections. */ forceDisconnect(err) { if (!this.connected) { return; } this.ntAdapter.stream.destroy(err); this.connected = false; } /** * Disconnect * @param {int} type of disconnect */ disconnect(type) { if (!this.connected) { return; } if (type != constants.NSFIMM && !this.ntAdapter.err) { /* Send EOF packet */ this.sndDatapkt.dataLen = this.sndDatapkt.offset; this.sndDatapkt.prepare2Send(constants.NSPDAFEOF); this.sendPacket(this.sndDatapkt.dataBuf); } this.ntAdapter.disconnect(type); this.ntAdapter = null; this.connected = false; } /** * Get connection attributes * @param {int} opcode type of attribute * @returns attribute value */ getOption(opcode) { switch (opcode) { case constants.NS_MOREDATA: /* Is there more data in read buffers */ return (this.ntAdapter.packets.length > 0); case constants.SVCNAME: /* Service name */ return findValue(this.cDataNVPair, ["DESCRIPTION", "CONNECT_DATA", "SERVICE_NAME"]); case constants.SID: /* Service name */ return findValue(this.cDataNVPair, ["DESCRIPTION", "CONNECT_DATA", "SID"]); case constants.SERVERTYPE: /* Server type */ return findValue(this.cDataNVPair, ["DESCRIPTION", "CONNECT_DATA", "SERVER"]); case constants.REMOTEADDR: /* Peer address */ if (this.ntAdapter) { return this.ntAdapter.getOption(opcode); // Pass through to NT } else { return null; } case constants.CONNCLASS: /* Connection Class */ return findValue(this.cDataNVPair, ["DESCRIPTION", "CONNECT_DATA", "POOL_CONNECTION_CLASS"]); case constants.PURITY: /* Purity */ return findValue(this.cDataNVPair, ["DESCRIPTION", "CONNECT_DATA", "POOL_PURITY"]); case constants.HEALTHCHECK: /* Is connection healthy */ return (this.connected && this.ntAdapter.connected && !this.ntAdapter.err); default: errors.throwErr(errors.ERR_INTERNAL, "getOption not supported for opcode " + opcode); } } /** * receive inband notification * @param {Object} obj Return the notification into user provided object * @returns Error number sent from server, or error on the connection. * returns 0 if healthy connection */ recvInbandNotif() { let error = 0; if (this.controlPkt.errno) { /* Control pkt already read */ error = this.controlPkt.errno; return (error); } else if (!this.getOption(constants.HEALTHCHECK)) { return errors.ERR_CONNECTION_CLOSED; } else { if (this.getOption(constants.NS_MOREDATA)) { //More data available const packet = this.ntAdapter.syncReceive(); if (packet.type == constants.NSPTCNL) { this.controlPkt.fromPacket(packet); error = this.controlPkt.errno; return (error); } else { this.ntAdapter.packets.unshift(packet); /* Push packet back */ return (0); } } else return (0); } } } module.exports = { NetworkSession, resolveAddress, getConnectionInfo };