Skip to content

Commit 0814380

Browse files
committed
Added support for SNI (Server Name Indicator), an Oracle Database 23.7 network optimization feature
1 parent 2a1e52f commit 0814380

File tree

9 files changed

+228
-8
lines changed

9 files changed

+228
-8
lines changed

doc/src/api_manual/oracledb.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2574,6 +2574,20 @@ Oracledb Methods
25742574
.. versionadded:: 5.2
25752575

25762576
The alias ``username``.
2577+
* - ``useSNI``
2578+
- Boolean
2579+
- Thin
2580+
- .. _createpoolpoolattrsusesni:
2581+
2582+
Enables the connection to use the TLS extension, Server Name Indication (SNI).
2583+
2584+
Usually, two TLS handshakes are required to establish a connection, one with the listener and the other with the server process. With useSNI, the connection information is sent in the SNI field which enables the listener to hand-off the connection to the appropriate server process without the listener having to perform a TLS handshake. SNI helps improve the connection establishment time.
2585+
2586+
The default is *False*.
2587+
2588+
This property requires Oracle Database 23.7 (or later).
2589+
2590+
.. versionadded:: 6.8
25772591

25782592
**createPool(): accessToken Object Properties**
25792593

@@ -3214,6 +3228,20 @@ Oracledb Methods
32143228
.. versionadded:: 5.2
32153229

32163230
The alias ``username``.
3231+
* - ``useSNI``
3232+
- Boolean
3233+
- Thin
3234+
- .. _getconnectiondbattrsusesni:
3235+
3236+
Enables the connection to use the TLS extension, Server Name Indication (SNI).
3237+
3238+
Usually, two TLS handshakes are required to establish a connection, one with the listener and the other with the server process. With useSNI, the connection information is sent in the SNI field which enables the listener to hand-off the connection to the appropriate server process without the listener having to perform a TLS handshake. SNI helps improve the connection establishment time.
3239+
3240+
The default is *False*.
3241+
3242+
This property requires Oracle Database 23.7 (or later).
3243+
3244+
.. versionadded:: 6.8
32173245

32183246
**getConnection(): accessToken Object Properties**
32193247

doc/src/release_notes.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ Common Changes
2828
Thin Mode Changes
2929
+++++++++++++++++
3030

31+
#) Added connection optimization feature which uses Server Name Indication (SNI)
32+
extension of the TLS protocol.
33+
3134
#) Added support for setting the :attr:`~oracledb.edition` when connecting to
3235
the database.
3336

doc/src/user_guide/appendix_a.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,9 @@ are in a ``tnsnames.ora`` file. All unrecognized parameters are ignored.
469469
* - POOL_CONNECTION_CLASS
470470
- :attr:`cclass <oracledb.connectionClass>`
471471
- Defines a logical name for connections.
472+
* - USE_SNI
473+
- :ref:`useSNI <getconnectiondbattrsusesni>`
474+
- Indicates whether the TLS extension, Server Name Indication (SNI), is enabled.
472475

473476
In node-oracledb Thick mode, the above values only work when connected to
474477
Oracle Database 21c or later.

lib/oracledb.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2015, 2024, Oracle and/or its affiliates.
1+
// Copyright (c) 2015, 2025, Oracle and/or its affiliates.
22

33
//-----------------------------------------------------------------------------
44
//
@@ -362,6 +362,13 @@ async function _verifyOptions(options, inCreatePool) {
362362
outOptions.terminal = options.terminal;
363363
}
364364

