277 lines
12 KiB
C++
277 lines
12 KiB
C++
#ifndef __TCPServer_h__
|
|
# define __TCPServer_h__
|
|
|
|
# include "Command.h"
|
|
# include "CommandList.h"
|
|
# include "IPAddressList.h"
|
|
# include "Socket.h"
|
|
# include "SubscriptionManager.h"
|
|
# include "TCPSession.h"
|
|
# include "TCPSocket.h"
|
|
# include "TLSInfo.h"
|
|
# include <mutex>
|
|
# include <set>
|
|
|
|
namespace core {
|
|
|
|
///
|
|
/// TCPServer
|
|
///
|
|
/// Manage a socket connection as a TCP server type. Connections to the socket are processed through
|
|
/// the accept functionality.
|
|
///
|
|
/// A list of connections is maintained in a vector object.
|
|
///
|
|
/// This object extends the Command object as well so it can be added to a Console object and
|
|
/// process commands to display status information.
|
|
///
|
|
|
|
class TCPServer : public TCPSocket, public Command {
|
|
|
|
public:
|
|
|
|
///
|
|
/// The constructor for the TCPServer object.
|
|
///
|
|
/// @param ePoll the EPoll instance that manages the socket.
|
|
/// @param url the IP address for the socket to receive connection requests.
|
|
/// @param port the port number that the socket will listen on.
|
|
/// @param commandName the name of the command used to invoke the status display for this object.
|
|
///
|
|
|
|
TCPServer(EPoll &ePoll, IPAddress address, TLSInfo *tlsInfo, std::string delimiter = " ", int depth = 10, std::string text = "");
|
|
|
|
///
|
|
/// The destructor for this object.
|
|
///
|
|
|
|
virtual ~TCPServer();
|
|
|
|
virtual void sessionErrorHandler(std::string errorString, std::stringstream &out);
|
|
|
|
///
|
|
/// getSocketAccept is designed to allow a polymorphic extension of this object to
|
|
/// return a type of object that extends the definition of the server socket.
|
|
/// Returning the appropriate session object that extends from Session provides
|
|
/// the mechanism where the server can select the protocol dialog for the desired
|
|
/// service.
|
|
///
|
|
|
|
virtual TCPSession *getSocketAccept(EPoll &epoll);
|
|
|
|
///
|
|
/// The list of sessions that are currently open and being maintained by this object.
|
|
///
|
|
/// This is the one real, strong-owning reference per live session -
|
|
/// see TCPSession::pin()'s own doc comment for the full reasoning.
|
|
/// Every other place that keeps a TCPSession* around (Subscription::
|
|
/// subscribers, a Zephora-specific sessionsByPlayerName map, etc.)
|
|
/// is just a non-owning observer and must pin() before touching the
|
|
/// object. A session stops existing, for real, the instant its
|
|
/// entry here is erased and every other outstanding pin() (if any
|
|
/// thread happened to be mid-call) has released its own reference -
|
|
/// removeFromSessionList() is the only place that ever removes an
|
|
/// entry, and Thread::run() calls it instead of ever calling
|
|
/// `delete` on a TCPSession directly (see its own comment).
|
|
///
|
|
|
|
std::vector<std::shared_ptr<TCPSession>> sessions;
|
|
|
|
///
|
|
/// The commands object is a CommandList and is used to store Command objects to be
|
|
/// parsed and run as data comes into the session.
|
|
///
|
|
|
|
CommandList commands;
|
|
|
|
///
|
|
/// If not NULL the blacklist object can be assigned to this server socket and the server
|
|
/// IP addresses connecting to the server attempting to accept a socket are contained in
|
|
/// this list then the connection is rejected and no accept is granted.
|
|
///
|
|
|
|
IPAddressList *blackList;
|
|
|
|
///
|
|
/// If not NULL the blacklist object can be assigned to this server socket and the server
|
|
/// IP addresses connecting to the server attempting to accept a socket are contained in
|
|
/// this list then the connection is rejected and no accept is granted.
|
|
///
|
|
|
|
IPAddressList *whiteList;
|
|
|
|
void removeFromSessionList(TCPSession *session);
|
|
|
|
void output(std::stringstream &out); ///< Output the consoles array to the console.
|
|
|
|
///
|
|
///
|
|
///
|
|
|
|
void sendToAll(std::stringstream &out);
|
|
|
|
///
|
|
/// Use this sendToAll method to output the contents of the out stream
|
|
/// to all the connections on the server excluding the sender session.
|
|
///
|
|
|
|
void sendToAll(std::stringstream &out, TCPSession &sender, SessionFilter filter);
|
|
|
|
///
|
|
/// Use this sendToAll method to output the contents of the out stream
|
|
/// to all the connections on the server excluding the sender session
|
|
/// and the entries identified by the passed in filter object.
|
|
///
|
|
|
|
void sendToAll(std::stringstream &out, TCPSession &sender);
|
|
|
|
///
|
|
/// The Subscription Manager tracks all subscriptions on the server.
|
|
///
|
|
|
|
SubscriptionManager subscriptions;
|
|
|
|
///
|
|
/// Use the getSessionByAlias to retrieve a session pointer by the value
|
|
/// of the alias pointer. Returns an already-pin()'d shared_ptr (null
|
|
/// if not found) rather than a raw pointer, so callers can't
|
|
/// accidentally hold onto - or use - a reference that isn't actually
|
|
/// keeping the session alive.
|
|
///
|
|
|
|
std::shared_ptr<TCPSession> getSessionByAlias(void *alias);
|
|
|
|
protected:
|
|
///
|
|
/// This method is called by the lower socket when a connection request comes into
|
|
/// the listening and bound server port.
|
|
///
|
|
|
|
void connectionRequest() override;
|
|
|
|
///
|
|
/// Drops any session that completed a TCP accept() but has never
|
|
/// had a single byte of real data pass through onDataReceived() -
|
|
/// covers both a TLS handshake that never finishes and a plain
|
|
/// connection that never sends anything - once it's been open
|
|
/// longer than a short grace period. Nothing else in this class
|
|
/// ever revisits an already-accepted session once it stops
|
|
/// generating epoll events (Thread::run()'s dispatch loop only
|
|
/// acts on a session when a real event for it arrives), so a
|
|
/// connection that completes the handshake and then goes
|
|
/// permanently silent - exactly what a port scan/prober looks
|
|
/// like from here, as opposed to a normal scan that just connects
|
|
/// and immediately closes again, which the existing read-hangup
|
|
/// path already cleans up fine - would otherwise sit in `sessions`
|
|
/// forever, one TCPSession + one fd each. Called from
|
|
/// connectionRequest() (see its own call site) so the sweep keeps
|
|
/// pace with however fast new connections are actually arriving,
|
|
/// without needing a dedicated timer/thread of its own; a
|
|
/// legitimate, already-talking client is never at risk, since
|
|
/// hasReceivedData latches true permanently on its first real byte
|
|
/// and is never consulted again after that.
|
|
///
|
|
|
|
void sweepStaleSessions();
|
|
|
|
///
|
|
/// The listening socket itself is never a TLS peer - accepting a
|
|
/// connection on it surfaces as a plain read() failing with ENOTCONN
|
|
/// (see Socket::receiveData(), which is what actually detects and
|
|
/// dispatches to connectionRequest() above). When this server is
|
|
/// TLS-configured, TCPSocket::receiveData()'s normal ctx-based
|
|
/// dispatch would instead route that same event through
|
|
/// TLS::receiveData()/SSL_read() against a listening fd that was
|
|
/// never SSL_accept()-ed at all (real per-connection sessions are
|
|
/// the only sockets TLS applies to) - confirmed via a real crash
|
|
/// (SIGSEGV inside SSL_read, called with `this` pointing at this
|
|
/// server object, not a session). Always using the plain path here,
|
|
/// regardless of whether tlsInfo is set, keeps connection-detection
|
|
/// working the same way with or without TLS configured.
|
|
///
|
|
|
|
void receiveData(coreutils::ZString &buffer) override;
|
|
|
|
///
|
|
/// This method is called when the Command associated with this object is requested
|
|
/// because a user has typed in the associated command name on a command entry line.
|
|
///
|
|
/// @param the session object to write the output to.
|
|
///
|
|
|
|
int processCommand(coreutils::ZString &request, TCPSession &session) override;
|
|
|
|
protected:
|
|
// Was private - ConsoleServer::logSend() (and any other subclass
|
|
// that iterates `sessions`, itself public further up) needs to be
|
|
// able to synchronize against the exact same mutex
|
|
// connectionRequest()/removeFromSessionList() already use, rather
|
|
// than reading a shared_ptr vector with no locking of its own.
|
|
//
|
|
// Was a plain std::mutex - self-deadlocked the accepting thread on
|
|
// EVERY single connection to ConsoleServer (confirmed live via a
|
|
// gdb thread dump caught mid-stall): connectionRequest() locks
|
|
// `lock`, then calls accept(), which constructs the new session -
|
|
// whose base Socket constructor logs its own creation via
|
|
// coreutils::Log, whose destructor calls back into
|
|
// ConsoleServer::logSend() (exactly the synchronization this was
|
|
// designed for, per the comment above) - which tries to lock this
|
|
// SAME mutex again, on the SAME thread that already holds it.
|
|
// std::mutex is not reentrant, so that thread deadlocked
|
|
// permanently, every time, without ever completing the accept -
|
|
// this is why the admin console never responded to anything.
|
|
// Since all of a server's listening sockets share one fixed-size
|
|
// EPoll worker-thread pool, each such deadlocked thread is gone
|
|
// for good - enough accumulated connection attempts (even
|
|
// sporadic ones, e.g. internet-wide port scanners probing every
|
|
// port) eventually exhausts every worker thread, at which point
|
|
// the WHOLE server - every listening socket, not just the
|
|
// console - stops accepting anything new. This is the actual
|
|
// root cause of BARANTMail's own multi-day-recurring "all ports
|
|
// simultaneously stop accepting connections" incidents - nothing
|
|
// to do with DNS/SPF lookup timing, which was a real, separate,
|
|
// worthwhile fix but not this bug. recursive_mutex lets the same
|
|
// thread re-enter safely (identical behavior to std::mutex for
|
|
// every other thread) without changing the locking design at all.
|
|
std::recursive_mutex lock;
|
|
|
|
public:
|
|
///
|
|
/// A listening TCPServer/ConsoleServer (as opposed to an accepted
|
|
/// TCPSession) is constructed once, directly by main(), and lives
|
|
/// for the whole process - never through the shared_ptr-based
|
|
/// ownership TCPSession::registerLive()/pin() provide (see their
|
|
/// own comments), since nothing ever calls `new` on one of these.
|
|
/// Thread::run()'s epoll dispatch still needs a SAFE way to tell
|
|
/// "this raw pointer is one of those known-immortal listening
|
|
/// sockets, fine to call methods on directly" apart from "this is
|
|
/// an unrecognized/stale pointer that must not be touched at all"
|
|
/// (see TCPSession::tryGet()'s own comment on why dynamic_cast
|
|
/// can't make that distinction safely). Registering `this` once,
|
|
/// in the constructor, into this tiny set is that signal - it's
|
|
/// checked by address only, never dereferenced for the check
|
|
/// itself.
|
|
///
|
|
|
|
static bool isKnownListeningSocket(void *ptr) {
|
|
std::lock_guard<std::mutex> guard(listeningSocketsMutex());
|
|
return listeningSockets().count(ptr) > 0;
|
|
}
|
|
|
|
private:
|
|
TCPSession *accept();
|
|
|
|
static std::mutex &listeningSocketsMutex() {
|
|
static std::mutex m;
|
|
return m;
|
|
}
|
|
static std::set<void *> &listeningSockets() {
|
|
static std::set<void *> s;
|
|
return s;
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
#endif
|