From 2524498ecc5a6026153be62ef428a41a1dce843d Mon Sep 17 00:00:00 2001
From: Sebastião Guerra <sebastiao.luiz.guerra@gmail.com>
Date: Fri, 23 Oct 2026 19:24:30 -0300
Subject: Send SNI in TLS client handshakes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Sebastião Guerra <sebastiao.luiz.guerra@gmail.com>
---
 src/tls/tqca-tls.cpp | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 src/tqca.cpp         |  6 ++++++
 src/tqcaprovider.h   | 10 ++++++++++
 3 files changed, 67 insertions(+), 1 deletion(-)

diff --git a/src/tls/tqca-tls.cpp b/src/tls/tqca-tls.cpp
index b1fa274..fcccb42 100644
--- a/src/tls/tqca-tls.cpp
+++ b/src/tls/tqca-tls.cpp
@@ -992,7 +992,7 @@ public:
 };
 
 static bool ssl_init = false;
-class TLSContext : public TQCA_TLSContext
+class TLSContext : public TQCA_TLSContext, public TQCA_TLSContextPeerHost
 {
 public:
 	enum { Good, TryAgain, Bad };
@@ -1021,10 +1021,14 @@ public:
 			ssl_init = true;
 		}
 
+		serv = false;
+		mode = Idle;
 		ssl = nullptr;
 		context = nullptr;
 		cert = nullptr;
 		key = nullptr;
+		rbio = nullptr;
+		wbio = nullptr;
 	}
 
 	~TLSContext()
@@ -1038,6 +1042,9 @@ public:
 			SSL_free(ssl);
 			ssl = nullptr;
 		}
+		// SSL_set_bio() transfers ownership to SSL; SSL_free() releases both BIOs.
+		rbio = nullptr;
+		wbio = nullptr;
 		if(context) {
 			SSL_CTX_free(context);
 			context = nullptr;
@@ -1053,6 +1060,7 @@ public:
 
 		sendQueue.resize(0);
 		recvQueue.resize(0);
+		serv = false;
 		mode = Idle;
 		cc.reset();
 		vr = TQCA::TLS::Unknown;
@@ -1090,6 +1098,30 @@ public:
 		return true;
 	}
 
+	bool setPeerHost(const TQString &host)
+	{
+		TQString peerHost = host.stripWhiteSpace();
+		if(serv || !ssl || peerHost.isEmpty())
+			return true;
+
+		TQString numericHost = peerHost;
+		if(numericHost.startsWith("[") && numericHost.endsWith("]"))
+			numericHost = numericHost.mid(1, numericHost.length() - 2);
+		TQHostAddress address;
+		if(address.setAddress(numericHost))
+			return true;
+
+		TQString asciiHost;
+		if(!domainToAscii(peerHost, &asciiHost))
+			return true;
+
+#if defined(SSL_CTRL_SET_TLSEXT_HOSTNAME) && !defined(OPENSSL_NO_TLSEXT)
+		return SSL_set_tlsext_host_name(ssl, asciiHost.latin1()) == 1;
+#else
+		return true;
+#endif
+	}
+
 	bool setup(const TQPtrList<TQCA_CertContext> &list, const TQCA_CertContext &_cc, const TQCA_RSAKeyContext &kc)
 	{
 		context = SSL_CTX_new(method);
@@ -1116,6 +1148,16 @@ public:
 		// setup the memory bio
 		rbio = BIO_new(BIO_s_mem());
 		wbio = BIO_new(BIO_s_mem());
+		if(!rbio || !wbio) {
+			if(rbio)
+				BIO_free(rbio);
+			if(wbio)
+				BIO_free(wbio);
+			rbio = nullptr;
+			wbio = nullptr;
+			reset();
+			return false;
+		}
 
 		// this passes control of the bios to ssl.  we don't need to free them.
 		SSL_set_bio(ssl, rbio, wbio);
@@ -1139,6 +1181,8 @@ public:
 
 	int handshake(const TQByteArray &in, TQByteArray *out)
 	{
+		if(!ssl || !rbio || !wbio)
+			return Error;
 		if(!in.isEmpty())
 			BIO_write(rbio, in.data(), in.size());
 
@@ -1188,6 +1232,8 @@ public:
 
 	int shutdown(const TQByteArray &in, TQByteArray *out)
 	{
+		if(!ssl || !rbio || !wbio)
+			return Error;
 		if(!in.isEmpty())
 			BIO_write(rbio, in.data(), in.size());
 
@@ -1314,6 +1360,8 @@ public:
 	TQByteArray unprocessed()
 	{
 		TQByteArray a;
+		if(!rbio)
+			return a;
 		int size = BIO_pending(rbio);
 		if(size <= 0)
 			return a;
@@ -1332,6 +1380,8 @@ public:
 	TQByteArray readOutgoing()
 	{
 		TQByteArray a;
+		if(!wbio)
+			return a;
 		int size = BIO_pending(wbio);
 		if(size <= 0)
 			return a;
diff --git a/src/tqca.cpp b/src/tqca.cpp
index 979a251..397cac8 100644
--- a/src/tqca.cpp
+++ b/src/tqca.cpp
@@ -907,6 +907,12 @@ bool TLS::startClient(const TQString &host)
 
 	if(!d->c->startClient(d->store, *d->ourCert.d->c, *d->ourKey.d->c))
 		return false;
+	if(TQCA_TLSContextPeerHost *peerHost = dynamic_cast<TQCA_TLSContextPeerHost *>(d->c)) {
+		if(!peerHost->setPeerHost(host)) {
+			d->c->reset();
+			return false;
+		}
+	}
 	TQTimer::singleShot(0, this, TQ_SLOT(update()));
 	return true;
 }
diff --git a/src/tqcaprovider.h b/src/tqcaprovider.h
index b57cc61..d600429 100644
--- a/src/tqcaprovider.h
+++ b/src/tqcaprovider.h
@@ -139,6 +139,16 @@ public:
 	virtual int validityResult() const=0;
 };
 
+// Optional TLS provider capability.  The core calls this after startClient()
+// has created the provider SSL object, but before the first handshake step.
+// Keeping this separate preserves the existing TQCA_TLSContext vtable ABI.
+class TQCA_TLSContextPeerHost
+{
+public:
+	virtual ~TQCA_TLSContextPeerHost() {}
+	virtual bool setPeerHost(const TQString &host)=0;
+};
+
 struct TQCA_SASLHostPort
 {
 	TQHostAddress addr;
-- 
cgit v1.2.3

