diff --git a/include/CSFML/Network/TcpSocket.h b/include/CSFML/Network/TcpSocket.h index a5d9501..5ed4d18 100644 --- a/include/CSFML/Network/TcpSocket.h +++ b/include/CSFML/Network/TcpSocket.h @@ -34,9 +34,23 @@ #include #include +#include #include +//////////////////////////////////////////////////////////// +/// \brief Transport layer security status codes +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfTlsNotConnected, ///< TCP connection not yet connected + sfTlsHandshakeStarted, ///< TLS handshake has been started + sfTlsHandshakeComplete, ///< TLS handshake is complete, stream is encrypted + sfTlsError ///< An unexpected error happened +} sfTlsStatus; + + //////////////////////////////////////////////////////////// /// \brief Create a new TCP socket /// @@ -229,3 +243,121 @@ CSFML_NETWORK_API sfSocketStatus sfTcpSocket_sendPacket(sfTcpSocket* socket, sfP /// //////////////////////////////////////////////////////////// CSFML_NETWORK_API sfSocketStatus sfTcpSocket_receivePacket(sfTcpSocket* socket, sfPacket* packet); + +//////////////////////////////////////////////////////////// +/// \brief Set up transport layer security as a client +/// +/// Once the TCP connection is connected, transport layer +/// security can be set up. +/// +/// If this function is called before the TCP connection is +/// connected, it will return sfTlsNotConnected and must be +/// called again once the TCP connection is connected. +/// +/// If this function started TLS setup but could not finish +/// it within this call e.g. because this socket was set to +/// non-blocking, it will return sfTlsHandshakeStarted and +/// this function will have to be called repeatedly until +/// sfTlsHandshakeComplete is returned. If this socket is +/// blocking, sfTlsHandshakeComplete should be returned +/// within the same function call if TLS setup was successful. +/// +/// If sfTlsError is returned, something went wrong with TLS +/// setup and the connection must be reconnected and TLS setup +/// reattempted after it is connected again. +/// +/// If verification is enabled, this function verifies the peer +/// using the system provided certificate store. If the peer +/// does not have a certificate that was signed by a certificate +/// authority i.e. a self-signed certificate, the entire certificate +/// chain can be provided using sfTcpSocket_setupTlsClientWithCertificate. +/// +/// The hostname is sent to the server via server name indication +/// (SNI) and used to verify the certificate chain returned by the +/// server. +/// +/// \param socket TCP socket object +/// \param hostname Hostname of the remote peer, encoded in UTF-8, used for verification +/// \param verifyPeer True to enable peer verification, false to disable it +/// +/// \return TLS status code +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfTlsStatus sfTcpSocket_setupTlsClient(sfTcpSocket* socket, const char* hostname, bool verifyPeer); + +//////////////////////////////////////////////////////////// +/// \brief Set up transport layer security as a client with a given certificate chain +/// +/// When calling this function, the certificate chain to verify +/// the host with has to be provided. Verification is always +/// enabled when calling this function. +/// +/// The certificate data can be provided in PEM or DER format. +/// +/// See sfTcpSocket_setupTlsClient for details on the returned +/// status codes. +/// +/// \param socket TCP socket object +/// \param hostname Hostname of the remote peer, encoded in UTF-8, used for verification +/// \param certificateChainData Certificate chain data in PEM or DER encoding +/// \param certificateChainSize Size of the certificate chain data +/// +/// \return TLS status code +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfTlsStatus sfTcpSocket_setupTlsClientWithCertificate( + sfTcpSocket* socket, + const char* hostname, + const void* certificateChainData, + size_t certificateChainSize); + +//////////////////////////////////////////////////////////// +/// \brief Set up transport layer security as a server +/// +/// Once the TCP connection is connected, transport layer +/// security can be set up. +/// +/// As a server, a certificate chain as well as a private key +/// must be provided. The certificate and private key data can +/// be provided in PEM or DER format. If the private key is +/// secured by a password, the password must be provided. +/// +/// If sfTlsError is returned, something went wrong with TLS +/// setup and the connection must be disconnected. The client +/// must reconnect and reattempt TLS setup again. +/// +/// See sfTcpSocket_setupTlsClient for details on the other +/// returned status codes. +/// +/// \param socket TCP socket object +/// \param certificateChainData Certificate chain data in PEM or DER encoding +/// \param certificateChainSize Size of the certificate chain data +/// \param privateKeyData Private key data in PEM or DER encoding +/// \param privateKeySize Size of the private key data +/// \param privateKeyPasswordData Private key password data, can be NULL if there is no password +/// \param privateKeyPasswordSize Size of the private key password data +/// +/// \return TLS status code +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfTlsStatus sfTcpSocket_setupTlsServer( + sfTcpSocket* socket, + const void* certificateChainData, + size_t certificateChainSize, + const void* privateKeyData, + size_t privateKeySize, + const void* privateKeyPasswordData, + size_t privateKeyPasswordSize); + +//////////////////////////////////////////////////////////// +/// \brief Get the name of the TLS ciphersuite currently in use +/// +/// The returned string stays valid until the next call to +/// this function or until the socket is destroyed. +/// +/// \param socket TCP socket object +/// +/// \return TLS ciphersuite currently in use or NULL if TLS is not set up +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API const char* sfTcpSocket_getCurrentCiphersuiteName(const sfTcpSocket* socket); diff --git a/src/CSFML/Network/TcpSocket.cpp b/src/CSFML/Network/TcpSocket.cpp index 5308533..70afe8e 100644 --- a/src/CSFML/Network/TcpSocket.cpp +++ b/src/CSFML/Network/TcpSocket.cpp @@ -31,7 +31,9 @@ #include #include +#include +#include #include @@ -170,3 +172,73 @@ sfSocketStatus sfTcpSocket_receivePacket(sfTcpSocket* socket, sfPacket* packet) assert(packet); return static_cast(socket->receive(*packet)); } + + +namespace +{ +//////////////////////////////////////////////////////////// +[[nodiscard]] sf::String fromUtf8(const char* string) +{ + return sf::String::fromUtf8(string, string + std::strlen(string)); +} +} // namespace + + +//////////////////////////////////////////////////////////// +sfTlsStatus sfTcpSocket_setupTlsClient(sfTcpSocket* socket, const char* hostname, bool verifyPeer) +{ + assert(socket); + assert(hostname); + return static_cast(socket->setupTlsClient(fromUtf8(hostname), verifyPeer)); +} + + +//////////////////////////////////////////////////////////// +sfTlsStatus sfTcpSocket_setupTlsClientWithCertificate(sfTcpSocket* socket, + const char* hostname, + const void* certificateChainData, + size_t certificateChainSize) +{ + assert(socket); + assert(hostname); + assert(certificateChainData); + return static_cast( + socket->setupTlsClient(fromUtf8(hostname), static_cast(certificateChainData), certificateChainSize)); +} + + +//////////////////////////////////////////////////////////// +sfTlsStatus sfTcpSocket_setupTlsServer( + sfTcpSocket* socket, + const void* certificateChainData, + size_t certificateChainSize, + const void* privateKeyData, + size_t privateKeySize, + const void* privateKeyPasswordData, + size_t privateKeyPasswordSize) +{ + assert(socket); + assert(certificateChainData); + assert(privateKeyData); + return static_cast( + socket->setupTlsServer(static_cast(certificateChainData), + certificateChainSize, + static_cast(privateKeyData), + privateKeySize, + static_cast(privateKeyPasswordData), + privateKeyPasswordData ? privateKeyPasswordSize : 0)); +} + + +//////////////////////////////////////////////////////////// +const char* sfTcpSocket_getCurrentCiphersuiteName(const sfTcpSocket* socket) +{ + assert(socket); + + const auto name = socket->getCurrentCiphersuiteName(); + if (!name) + return nullptr; + + socket->CiphersuiteName = *name; + return socket->CiphersuiteName.c_str(); +} diff --git a/src/CSFML/Network/TcpSocketStruct.hpp b/src/CSFML/Network/TcpSocketStruct.hpp index f4c8908..8c7bfe0 100644 --- a/src/CSFML/Network/TcpSocketStruct.hpp +++ b/src/CSFML/Network/TcpSocketStruct.hpp @@ -29,10 +29,13 @@ //////////////////////////////////////////////////////////// #include +#include + //////////////////////////////////////////////////////////// // Internal structure of sfTcpSocket //////////////////////////////////////////////////////////// struct sfTcpSocket : sf::TcpSocket { + mutable std::string CiphersuiteName; }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c944cab..c18fb00 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -71,6 +71,7 @@ add_executable(test-csfml-network Network/Http.test.cpp Network/IpAddress.test.cpp Network/SocketStatus.test.cpp + Network/TcpSocket.test.cpp ) target_link_libraries(test-csfml-network PRIVATE csfml-network csfml-system Catch2::Catch2WithMain SFML::Network) set_target_warnings(test-csfml-network) diff --git a/test/Network/TcpSocket.test.cpp b/test/Network/TcpSocket.test.cpp new file mode 100644 index 0000000..2db3849 --- /dev/null +++ b/test/Network/TcpSocket.test.cpp @@ -0,0 +1,24 @@ +#include + +#include + +#include + +TEST_CASE("[Network] sfTcpSocket") +{ + SECTION("sfTlsStatus") + { + STATIC_CHECK(sfTlsNotConnected == static_cast(sf::TcpSocket::TlsStatus::NotConnected)); + STATIC_CHECK(sfTlsHandshakeStarted == static_cast(sf::TcpSocket::TlsStatus::HandshakeStarted)); + STATIC_CHECK(sfTlsHandshakeComplete == static_cast(sf::TcpSocket::TlsStatus::HandshakeComplete)); + STATIC_CHECK(sfTlsError == static_cast(sf::TcpSocket::TlsStatus::Error)); + } + + SECTION("TLS on unconnected socket") + { + sfTcpSocket* socket = sfTcpSocket_create(); + CHECK(sfTcpSocket_setupTlsClient(socket, "localhost", true) == sfTlsNotConnected); + CHECK(sfTcpSocket_getCurrentCiphersuiteName(socket) == nullptr); + sfTcpSocket_destroy(socket); + } +}