Squashed commit of the following:

commit e131ec19cc4193d13310f1fa51e1d8e83d18a8a2
Merge: eae1454 fd94689
Author: Ian Craggs <icraggs@gmail.com>
Date:   Thu Feb 12 11:48:35 2026 +0000

    Merge branch 'fix-ssl-handshake-want-write' of github.com:spencerbug/paho.mqtt.c into spencerbug-fix-ssl-handshake-want-write

commit fd946895c7
Author: Spencer Neilan <sneilan@shoppertrak.com>
Date:   Fri Nov 14 08:14:20 2025 -0600

    #1627 Fixed failing unit tests

    Signed-off-by: Spencer Neilan <jneilas@jci.com>

commit 2d23bf0f61
Author: Spencer Neilan <sneilan@shoppertrak.com>
Date:   Thu Nov 13 16:22:15 2025 -0600

    #1627 Added unit test integrated as test-ssl-want-write-handshake in ctest suite

    Signed-off-by: Spencer Neilan <jneilas@jci.com>

commit 7ce39d2db1
Author: Spencer Neilan <sneilan@shoppertrak.com>
Date:   Thu Nov 13 16:21:33 2025 -0600

    #1627 Fix SSL handshake WANT_WRITE handling by adding to pending-write poll set.

    Signed-off-by: Spencer Neilan <jneilas@jci.com>

commit 010c5cd44a
Author: Spencer Neilan <sneilan@shoppertrak.com>
Date:   Thu Nov 13 16:25:25 2025 -0600

    Merge branch 'master' of https://github.com/spencerbug/paho.mqtt.c into fix-ssl-handshake-want-write

commit 651b71d1be
Merge: 5571f9b c4381a7
Author: Spencer Neilan <spencer.neilan@gmail.com>
Date:   Thu Nov 13 16:23:30 2025 -0600

    Merge pull request #1 from eclipse-paho/master

    Sync
This commit is contained in:
Ian Craggs 2026-02-12 12:18:32 +00:00
parent 092e2c4682
commit 9a35ea7948
No known key found for this signature in database
GPG Key ID: A7AE1A8F2CCAB186
3 changed files with 349 additions and 40 deletions

View File

@ -777,53 +777,72 @@ int SSLSocket_connect(SSL* ssl, SOCKET sock, const char* hostname, int verify, i
error = SSLSocket_error("SSL_connect", ssl, sock, rc, cb, u);
if (error == SSL_FATAL)
rc = error;
if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE)
rc = TCPSOCKET_INTERRUPTED;
}
#if (OPENSSL_VERSION_NUMBER >= 0x010002000) /* 1.0.2 and later */
else if (verify)
{
char* peername = NULL;
int port;
size_t hostname_len;
X509* cert = SSL_get_peer_certificate(ssl);
hostname_len = MQTTProtocol_addressPort(hostname, &port, NULL, MQTT_DEFAULT_PORT);
rc = X509_check_host(cert, hostname, hostname_len, 0, &peername);
if (rc == 1)
Log(TRACE_PROTOCOL, -1, "peername from X509_check_host is %s", peername);
else
Log(TRACE_PROTOCOL, -1, "X509_check_host for hostname %.*s failed, rc %d",
(int)hostname_len, hostname, rc);
if (peername != NULL)
OPENSSL_free(peername);
/* 0 == fail, -1 == SSL internal error, -2 == malformed input */
if (rc == 0 || rc == -1 || rc == -2)
{
char* ip_addr = malloc(hostname_len + 1);
/* cannot use = strndup(hostname, hostname_len); here because of custom Heap */
if (ip_addr)
switch (error)
{
strncpy(ip_addr, hostname, hostname_len);
ip_addr[hostname_len] = '\0';
case SSL_ERROR_WANT_READ:
rc = TCPSOCKET_INTERRUPTED;
Socket_clearPendingWrite(sock);
break;
case SSL_ERROR_WANT_WRITE:
rc = TCPSOCKET_INTERRUPTED;
Socket_addPendingWrite(sock);
break;
default:
Socket_clearPendingWrite(sock);
break;
}
}
}
else
{
Socket_clearPendingWrite(sock);
#if (OPENSSL_VERSION_NUMBER >= 0x010002000) /* 1.0.2 and later */
if (verify)
{
char* peername = NULL;
int port;
size_t hostname_len;
rc = X509_check_ip_asc(cert, ip_addr, 0);
Log(TRACE_MIN, -1, "rc from X509_check_ip_asc is %d", rc);
X509* cert = SSL_get_peer_certificate(ssl);
hostname_len = MQTTProtocol_addressPort(hostname, &port, NULL, MQTT_DEFAULT_PORT);
free(ip_addr);
rc = X509_check_host(cert, hostname, hostname_len, 0, &peername);
if (rc == 1)
Log(TRACE_PROTOCOL, -1, "peername from X509_check_host is %s", peername);
else
Log(TRACE_PROTOCOL, -1, "X509_check_host for hostname %.*s failed, rc %d",
(int)hostname_len, hostname, rc);
if (peername != NULL)
OPENSSL_free(peername);
/* 0 == fail, -1 == SSL internal error, -2 == malformed input */
if (rc == 0 || rc == -1 || rc == -2)
{
char* ip_addr = malloc(hostname_len + 1);
/* cannot use = strndup(hostname, hostname_len); here because of custom Heap */
if (ip_addr)
{
strncpy(ip_addr, hostname, hostname_len);
ip_addr[hostname_len] = '\0';
rc = X509_check_ip_asc(cert, ip_addr, 0);
Log(TRACE_MIN, -1, "rc from X509_check_ip_asc is %d", rc);
free(ip_addr);
}
if (rc == 0 || rc == -1 || rc == -2)
rc = SSL_FATAL;
}
if (rc == 0 || rc == -1 || rc == -2)
rc = SSL_FATAL;
if (cert)
X509_free(cert);
}
if (cert)
X509_free(cert);
}
#endif
}
FUNC_EXIT_RC(rc);
return rc;