365+
// useSNI must be a boolean
366+
if (options.useSNI !== undefined) {
367+
errors.assertParamPropValue(typeof options.useSNI === 'boolean', 1,
368+
"useSNI");
369+
outOptions.useSNI = options.useSNI;
370+
}
371+
365372
// check pool specific options
366373
if (inCreatePool) {
367374

lib/thin/sqlnet/ezConnectResolver.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2022, 2024, Oracle and/or its affiliates.
1+
// Copyright (c) 2022, 2025, Oracle and/or its affiliates.
22

33
//-----------------------------------------------------------------------------
44
//
@@ -57,7 +57,7 @@ const EXT_PARAM_SEP = '&';
5757
const DESCRIPTION_PARAMS = ["ENABLE", "FAILOVER", "LOAD_BALANCE",
5858
"RECV_BUF_SIZE", "SEND_BUF_SIZE", "SDU",
5959
"SOURCE_ROUTE", "RETRY_COUNT", "RETRY_DELAY",
60-
"CONNECT_TIMEOUT", "TRANSPORT_CONNECT_TIMEOUT", "RECV_TIMEOUT"];
60+
"CONNECT_TIMEOUT", "TRANSPORT_CONNECT_TIMEOUT", "RECV_TIMEOUT", "USE_SNI"];
6161
/*
6262
DESCRIPTION
6363
This class takes care resolving the EZConnect format to Long TNS URL format.
@@ -459,6 +459,7 @@ class EZConnectResolver {
459459
aliasMap.set("service_tag", "SERVICE_TAG");
460460
aliasMap.set("connection_id_prefix", "CONNECTION_ID_PREFIX");
461461
aliasMap.set("pool_boundary", "POOL_BOUNDARY");
462+
aliasMap.set("use_sni", "USE_SNI");
462463
return aliasMap;
463464
}
464465
}module.exports = EZConnectResolver;

lib/thin/sqlnet/navNodes.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2022, 2024, Oracle and/or its affiliates.
1+
// Copyright (c) 2022, 2025, Oracle and/or its affiliates.
22

33
//-----------------------------------------------------------------------------
44
//
@@ -246,6 +246,10 @@ class Description {
246246
this.failover = (childnv.atom.toLowerCase() == "yes"
247247
|| childnv.atom.toLowerCase() == "on"
248248
|| childnv.atom.toLowerCase() == "true");
249+
} else if (childnv.name.toUpperCase() == "USE_SNI") {
250+
this.params.useSNI = (childnv.atom.toLowerCase() == "yes"
251+
|| childnv.atom.toLowerCase() == "on"
252+
|| childnv.atom.toLowerCase() == "true");
249253
} else if (childnv.name.toUpperCase() == "ADDRESS_LIST") {
250254
child = new NavAddressList();
251255
child.initFromNVPair(childnv);
@@ -715,6 +719,9 @@ class NavDescription extends Description {
715719
if ('enable' in this.params) {
716720
cs.sBuf.push("(ENABLE=" + this.params.enable + ")");
717721
}
722+
if ('useSNI' in this.params) {
723+
cs.sBuf.push("(USE_SNI=" + this.params.useSNI + ")");
724+
}
718725
if (('sslServerCertDN' in this.params) || ('sslServerDNMatch' in this.params) || ('walletLocation' in this.params) || ('sslAllowWeakDNMatch' in this.params)) {
719726
cs.sBuf.push("(SECURITY=");
720727
if ('sslServerCertDN' in this.params) {

lib/thin/sqlnet/ntTcp.js

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2022, 2024, Oracle and/or its affiliates.
1+
// Copyright (c) 2022, 2025, Oracle and/or its affiliates.
22

33
//-----------------------------------------------------------------------------
44
//
@@ -34,7 +34,7 @@ const http = require("http");
3434
const Timers = require('timers');
3535
const constants = require("./constants.js");
3636
const errors = require("../../errors.js");
37-
const { findValue } = require("./nvStrToNvPair.js");
37+
const { findValue, findNVPair } = require("./nvStrToNvPair.js");
3838

3939
const PACKET_HEADER_SIZE = 8;
4040
const DEFAULT_PORT = 1521;
@@ -48,6 +48,12 @@ const TCPCHA = 1 << 1 | /* ASYNC support */
4848
1 << 9 | /* Full Duplex support */
4949
1 << 12; /* SIGPIPE Support */
5050

51+
const sniAllowedCDParams = ["SERVICE_NAME", "INSTANCE_NAME", "SERVER", "COLOCATION_TAG", "CONNECTION_ID", "POOL_BOUNDARY",
52+
"POOL_PURITY", "POOL_CONNECTION_CLASS", "POOL_NAME", "SERVICE_TAG", "CID"];
53+
const sniParams = ["SERVICE_NAME", "INSTANCE_NAME", "SERVER", "COLOCATION_TAG"];
54+
const sniMap = ['S', 'I', 'T', 'C'];
55+
const SNI_MAX_BYTES = 256;
56+
5157
let streamNum = 1;
5258

