oracledb
Version:
A Node.js module for Oracle Database access from JavaScript and TypeScript
610 lines (558 loc) • 18.5 kB
JavaScript
// 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 { Buffer } = require('buffer');
const net = require("net");
const process = require("process");
const tls = require("tls");
const http = require("http");
const Timers = require('timers');
const constants = require("./constants.js");
const errors = require("../../errors.js");
const { findValue, findNVPair } = require("./nvStrToNvPair.js");
const PACKET_HEADER_SIZE = 8;
const DEFAULT_PORT = 1521;
const DEFAULT_HTTPS_PROXY_PORT = 80;
/* Protocol characteristics */
const TCPCHA = 1 << 1 | /* ASYNC support */
1 << 2 | /* Callback support */
1 << 3 | /* More Data support */
1 << 8 | /* Read/Write Readiness support */
1 << 9 | /* Full Duplex support */
1 << 12; /* SIGPIPE Support */
const sniAllowedCDParams = ["SERVICE_NAME", "INSTANCE_NAME", "SERVER", "COLOCATION_TAG", "CONNECTION_ID", "POOL_BOUNDARY",
"POOL_PURITY", "POOL_CONNECTION_CLASS", "POOL_NAME", "SERVICE_TAG", "CID"];
const sniParams = ["SERVICE_NAME", "INSTANCE_NAME", "SERVER", "COLOCATION_TAG"];
const sniMap = ['S', 'I', 'T', 'C'];
const SNI_MAX_BYTES = 256;
let streamNum = 1;
/**
* Network Transport TCP/TCPS adapter
* @param {Address} address Destination Address
* @param {Object} atts Transport Attributes
*/
class NTTCP {
constructor(atts) {
this.atts = atts;
this.cha = TCPCHA;
this.connected = false;
this.err = false;
this.needsDrain = false;
this.numPacketsSinceLastWait = 0;
this.secure = false;
this.largeSDU = false;
this.streamNum = streamNum++;
this.packetNum = 1;
this.doDNMatch = true;
}
/**
* DN matching function(used with TLS)
*/
dnMatch(serverName, cert) {
if (this.atts.sslServerDNMatch && this.doDNMatch) {
const toObject = str =>str .split(',').map(x => x.split('=').map(y => y.trim())).reduce((a, x) => {
a[x[0]] = x[1];
return a;
}, {});
if (this.atts.sslServerCertDN) { /* Full DN Match */
const obj = toObject(this.atts.sslServerCertDN);
if (Object.keys(obj).length == Object.keys(cert.subject).length) {
for (const key in obj) {
if (obj[key] != cert.subject[key]) {
return (errors.getErr(errors.ERR_TLS_DNMATCH_FAILURE));
}
}
} else {
return (errors.getErr(errors.ERR_TLS_DNMATCH_FAILURE));
}
} else {
if (tls.checkServerIdentity(this.hostName, cert) && (!this.originHost || tls.checkServerIdentity(this.originHost, cert))) { /* Hostname match */
if (this.atts.sslAllowWeakDNMatch) {
const serviceName = findValue(this.atts.cDataNVPair, ["DESCRIPTION", "CONNECT_DATA", "SERVICE_NAME"]); /* Service Name match */
if (serviceName != cert.subject.CN) {
return (errors.getErr(errors.ERR_TLS_DNMATCH_FAILURE));
}
} else {
const hostName = this.hostName + " " + (this.originHost ? "or " + this.originHost : "");
return (errors.getErr(errors.ERR_TLS_HOSTMATCH_FAILURE, hostName));
}
}
}
}
}
/**
* TLS connection establishment
* @returns Promise
*/
async tlsConnect(secureContext, connStream) {
this.stream.removeAllListeners();
let connectErrCause, sni = null;
if (this.atts.useSNI)
sni = this.generateSNI();
const tlsOptions = {
host: this.host,
socket: connStream,
servername: sni,
rejectUnauthorized: true,
secureContext: secureContext,
enableTrace: false,
checkServerIdentity: this.dnMatch.bind(this)
};
await new Promise((resolve) => {
this.stream = tls.connect(tlsOptions, () => {
if (!this.stream.authorized) {
connectErrCause = "server certificate unauthorized";
}
resolve();
}).on('error', (err) => {
connectErrCause = err.message;
resolve();
});
});
if (connectErrCause)
errors.throwErr(errors.ERR_TLS_AUTH_FAILURE, this.host, this.port, this.atts.connectionId, connectErrCause);
this.connStream = connStream;
}
/**
* TCP connection establishment
* @returns Promise
*/
async ntConnect(address) {
if (!address.port) {
address.port = DEFAULT_PORT;
}
let connectErrCause, proxyConnectErrCause, req;
const httpsProxy = address.httpsProxy || this.atts.httpsProxy;
let httpsProxyPort = address.httpsProxyPort || this.atts.httpsProxyPort;
await new Promise((resolve) => {
if (httpsProxy) {
if (!httpsProxyPort) {
httpsProxyPort = DEFAULT_HTTPS_PROXY_PORT;
}
req = http.request({
host: httpsProxy,
port: httpsProxyPort,
method: 'CONNECT',
path: address.host + ':' + address.port,
});
req.once('connect', (res, socket) => {
if (res.statusCode == 200) {
this.connected = true;
this.stream = socket;
} else {
proxyConnectErrCause = res.statusCode;
}
resolve();
});
req.once('error', (err) => {
proxyConnectErrCause = err.message;
resolve();
});
req.end();
} else {
this.stream = net.connect(address.port, address.host, () => {
this.connected = true;
resolve();
});
this.stream.once('error', (err) => {
connectErrCause = err.message;
resolve();
});
}
});
if (req)
req.removeAllListeners();
if (!this.connected) {
if (proxyConnectErrCause) {
errors.throwErr(errors.ERR_PROXY_CONNECTION_FAILURE, httpsProxy, httpsProxyPort, this.atts.connectionId, proxyConnectErrCause);
} else {
errors.throwErr(errors.ERR_CONNECTION_INCOMPLETE, this.host, this.port, this.atts.connectionId, connectErrCause);
}
}
}
/**
* Network Transport connection establishment
* @returns Promise
*/
async connect(address) { /* Connect function for TCP sockets */
this.originHost = address.originHost;
this.host = address.host;
this.hostName = address.hostname;
this.port = address.port;
try {
await this.ntConnect(address);
if (this.atts.expireTime || this.atts.enableDCD) { /* Set KeepAlives */
if (this.atts.expireTime) {
this.stream.setKeepAlive(true, this.atts.expireTime);
} else {
this.stream.setKeepAlive(true);
}
}
if (this.atts.tcpNoDelay) { /* Turn off Nagle's unless explicitly enabled by user */
this.stream.setNoDelay(true);
}
if (address.protocol.toUpperCase() == "TCPS") {
let secureContext;
this.secure = true;
if (this.atts.sslAllowWeakDNMatch)
this.doDNMatch = false; //Don't match initial connect
try {
secureContext = tls.createSecureContext({
cert: this.atts.wallet,
key: this.atts.wallet,
passphrase: this.atts.walletPassword,
ca: this.atts.wallet,
});
} catch (err) {
errors.throwErr(errors.ERR_TLS_INIT_FAILURE, err.message);
}
await this.tlsConnect(secureContext, this.stream);
}
} finally {
if (this.stream) {
this.setupEventHandlers();
}
}
}
/**
* Disconnect Network Transoprt
* @param {int} type
* @returns Promise
*/
disconnect(type) { /* Disconnect function for TCP sockets */
if (this.connected && !this.err) {
if (type == constants.NSFIMM)
this.stream.destroy();
else
this.stream.end();
}
this.stream = null;
this.connected = false;
this.drainWaiter = null;
this.readWaiter = null;
}
/**
* Get the string containing a packet dump.
* @param {Buffer} buffer containing packet data
*/
getPacketDump(buffer) {
const lines = [];
for (let i = 0; i < buffer.length; i += 8) {
const address = i.toString().padStart(4, '0');
const block = buffer.slice(i, i + 8);
const hexDumpValues = [];
const printableValues = [];
for (const hexByte of block) {
hexDumpValues.push(hexByte.toString(16).toUpperCase().padStart(2, '0'));
if (hexByte > 0x20 && hexByte < 0x7f) {
printableValues.push(String.fromCharCode(hexByte));
} else {
printableValues.push(".");
}
}
while (hexDumpValues.length < 8) {
hexDumpValues.push(" ");
printableValues.push(" ");
}
const hexValuesBlock = hexDumpValues.join(" ");
const printableBlock = printableValues.join("");
lines.push(`${address} : ${hexValuesBlock} |${printableBlock}|`);
}
return lines.join("\n");
}
/**
* Print the packet to the console.
* @param {String} operation which was performed
* @param {Buffer} buffer containing packet data
*/
printPacket(operation, buffer) {
const now = new Date();
const formattedDate =
`${now.getFullYear()}-${now.getMonth().toString().padStart(2, '0')}-` +
`${now.getDay().toString().padStart(2, '0')} ` +
`${now.getHours().toString().padStart(2, '0')}:` +
`${now.getMinutes().toString().padStart(2, '0')}:` +
`${now.getSeconds().toString().padStart(2, '0')}.` +
`${now.getMilliseconds().toString().padStart(3, '0')}`;
const packetDump = this.getPacketDump(buffer);
console.log(`${formattedDate} ${operation}:\n${packetDump}\n`);
}
/**
* Check for errors
*/
checkErr() {
if (!this.connected || this.err) {
let err;
if (this.savedErr) {
err = errors.getErr(errors.ERR_CONNECTION_LOSTCONTACT,
this.host, this.port, this.atts.connectionId, this.savedErr.message);
} else {
err = errors.getErr(errors.ERR_CONNECTION_EOF, this.host, this.port, this.atts.connectionId,);
}
/* Wrap around NJS-500 */
const newErr = errors.getErr(errors.ERR_CONNECTION_CLOSED);
newErr.message = newErr.message + "\n" + err.message;
throw (newErr);
}
}
/**
* Transport Send
* @param {Buffer} buf Buffer to send
* @returns Promise
*/
send(buf) {
this.checkErr();
if (process.env.NODE_ORACLEDB_DEBUG_PACKETS)
this.printPacket(`Sending packet ${this.packetNum} on stream ${this.streamNum}`, buf);
const result = this.stream.write(buf, (err) => {
if (err) {
this.savedErr = err;
this.err = true;
this._notifyWaiters();
}
});
if (!result) {
this.needsDrain = true;
}
this.numPacketsSinceLastWait++;
this.packetNum++;
}
/**
* Should writing to the transport be paused? This occurs if draining is
* required or if the number of packets written since the last pause exceeds
* 100 (in order to avoid starvation of the event loop during large writes).
*/
shouldPauseWrite() {
return (this.needsDrain || this.numPacketsSinceLastWait >= 100);
}
/**
* Perform a wait -- if draining is required, then until the drain event is
* emitted or if draining is not required, then a simple setImmediate() that
* ensures that the event loop is not starved.
*/
async pauseWrite() {
this.checkErr();
if (this.needsDrain) {
await new Promise((resolve) => {
this.drainWaiter = resolve;
});
this.checkErr();
} else {
await new Promise((resolve) => Timers.setImmediate(resolve));
}
this.numPacketsSinceLastWait = 0;
}
/**
* Start Async reads
*/
startRead() {
let tempBuf;
this.packets = [];
this.stream.on('data', (chunk) => {
// append buffer if previous chunk(s) were insufficient for a full packet
if (tempBuf) {
tempBuf = Buffer.concat([tempBuf, chunk]);
} else {
tempBuf = chunk;
}
while (tempBuf.length >= PACKET_HEADER_SIZE) {
// determine the length of the packet
let len;
if (this.largeSDU) {
len = tempBuf.readUInt32BE();
} else {
len = tempBuf.readUInt16BE();
}
// not enough for a full packet so wait for more data to arrive
if (len > tempBuf.length)
break;
// enough for a full packet, extract details from the packet header
// and pass them along for processing
const packet = {
buf: tempBuf.subarray(0, len),
type: tempBuf[4],
flags: tempBuf[5],
num: this.packetNum++
};
this.packets.push(packet);
if (this.readWaiter) {
this.readWaiter();
this.readWaiter = null;
}
if (process.env.NODE_ORACLEDB_DEBUG_PACKETS)
this.printPacket(`Receiving packet ${packet.num} on stream ${this.streamNum}`, packet.buf);
// if the packet consumed all of the bytes (most common scenario), then
// simply clear the temporary buffer; otherwise, retain whatever bytes
// are unused and see if sufficient data is available for another
// packet
if (len === tempBuf.length) {
tempBuf = null;
break;
} else {
tempBuf = tempBuf.subarray(len);
}
}
});
}
/**
* Synchronous receive
* @returns a single packet or undefined if no packets are available
*/
syncReceive() {
return this.packets.shift();
}
/**
* Asynchronous receive
* @returns a single packet
*/
async receive() {
if (this.packets.length === 0) {
this.checkErr();
await new Promise((resolve) => {
this.readWaiter = resolve;
this.numPacketsSinceLastWait = 0;
});
this.checkErr();
}
return this.packets.shift();
}
/**
* TLS renegotiate
* @returns Promise
*/
async renegTLS() {
let initTLSDone = false;
this.checkErr();
try {
this.doDNMatch = true;
const secureContext = tls.createSecureContext({
cert: this.atts.wallet,
key: this.atts.wallet,
passphrase: this.atts.walletPassword,
ca: this.atts.wallet,
});
initTLSDone = true;
await this.tlsConnect(secureContext, this.connStream);
} catch (err) {
if (!initTLSDone)
errors.throwErr(errors.ERR_TLS_INIT_FAILURE, err.message);
else throw err;
} finally {
this.setupEventHandlers();
}
}
/**
* Setup handling of events
*/
setupEventHandlers() {
this.stream.removeAllListeners();
this.stream.on('error', (err) => {
this.savedErr = err;
this.err = true;
this._notifyWaiters();
});
this.stream.on('end', () => {
this.err = true;
this._notifyWaiters();
});
this.stream.on('close', () => {
this.connected = false;
this._notifyWaiters();
});
this.stream.on('drain', () => {
this.needsDrain = false;
if (this.drainWaiter) {
this.drainWaiter();
this.drainWaiter = null;
}
});
}
/**
* Get Transport Attributes
* @param {int} opcode type of attribute
* @returns attribute value
*/
getOption(opcode) {
this.checkErr();
switch (opcode) {
case constants.NT_MOREDATA: /* More data available to read */
return (this.packets.length > 0);
case constants.REMOTEADDR: /* Remote Address */
{
const socket = this.secure ? this.connStream : this.stream;
return (socket.remoteAddress + ":" + socket.remotePort);
}
default:
errors.throwErr(errors.ERR_INTERNAL, "getOption not supported for opcode " + opcode);
}
}
/**
* Notify the waiters (drain and read) and reset them, if applicable.
*/
_notifyWaiters() {
if (this.drainWaiter) {
this.drainWaiter();
this.drainWaiter = null;
}
if (this.readWaiter) {
this.readWaiter();
this.readWaiter = null;
}
}
/**
* Generate SNI data.
*/
generateSNI() {
/* No SNI if source route is set */
if ((findValue(this.atts.cDataNVPair, ["DESCRIPTION", "SOURCE_ROUTE"]) == "yes") ||
(findValue(this.atts.cDataNVPair, ["DESCRIPTION", "ADDRESS_LIST", "SOURCE_ROUTE"]) == "yes"))
return null;
const cdnvp = findNVPair(this.atts.cDataNVPair, "CONNECT_DATA");
/* Loop through the list of params */
for (let i = 0; i < cdnvp.getListSize(); i++) {
const child = cdnvp.getListElement(i);
if (!sniAllowedCDParams.includes(child.name.toUpperCase()))
return null; /* No SNI for unsupported Connect Data params */
}
/* Generate SNI */
let value, sni = "";
for (let i = 0; i < sniParams.length; i++) {
if ((value = findValue(this.atts.cDataNVPair, ["DESCRIPTION", "CONNECT_DATA", sniParams[i]]))) {
if (sniParams[i] == 'SERVER') /* For server type just pick the first letter */
sni += sniMap[i] + "1" + "." + value[0] + ".";
else
sni += sniMap[i] + value.length + "." + value + ".";
}
}
sni += "V3." + constants.TNS_VERSION_DESIRED; /* Version */
const match_pattern = new RegExp("^[A-Za-z0-9._-]+$");
if (!(sni.match(match_pattern)))
return null; /* No SNI if special characters are present */
if (sni.length > SNI_MAX_BYTES)
return null; /* Max allowed length */
return sni;
}
}
module.exports = NTTCP;