View File

@ -1207,8 +1207,53 @@ if(PAHO_WITH_SSL OR PAHO_WITH_LIBRESSL)
PROPERTIES TIMEOUT 540
)
endif()
endif()
ENDIF()
# SSL WANT_WRITE handshake test - validates proper handling of SSL_ERROR_WANT_WRITE/WANT_READ
IF (PAHO_BUILD_STATIC)
ADD_EXECUTABLE(
test_ssl_want_write-static
test_ssl_want_write.c
)
TARGET_LINK_LIBRARIES(
test_ssl_want_write-static
paho-mqtt3cs-static
)
ADD_TEST(
NAME test-ssl-want-write-handshake-static
COMMAND test_ssl_want_write-static "--hostname" ${MQTT_SSL_HOSTNAME} "--client_key" "${CERTDIR}/client.pem" "--server_key" "${CERTDIR}/test-root-ca.crt"
)
SET_TESTS_PROPERTIES(
test-ssl-want-write-handshake-static
PROPERTIES TIMEOUT 60
)
ENDIF()
IF (PAHO_BUILD_SHARED)
ADD_EXECUTABLE(
test_ssl_want_write
test_ssl_want_write.c
)
TARGET_LINK_LIBRARIES(
test_ssl_want_write
paho-mqtt3cs
)
ADD_TEST(
NAME test-ssl-want-write-handshake
COMMAND test_ssl_want_write "--hostname" ${MQTT_SSL_HOSTNAME} "--client_key" "${CERTDIR}/client.pem" "--server_key" "${CERTDIR}/test-root-ca.crt"
)
SET_TESTS_PROPERTIES(
test-ssl-want-write-handshake
PROPERTIES TIMEOUT 60
)
ENDIF()
ENDIF()
if(PAHO_BUILD_STATIC)

245
test/test_ssl_want_write.c Normal file
View File