5359
/**
@@ -113,10 +119,13 @@ class NTTCP {
113119
*/
114120
async tlsConnect(secureContext, connStream) {
115121
this.stream.removeAllListeners();
116-
let connectErrCause;
122+
let connectErrCause, sni = null;
123+
if (this.atts.useSNI)
124+
sni = this.generateSNI();
117125
const tlsOptions = {
118126
host: this.host,
119127
socket: connStream,
128+
servername: sni,
120129
rejectUnauthorized: true,
121130
secureContext: secureContext,
122131
enableTrace: false,
@@ -557,6 +566,44 @@ class NTTCP {
557566
}
558567
}
559568

569+
/**
570+
* Generate SNI data.
571+
*/
572+
generateSNI() {
573+
/* No SNI if source route is set */
574+
if ((findValue(this.atts.cDataNVPair, ["DESCRIPTION", "SOURCE_ROUTE"]) == "yes") ||
575+
(findValue(this.atts.cDataNVPair, ["DESCRIPTION", "ADDRESS_LIST", "SOURCE_ROUTE"]) == "yes"))
576+
return null;
577+
578+
const cdnvp = findNVPair(this.atts.cDataNVPair, "CONNECT_DATA");
579+
/* Loop through the list of params */
580+
for (let i = 0; i < cdnvp.getListSize(); i++) {
581+
const child = cdnvp.getListElement(i);
582+
if (!sniAllowedCDParams.includes(child.name.toUpperCase()))
583+
return null; /* No SNI for unsupported Connect Data params */
584+
}
585+
586+
/* Generate SNI */
587+
let value, sni = "";
588+
for (let i = 0; i < sniParams.length; i++) {
589+
if ((value = findValue(this.atts.cDataNVPair, ["DESCRIPTION", "CONNECT_DATA", sniParams[i]]))) {
590+
if (sniParams[i] == 'SERVER') /* For server type just pick the first letter */
591+
sni += sniMap[i] + "1" + "." + value[0] + ".";
592+
else
593+
sni += sniMap[i] + value.length + "." + value + ".";
594+
}
595+
}
596+
sni += "V3." + constants.TNS_VERSION_DESIRED; /* Version */
597+
598+
const match_pattern = new RegExp("^[A-Za-z0-9._-]+$");
599+
if (!(sni.match(match_pattern)))
600+
return null; /* No SNI if special characters are present */
601+
602+
if (sni.length > SNI_MAX_BYTES)
603+
return null; /* Max allowed length */
604+
605+
return sni;
606+
}
560607
}
561608

562609
module.exports = NTTCP;

lib/thin/sqlnet/sessionAtts.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2022, 2023, Oracle and/or its affiliates.
1+
// Copyright (c) 2022, 2025, Oracle and/or its affiliates.
22

33
//-----------------------------------------------------------------------------
44
//
@@ -60,6 +60,7 @@ class SessionAtts {
6060
this.uuid = uuid;
6161
this.nt.sslServerDNMatch = true;
6262
this.nt.sslAllowWeakDNMatch = false;
63+
this.nt.useSNI = false;
6364
}
6465

