Files
ServerCore/TCPSocket.h
T
Brad Arant 2676aaf84c Fix TLS write busy-spin on SSL_ERROR_WANT_READ
A TLS sendData() failing with SSL_ERROR_WANT_READ (mid-handshake
application data, or a post-handshake session ticket exchange) has
nothing to do with write-readiness, but resetSocket() kept re-arming
EPOLLWRNORM anyway - an already-writable TCP socket fires that
immediately, so the retry loop spun the CPU as fast as it could
until the connection was eventually dropped, without ever reading
the peer's first command. Confirmed via a 15GB+ debug log from one
stuck connection. sendNeedsRead() lets Socket now tell the
difference and wait for a real EPOLLIN instead of busy-retrying.
2026-09-06 04:05:40 +00:00

188 lines
6.4 KiB
C++

#ifndef __TCPSocket_h__
#define __TCPSocket_h__
#include "Socket.h"
#include "TLS.h"
#include "TLSInfo.h"
#include "IPAddress.h"
namespace core {
///
/// TCPSocket
///
/// Provides a network TCP socket.
///
/// For accessing TCP network functions use this object. The connection oriented nature of TCP
/// provides a single client persistent connection with data error correction and a durable
/// synchronous data connection.
///
class TCPSocket : public Socket, public TLS {
public:
///
///
///
TCPSocket(EPoll &ePoll);
///
///
///
TCPSocket(EPoll &ePoll, std::string text);
///
///
///
TCPSocket(EPoll &ePoll, TLSInfo *tlsInfo, std::string text);
///
///
///
TCPSocket(EPoll &ePoll, SSL_CTX *ctx, std::string text);
///
///
///
virtual ~TCPSocket();
///
///
///
void connect(IPAddress &address);
IPAddress ipAddress;
///
/// The output method is called by a socket session (TCPSession) and
/// will output the detail information for the client socket. When extending
/// TCPSocket or TCPSession you can override the method to add attributes
/// to the list.
///
virtual void output(std::stringstream &out);
///
/// Override this method to receive data directly from the socket as data is
/// received. If you need data split by line termination characters then
/// override the onLineReceived method instead.
///
virtual void onDataReceived(coreutils::ZString &data) override;
///
/// Override the onLineReceived method to receive a string of characters that
/// represents a single line of data terminated by a LF or CRLF. If onDataReceived
/// was overriden this method will not be called unless the onDataReceived calls
/// this method explicitly using the class and member name.
///
virtual void onLineReceived(coreutils::ZString &line);
///
/// Override the onBlockReceived method to receive a string of characters that
/// represents a single block of data of length determined by the block length value. If
/// onDataReceived was overriden this method will not be called unless the onDataReceived
/// calls this method explicitly using the class and member name.
///
virtual void onBlockReceived(coreutils::ZString &block);
///
/// Use setBlockSize to set the amount of data that should be read at once from the
/// session data buffer.
/// If this value is set to 0 then the data will be retrieved
///
void setBlockSize(int size = 0);
///
/// Dispatches to TLS::receiveData (SSL_read) when this socket was constructed with a
/// TLSInfo/SSL_CTX, otherwise falls back to Socket::receiveData (plain ::read). Needed
/// because Socket and TLS both declare receiveData as unrelated sibling methods -
/// Socket::receiveData is virtual but TLS::receiveData does not override it, so without
/// this explicit override TLS::receiveData would never be reached and encrypted bytes
/// would be handed to onDataReceived as if they were plaintext.
///
virtual void receiveData(coreutils::ZString &buffer) override;
///
/// Dispatches to TLS::sendData (SSL_write) when this socket was constructed with a
/// TLSInfo/SSL_CTX, otherwise falls back to Socket::sendData (plain ::write). Mirrors
/// the receiveData override above for the write path.
///
virtual int sendData(const char *data, int length) override;
///
/// True only when this socket is actually TLS (ctx set) and the
/// last sendData() call failed specifically with
/// SSL_ERROR_WANT_READ - see TLS::lastSendWantedRead's own doc
/// comment for why Socket::writeSocket()/resetSocket() need this.
///
virtual bool sendNeedsRead() override;
///
/// Completes the TLS handshake (SSL_new/SSL_set_fd/SSL_accept) for
/// THIS socket's own ctx/descriptor. registerSocket()/acceptSocket()
/// are protected TLS members, reachable here via an ordinary
/// same-class call - the point of exposing this as a public wrapper
/// is so a sibling class (TCPServer, which does not inherit
/// TCPSocket-the-session's-particular-instance and so cannot reach
/// another object's protected members directly) can still trigger
/// the handshake on a specific accepted session from
/// TCPServer::connectionRequest(), rather than accidentally running
/// it against the listening server socket itself (see that call
/// site's own comment for the real, confirmed crash this replaces -
/// SSL_read() on an accepted session's never-initialized ssl
/// member).
///
void performTLSHandshake();
protected:
bool term = false;
TLSInfo *tlsInfo;
private:
char *lineBuffer = NULL;
int lineBufferSize = 0;
int lineLength = 0;
int blockSize = 0;
// Socket's own `lock` (guards receiveData) and `outlock` (guards
// writeSocket/the fifo) are two DIFFERENT mutexes - a thread
// handling EPOLLIN and a different thread handling EPOLLWRNORM for
// the SAME connection can therefore call SSL_read and SSL_write on
// the SAME `ssl*` concurrently. OpenSSL does not support that: an
// SSL object must not be used by more than one thread at a time
// (even in 1.1.0+/3.x, which dropped the old app-supplied
// CRYPTO_set_locking_callback global-locking API entirely - that
// API never protected concurrent use of one SSL object anyway,
// only OpenSSL's own internal global state). Confirmed live via
// dmesg: "traps: apiserver[...] general protection fault ... in
// libssl.so.3" during ordinary interactive use. ioLock serializes
// just the raw SSL_read/SSL_write calls themselves (see
// receiveData/sendData below) - deliberately NOT the surrounding
// request-handling, which already nests `lock` before `outlock`
// (receiveData's call chain reaches Socket::write(), which takes
// outlock) - wrapping any larger scope in ioLock+lock+outlock
// together risks recreating that same ordering as a new cycle.
std::mutex ioLock;
};
}
#endif