@ -0,0 +1,245 @@
/*******************************************************************************
* Copyright (c) 2024, 2026 Ian Craggs and others
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* https://www.eclipse.org/legal/epl-2.0/
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* Contributors:
* Unit test for SSL_ERROR_WANT_WRITE/WANT_READ handling during SSL handshake
*******************************************************************************/
/**
* @file
* Test for SSL_ERROR_WANT_WRITE and SSL_ERROR_WANT_READ handling in SSLSocket_connect()
*
* This test verifies that SSL connections complete successfully even when
* SSL_ERROR_WANT_WRITE or SSL_ERROR_WANT_READ occur during the handshake.
* The fix ensures proper socket polling by calling Socket_addPendingWrite()
* when WANT_WRITE occurs.
*/
#include "MQTTClient.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
#if !defined(_WINDOWS)
#include <sys/time.h>
#include <unistd.h>
#endif
/* Test configuration */
struct Options
{
char* connection;
char* client_key_file;
char* server_key_file;
int verbose;
} options =
{
"ssl://localhost:18883",
NULL,
NULL,
1
};
void usage(void)
{
printf("SSL WANT_WRITE Handshake Test\n");
printf("Options:\n");
printf("\t--hostname <hostname> - SSL server hostname (default: ssl://localhost:18883)\n");
printf("\t--client_key <file> - Client certificate file\n");
printf("\t--server_key <file> - Server CA certificate file\n");
printf("\t--verbose - Enable verbose output\n");
printf("\t--help - This help\n");
exit(EXIT_FAILURE);
}
void getopts(int argc, char** argv)
{
int count = 1;
while (count < argc)
{
if (strcmp(argv[count], "--hostname") == 0)
{
if (++count < argc)
options.connection = argv[count];
else
usage();
}
else if (strcmp(argv[count], "--client_key") == 0)
{
if (++count < argc)
options.client_key_file = argv[count];
else
usage();
}
else if (strcmp(argv[count], "--server_key") == 0)
{
if (++count < argc)
options.server_key_file = argv[count];
else
usage();
}
else if (strcmp(argv[count], "--verbose") == 0)
{
options.verbose = 1;
}
else if (strcmp(argv[count], "--help") == 0)
{
usage();
}
else
{
printf("Unknown option: %s\n", argv[count]);
usage();
}
count++;
}
}
/* Logging */
#define LOGA_DEBUG 0
#define LOGA_INFO 1
void MyLog(int LOGA_level, char* format, ...)
{
static char msg_buf[256];
va_list args;
#if defined(_WIN32) || defined(_WINDOWS)
struct timeb ts;
#else
struct timeval ts;
#endif
struct tm timeinfo;
if (LOGA_level == LOGA_DEBUG && options.verbose == 0)
return;
#if defined(_WIN32) || defined(_WINDOWS)
ftime(&ts);
localtime_s(&timeinfo, &ts.time);
#else
gettimeofday(&ts, NULL);
localtime_r(&ts.tv_sec, &timeinfo);
#endif
strftime(msg_buf, 80, "%Y%m%d %H%M%S", &timeinfo);
#if defined(_WIN32) || defined(_WINDOWS)
sprintf(&msg_buf[strlen(msg_buf)], ".%.3hu ", ts.millitm);
#else
sprintf(&msg_buf[strlen(msg_buf)], ".%.3lu ", ts.tv_usec / 1000L);
#endif
va_start(args, format);
vsnprintf(&msg_buf[strlen(msg_buf)], sizeof(msg_buf) - strlen(msg_buf), format, args);
va_end(args);
printf("%s\n", msg_buf);
fflush(stdout);
}
/* Test tracking */
int tests = 0;
int failures = 0;
#define ASSERT(condition, message) \
do { \
tests++; \
if (!(condition)) { \
failures++; \
MyLog(1, "FAIL: %s (line %d): %s", __func__, __LINE__, message); \
return -1; \
} \
} while(0)
/**
* Test: Basic SSL connection
*
* Verifies that SSL connections complete successfully.
* This validates that the SSL_ERROR_WANT_WRITE/WANT_READ handling
* in SSLSocket_connect() works correctly.
*/
int test_ssl_connect_basic(void)
{
MQTTClient client;
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
MQTTClient_SSLOptions ssl_opts = MQTTClient_SSLOptions_initializer;
int rc;
MyLog(1, "Test: Basic SSL connection");
rc = MQTTClient_create(&client, options.connection, "test_ssl_want_write",
MQTTCLIENT_PERSISTENCE_NONE, NULL);
ASSERT(rc == MQTTCLIENT_SUCCESS, "MQTTClient_create failed");
conn_opts.keepAliveInterval = 20;
conn_opts.cleansession = 1;
if (options.server_key_file)
{
ssl_opts.trustStore = options.server_key_file;
conn_opts.ssl = &ssl_opts;
}
if (options.client_key_file)
{
ssl_opts.keyStore = options.client_key_file;
}
MyLog(1, "Connecting to %s", options.connection);
rc = MQTTClient_connect(client, &conn_opts);
if (rc != MQTTCLIENT_SUCCESS)
{
MyLog(1, "Connection failed: rc=%d", rc);
MyLog(1, "Note: This is expected if no SSL broker is running on %s", options.connection);
/* Not a test failure - just means no broker available */
}
else
{
MyLog(1, "Connection succeeded");
MQTTClient_disconnect(client, 1000);
}
MQTTClient_destroy(&client);
return 0;
}
/**
* Main test runner
*/
int main(int argc, char** argv)
{
int rc = 0;
getopts(argc, argv);
printf("SSL Connection Test\n");
printf("===================\n");
printf("Connection: %s\n", options.connection);
if (options.client_key_file)
printf("Client Key: %s\n", options.client_key_file);
if (options.server_key_file)
printf("Server Key: %s\n", options.server_key_file);
printf("\n");
rc = test_ssl_connect_basic();
printf("\nTest Results: %d tests, %d failures\n", tests, failures);
if (failures == 0)
printf("SUCCESS\n");
else
printf("FAILURE: %d test(s) failed\n", failures);
return (failures == 0) ? 0 : 1;
}