6566
/**
@@ -119,6 +120,9 @@ class SessionAtts {
119120
if (params.httpsProxyPort >= 0) {
120121
this.nt.httpsProxyPort = parseInt(params.httpsProxyPort);
121122
}
123+
if (typeof params.useSNI === 'boolean') {
124+
this.nt.useSNI = params.useSNI;
125+
}
122126
}
123127
}
124128

test/ext/sqlnet-tests/sniTest.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/* Copyright (c) 2025, Oracle and/or its affiliates. */
2+
3+
/******************************************************************************
4+
*
5+
* This software is dual-licensed to you under the Universal Permissive License
6+
* (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
7+
* 2.0 as shown at https://www.apache.org/licenses/LICENSE-2.0. You may choose
8+
* either license.
9+
*
10+
* If you elect to accept the software under the Apache License, Version 2.0,
11+
* the following applies:
12+
*
13+
* Licensed under the Apache License, Version 2.0 (the "License");
14+
* you may not use this file except in compliance with the License.
15+
* You may obtain a copy of the License at
16+
*
17+
* https://www.apache.org/licenses/LICENSE-2.0
18+
*
19+
* Unless required by applicable law or agreed to in writing, software
20+
* distributed under the License is distributed on an "AS IS" BASIS,
21+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22+
* See the License for the specific language governing permissions and
23+
* limitations under the License.
24+
*
25+
* NAME
26+
* 1. sniTest.js
27+
*
28+
* DESCRIPTION
29+
* This test checks that the SNI (Server Name Indication) extension is
30+
* correctly used when connecting to a TLS-enabled database. With
31+
* NODE_ORACLEDB_DEBUG_PACKETS=on, the packet dump should not show a
32+
* RESEND(NSPTRS) packet with SNI set to true.
33+
*
34+
* SNI can be set in dbconfig or the connection string.
35+
* To test SNI using connection string and config, you can try the following
36+
* - Set useSNI to true in dbconfig and USE_SNI = on in the connect string.
37+
* - Set useSNI to false in dbconfig and USE_SNI = off in the connect string.
38+
* - Set useSNI to false in dbconfig and USE_SNI = on in the connect string.
39+
* - Set useSNI to true in dbconfig and USE_SNI = off in the connect string.
40+
*
41+
*****************************************************************************/
42+
'use strict';
43+
const oracledb = require("oracledb");
44+
const assert = require('assert');
45+
const fs = require('fs').promises;
46+
const dbConfig = require('../../dbconfig.js');
47+
const testsUtil = require('../../testsUtil.js');
48+
const path = require('path');
49+
const os = require('os');
50+
51+
describe('1. SNI (Server Name Indication) test', function() {
52+
let isRunnable = false;
53+
before(async function() {
54+
isRunnable = await testsUtil.checkPrerequisites(2306000000, 2306000000);
55+
56+
if (!isRunnable || !oracledb.thin) this.skip();
57+
58+
process.env.NODE_ORACLEDB_DEBUG_PACKETS = 1; // Set to '1' to enable packet debugging
59+
});
60+
61+
after(function() {
62+
process.env.NODE_ORACLEDB_DEBUG_PACKETS = 0; // Set to '0' to disable packet debugging
63+
});
64+
65+
it('1.1 useSNI parameter set to true in the config', async function() {
66+
const tmpobj = await fs.mkdtemp(path.join(os.tmpdir(), 'tmp-'));
67+
const tmpfile = path.join(tmpobj, "sniTest.txt");
68+
const originalStdoutWrite = process.stdout.write;
69+
process.stdout.write = (string) => {
70+
fs.appendFile(tmpfile, string, (err) => {
71+
if (err) {
72+
console.error(err);
73+
}
74+
});
75+
originalStdoutWrite.apply(process.stdout, arguments);
76+
};
77+
dbConfig.useSNI = true;
78+
try {
79+
const conn = await oracledb.getConnection(dbConfig);
80+
const result = await conn.execute(`SELECT 'Hello World' FROM DUAL`);
81+
assert.strictEqual(result.rows[0][0], 'Hello World');
82+
await conn.close();
83+
process.stdout.write = originalStdoutWrite;
84+
const contents = await fs.readFile(tmpfile, {encoding: 'utf8'});
85+
assert(!contents.includes('Receiving packet 3 on stream 3:\n' +
86+
'0000 : 00 08 00 00 0B 08 00 00 |........|'));
87+
} finally {
88+
await fs.unlink(tmpfile);
89+
await fs.rmdir(tmpobj);
90+
}
91+
}); // 1.1
92+
93+
it('1.2 useSNI parameter set to false in the config', async function() {
94+
const tmpobj = await fs.mkdtemp(path.join(os.tmpdir(), 'tmp-'));
95+
const tmpfile = path.join(tmpobj, "sniTest.txt");
96+
const originalStdoutWrite = process.stdout.write;
97+
process.stdout.write = (string) => {
98+
fs.appendFile(tmpfile, string, (err) => {
99+
if (err) {
100+
console.error(err);
101+
}
102+
});
103+
originalStdoutWrite.apply(process.stdout, arguments);
104+
};
105+
dbConfig.useSNI = false;
106+
try {
107+
const conn = await oracledb.getConnection(dbConfig);
108+
const result = await conn.execute(`SELECT 'Hello World' FROM DUAL`);
109+
assert.strictEqual(result.rows[0][0], 'Hello World');
110+
await conn.close();
111+
process.stdout.write = originalStdoutWrite;
112+
const contents = await fs.readFile(tmpfile, {encoding: 'utf8'});
113+
assert(contents.includes('Receiving packet 3 on stream 3:\n' +
114+
'0000 : 00 08 00 00 0B 08 00 00 |........|'));
115+
} finally {
116+
await fs.unlink(tmpfile);
117+
await fs.rmdir(tmpobj);
118+
}
119+
}); // 1.2
120+
});

0 commit comments

Comments
 (0)