You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
340 lines
14 KiB
340 lines
14 KiB
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Digital Voice Modem - Common Library
|
|
* GPLv2 Open Source. Use is subject to license terms.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL
|
|
*
|
|
*/
|
|
/**
|
|
* @file SecureTcpClient.h
|
|
* @ingroup tcp_socket
|
|
* @file SecureTcpClient.cpp
|
|
* @ingroup tcp_socket
|
|
*/
|
|
#if !defined(__SECURE_TCP_CLIENT_H__)
|
|
#define __SECURE_TCP_CLIENT_H__
|
|
|
|
#if defined(ENABLE_SSL)
|
|
|
|
#include "Defines.h"
|
|
#include "common/Log.h"
|
|
#include "common/network/tcp/Socket.h"
|
|
|
|
#include <cassert>
|
|
#include <cstring>
|
|
|
|
#if !defined(_WIN32)
|
|
#include <fcntl.h>
|
|
#else
|
|
#include <winsock2.h>
|
|
#endif // !defined(_WIN32)
|
|
|
|
#include <openssl/ssl.h>
|
|
#include <openssl/err.h>
|
|
#include <openssl/x509v3.h>
|
|
|
|
namespace network
|
|
{
|
|
namespace tcp
|
|
{
|
|
// ---------------------------------------------------------------------------
|
|
// Class Declaration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @brief Implements a secure TCP client.
|
|
* @ingroup tcp_socket
|
|
*/
|
|
class DVM_COMMON_API SecureTcpClient : public Socket
|
|
{
|
|
public:
|
|
auto operator=(SecureTcpClient&) -> SecureTcpClient& = delete;
|
|
auto operator=(SecureTcpClient&&) -> SecureTcpClient& = delete;
|
|
SecureTcpClient(SecureTcpClient&) = delete;
|
|
|
|
/**
|
|
* @brief Initializes a new instance of the SecureTcpClient class.
|
|
* @param fd File Descriptor for existing socket.
|
|
* @param sslCtx Instance of the OpenSSL context.
|
|
* @param client Address for client.
|
|
* @param clientLen Length of sockaddr_in structure.
|
|
* @param nonBlocking Flag indicating socket operations should be non-blocking.
|
|
*/
|
|
SecureTcpClient(const int fd, SSL_CTX* sslCtx, sockaddr_in& client, int clientLen, bool nonBlocking) : Socket(fd),
|
|
m_sockaddr(),
|
|
m_pSSL(nullptr),
|
|
m_pSSLCtx(nullptr)
|
|
{
|
|
::memcpy(reinterpret_cast<char*>(&m_sockaddr), reinterpret_cast<char*>(&client), clientLen);
|
|
|
|
initSsl(sslCtx);
|
|
|
|
#if !defined(_WIN32)
|
|
// setup socket for non-blocking operations
|
|
int flags = fcntl(fd, F_GETFL, 0);
|
|
if (flags < 0) {
|
|
LogError(LOG_NET, "failed fcntl(F_GETFL), err: %d (%s)", errno, strerror(errno));
|
|
throw std::runtime_error("Cannot accept SSL client");
|
|
}
|
|
|
|
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
|
|
LogError(LOG_NET, "failed fcntl(F_SETFL), err: %d (%s)", errno, strerror(errno));
|
|
throw std::runtime_error("Cannot accept SSL client");
|
|
}
|
|
#else
|
|
// setup socket for non-blocking operations
|
|
u_long mode = 1;
|
|
if (ioctlsocket(fd, FIONBIO, &mode) != NO_ERROR) {
|
|
int err = WSAGetLastError();
|
|
LogError(LOG_NET, "failed ioctlsocket(FIONBIO), err: %d", err);
|
|
throw std::runtime_error("Cannot accept SSL client");
|
|
}
|
|
#endif // !defined(_WIN32)
|
|
|
|
int status = -1;
|
|
struct timeval tv, tvRestore;
|
|
tv.tv_sec = 5;
|
|
tv.tv_usec = 0;
|
|
tvRestore = tv;
|
|
|
|
fd_set writeFdSet;
|
|
fd_set readFdSet;
|
|
|
|
// begin application specific OpenSSL handshake polling
|
|
do {
|
|
tv = tvRestore;
|
|
FD_ZERO(&writeFdSet);
|
|
FD_ZERO(&readFdSet);
|
|
|
|
status = ::SSL_accept(m_pSSL);
|
|
switch (::SSL_get_error(m_pSSL, status)) {
|
|
case SSL_ERROR_NONE:
|
|
status = 0;
|
|
break;
|
|
|
|
case SSL_ERROR_WANT_WRITE:
|
|
FD_SET(fd, &writeFdSet);
|
|
status = 1; // wait for more activity
|
|
break;
|
|
|
|
case SSL_ERROR_WANT_READ:
|
|
FD_SET(fd, &readFdSet);
|
|
status = 1; // wait for more activity
|
|
break;
|
|
|
|
case SSL_ERROR_ZERO_RETURN:
|
|
case SSL_ERROR_SYSCALL:
|
|
// the peer has notified us that it is shutting down via
|
|
// the SSL "close_notify" message so we need to
|
|
// shutdown, too.
|
|
LogError(LOG_NET, "Cannot accept SSL client, connection closed during handshake, %s err: %d", ERR_error_string(ERR_get_error(), NULL), errno);
|
|
throw std::runtime_error("Cannot accept SSL client");
|
|
default:
|
|
LogError(LOG_NET, "Cannot accept SSL client, unexpected error during handshake, %s err: %d", ERR_error_string(ERR_get_error(), NULL), errno);
|
|
throw std::runtime_error("Cannot accept SSL client");
|
|
}
|
|
|
|
if (status == 1) {
|
|
// must have at least one handle to wait for at this point.
|
|
status = select(fd + 1, &readFdSet, &writeFdSet, NULL, &tv);
|
|
if (status >= 1) {
|
|
status = 1;
|
|
}
|
|
else {
|
|
LogError(LOG_NET, "Cannot accept SSL client, timeout during handshake, %s err: %d", ERR_error_string(ERR_get_error(), NULL), errno);
|
|
throw std::runtime_error("Cannot accept SSL client");
|
|
}
|
|
}
|
|
} while (status == 1 && !SSL_is_init_finished(m_pSSL));
|
|
|
|
#if !defined(_WIN32)
|
|
// reset socket blocking operations
|
|
if (!nonBlocking) {
|
|
flags = fcntl(fd, F_GETFL, 0);
|
|
if (flags < 0) {
|
|
LogError(LOG_NET, "failed fcntl(F_GETFL), err: %d (%s)", errno, strerror(errno));
|
|
throw std::runtime_error("Cannot accept SSL client");
|
|
}
|
|
|
|
if (fcntl(fd, F_SETFL, flags & (~O_NONBLOCK)) < 0) {
|
|
LogError(LOG_NET, "failed fcntl(F_SETFL), err: %d (%s)", errno, strerror(errno));
|
|
throw std::runtime_error("Cannot accept SSL client");
|
|
}
|
|
}
|
|
#else
|
|
// reset socket blocking operations
|
|
if (!nonBlocking) {
|
|
u_long mode = 0; // 0 = blocking, 1 = non-blocking
|
|
if (ioctlsocket(fd, FIONBIO, &mode) != NO_ERROR) {
|
|
int err = WSAGetLastError();
|
|
LogError(LOG_NET, "failed ioctlsocket(FIONBIO), err: %d", err);
|
|
throw std::runtime_error("Cannot accept SSL client");
|
|
}
|
|
}
|
|
#endif // !defined(_WIN32)
|
|
}
|
|
/**
|
|
* @brief Initializes a new instance of the SecureTcpClient class.
|
|
* @param address IP Address.
|
|
* @param port Port.
|
|
* @param nonBlocking Flag indicating socket operations should be non-blocking.
|
|
*/
|
|
SecureTcpClient(const std::string& address, const uint16_t port, bool nonBlocking) :
|
|
m_pSSL(nullptr),
|
|
m_pSSLCtx(nullptr)
|
|
{
|
|
assert(!address.empty());
|
|
assert(port > 0U);
|
|
|
|
OpenSSL_add_ssl_algorithms();
|
|
const SSL_METHOD* method = SSLv23_client_method();
|
|
SSL_load_error_strings();
|
|
m_pSSLCtx = SSL_CTX_new(method);
|
|
|
|
init();
|
|
|
|
sockaddr_in addr = {};
|
|
initAddr(address, port, addr);
|
|
|
|
::memcpy(reinterpret_cast<char*>(&m_sockaddr), reinterpret_cast<char*>(&addr), sizeof(addr));
|
|
|
|
ssize_t ret = ::connect(m_fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
|
|
if (ret < 0) {
|
|
LogError(LOG_NET, "Failed to connect to server, err: %d (%s)", errno, strerror(errno));
|
|
}
|
|
|
|
initSsl(m_pSSLCtx);
|
|
if (SSL_connect(m_pSSL) <= 0) {
|
|
LogError(LOG_NET, "Failed to connect to server, %s err: %d", ERR_error_string(ERR_get_error(), NULL), errno);
|
|
throw std::runtime_error("Failed to SSL connect to server");
|
|
}
|
|
|
|
#if !defined(_WIN32)
|
|
// setup socket for non-blocking operations
|
|
if (nonBlocking) {
|
|
int flags = fcntl(m_fd, F_GETFL, 0);
|
|
if (flags < 0) {
|
|
LogError(LOG_NET, "failed fcntl(F_GETFL), err: %d (%s)", errno, strerror(errno));
|
|
throw std::runtime_error("Failed to set SSL server connection to non-blocking");
|
|
}
|
|
|
|
if (fcntl(m_fd, F_SETFL, flags | O_NONBLOCK) < 0) {
|
|
LogError(LOG_NET, "failed fcntl(F_SETFL), err: %d (%s)", errno, strerror(errno));
|
|
throw std::runtime_error("Failed to set SSL server connection to non-blocking");
|
|
}
|
|
}
|
|
#else
|
|
// setup socket for non-blocking operations
|
|
if (nonBlocking) {
|
|
u_long mode = 1;
|
|
if (ioctlsocket(m_fd, FIONBIO, &mode) != NO_ERROR) {
|
|
int err = WSAGetLastError();
|
|
LogError(LOG_NET, "failed ioctlsocket(FIONBIO), err: %d", err);
|
|
throw std::runtime_error("Cannot accept SSL client");
|
|
}
|
|
}
|
|
#endif // !defined(_WIN32)
|
|
}
|
|
/**
|
|
* @brief Finalizes a instance of the SecureTcpClient class.
|
|
*/
|
|
~SecureTcpClient() override
|
|
{
|
|
if (m_pSSL != nullptr) {
|
|
SSL_shutdown(m_pSSL);
|
|
SSL_free(m_pSSL);
|
|
}
|
|
|
|
if (m_pSSLCtx != nullptr)
|
|
SSL_CTX_free(m_pSSLCtx);
|
|
}
|
|
|
|
/**
|
|
* @brief Read data from the socket.
|
|
* @param[out] buffer Buffer to read data into.
|
|
* @param[out] length Length of data to read.
|
|
* @returns ssize_t Actual length of data read from remote TCP socket.
|
|
*/
|
|
[[nodiscard]] ssize_t read(uint8_t* buffer, size_t len) noexcept override
|
|
{
|
|
return SSL_read(m_pSSL, buffer, (int)len);
|
|
}
|
|
/**
|
|
* @brief Write data to the socket.
|
|
* @param[in] buffer Buffer containing data to write to socket.
|
|
* @param length Length of data to write.
|
|
* @returns ssize_t Length of data written.
|
|
*/
|
|
ssize_t write(const uint8_t* buffer, size_t len) noexcept override
|
|
{
|
|
return SSL_write(m_pSSL, buffer, (int)len);
|
|
}
|
|
|
|
/**
|
|
* @brief Helper to get an IP address from the sockaddr_storage.
|
|
* @returns sockaddr_storage sockaddr_storage structure.
|
|
*/
|
|
sockaddr_storage getAddress() const { return m_sockaddr; }
|
|
|
|
/**
|
|
* @brief Sets the hostname for the SSL certificate.
|
|
* @param hostname Hostname.
|
|
*/
|
|
static void setHostname(std::string hostname) { s_sslHostname = hostname; }
|
|
|
|
private:
|
|
sockaddr_storage m_sockaddr;
|
|
static std::string s_sslHostname;
|
|
|
|
SSL* m_pSSL;
|
|
SSL_CTX* m_pSSLCtx;
|
|
|
|
/**
|
|
* @brief Internal helper to initialize the TCP socket.
|
|
*/
|
|
void init() noexcept(false)
|
|
{
|
|
m_fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
|
if (m_fd < 0) {
|
|
LogError(LOG_NET, "Cannot create the TCP socket, err: %d (%s)", errno, strerror(errno));
|
|
throw std::runtime_error("Cannot create the TCP socket");
|
|
}
|
|
|
|
int reuse = 1;
|
|
if (::setsockopt(m_fd, IPPROTO_TCP, TCP_NODELAY, (char*)& reuse, sizeof(reuse)) != 0) {
|
|
LogError(LOG_NET, "Cannot set the TCP socket option, err: %d (%s)", errno, strerror(errno));
|
|
throw std::runtime_error("Cannot set the TCP socket option");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Internal helper to initialize SSL.
|
|
* @param sslCtx Instance of the OpenSSL context.
|
|
*/
|
|
void initSsl(SSL_CTX* sslCtx) noexcept(false)
|
|
{
|
|
m_pSSL = SSL_new(sslCtx);
|
|
if (m_pSSL == nullptr) {
|
|
LogError(LOG_NET, "Failed to create SSL client, %s err: %d", ERR_error_string(ERR_get_error(), NULL), errno);
|
|
throw std::runtime_error("Failed to create SSL client");
|
|
}
|
|
|
|
SSL_set_hostflags(m_pSSL, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
|
|
if (!SSL_set1_host(m_pSSL, SecureTcpClient::s_sslHostname.c_str())) {
|
|
LogError(LOG_NET, "Failed to set SSL hostname, %s err: %d", ERR_error_string(ERR_get_error(), NULL), errno);
|
|
throw std::runtime_error("Failed to set SSL hostname");
|
|
}
|
|
|
|
SSL_set_verify(m_pSSL, SSL_VERIFY_NONE, NULL);
|
|
SSL_set_fd(m_pSSL, m_fd);
|
|
}
|
|
};
|
|
} // namespace tcp
|
|
} // namespace network
|
|
|
|
#endif // ENABLE_SSL
|
|
|
|
#endif // __SECURE_TCP_CLIENT_H__
|