From 6d786f6e29ecb88e0f8f99aa1c117dd051dc3472 Mon Sep 17 00:00:00 2001 From: wuraxor Date: Sat, 16 May 2026 20:21:14 +0300 Subject: [PATCH 1/3] Add socks5 proxy support --- CMakeLists.txt | 4 + ixwebsocket/IXHttpClient.cpp | 9 +- ixwebsocket/IXHttpClient.h | 5 + ixwebsocket/IXProxyOptions.cpp | 1 + ixwebsocket/IXProxyOptions.h | 23 ++ ixwebsocket/IXProxySocket.cpp | 406 ++++++++++++++++++++++++++++++++ ixwebsocket/IXProxySocket.h | 143 +++++++++++ ixwebsocket/IXSocketAppleSSL.h | 6 +- ixwebsocket/IXSocketFactory.cpp | 44 +++- ixwebsocket/IXSocketFactory.h | 5 +- ixwebsocket/IXSocketMbedTLS.h | 6 +- ixwebsocket/IXSocketOpenSSL.h | 6 +- 12 files changed, 639 insertions(+), 19 deletions(-) create mode 100644 ixwebsocket/IXProxyOptions.cpp create mode 100644 ixwebsocket/IXProxyOptions.h create mode 100644 ixwebsocket/IXProxySocket.cpp create mode 100644 ixwebsocket/IXProxySocket.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a010abf5..409c6608 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,6 +64,8 @@ set( IXWEBSOCKET_SOURCES ixwebsocket/IXWebSocketProxyServer.cpp ixwebsocket/IXWebSocketServer.cpp ixwebsocket/IXWebSocketTransport.cpp + ixwebsocket/IXProxyOptions.cpp + ixwebsocket/IXProxySocket.cpp ) set( IXWEBSOCKET_HEADERS @@ -117,6 +119,8 @@ set( IXWEBSOCKET_HEADERS ixwebsocket/IXWebSocketServer.h ixwebsocket/IXWebSocketTransport.h ixwebsocket/IXWebSocketVersion.h + ixwebsocket/IXProxyOptions.h + ixwebsocket/IXProxySocket.h ) option(BUILD_SHARED_LIBS "Build shared libraries (.dll/.so) instead of static ones (.lib/.a)" OFF) diff --git a/ixwebsocket/IXHttpClient.cpp b/ixwebsocket/IXHttpClient.cpp index 871be336..a0a49e3d 100644 --- a/ixwebsocket/IXHttpClient.cpp +++ b/ixwebsocket/IXHttpClient.cpp @@ -53,6 +53,11 @@ namespace ix _tlsOptions = tlsOptions; } + void HttpClient::setProxyOptions(const ProxyOptions& proxyOptions) + { + _proxyOptions = proxyOptions; + } + void HttpClient::setForceBody(bool value) { _forceBody = value; @@ -158,7 +163,9 @@ namespace ix bool tls = protocol == "https"; std::string errorMsg; - _socket = createSocket(tls, -1, errorMsg, _tlsOptions); + + bool proxy = !_proxyOptions.host.empty(); + _socket = createSocket(tls, -1, errorMsg, _tlsOptions, proxy, _proxyOptions); if (!_socket) { diff --git a/ixwebsocket/IXHttpClient.h b/ixwebsocket/IXHttpClient.h index c4b05845..bcfcffb0 100644 --- a/ixwebsocket/IXHttpClient.h +++ b/ixwebsocket/IXHttpClient.h @@ -7,6 +7,7 @@ #pragma once #include "IXHttp.h" +#include "IXProxyOptions.h" #include "IXSocket.h" #include "IXSocketTLSOptions.h" #include "IXWebSocketHttpHeaders.h" @@ -80,6 +81,9 @@ namespace ix // TLS void setTLSOptions(const SocketTLSOptions& tlsOptions); + // Proxy + void setProxyOptions(const ProxyOptions& proxyOptions); + std::string serializeHttpParameters(const HttpParameters& httpParameters); std::string serializeHttpFormDataParameters( @@ -117,6 +121,7 @@ namespace ix // might be called recursively to follow HTTP redirections SocketTLSOptions _tlsOptions; + ProxyOptions _proxyOptions; bool _forceBody; }; diff --git a/ixwebsocket/IXProxyOptions.cpp b/ixwebsocket/IXProxyOptions.cpp new file mode 100644 index 00000000..dbb61361 --- /dev/null +++ b/ixwebsocket/IXProxyOptions.cpp @@ -0,0 +1 @@ +#include "IXProxyOptions.h" diff --git a/ixwebsocket/IXProxyOptions.h b/ixwebsocket/IXProxyOptions.h new file mode 100644 index 00000000..8e4d0753 --- /dev/null +++ b/ixwebsocket/IXProxyOptions.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +enum class SocksVersion +{ + SOCKS4 = 0, + SOCKS4a = 1, + SOCKS5 = 2, + SOCKS5h = 3, +}; + +struct ProxyOptions +{ +public: + std::string host; + unsigned short port; + SocksVersion socksVersion; + std::string userId; + std::string username; + std::string password; + bool auth; +}; diff --git a/ixwebsocket/IXProxySocket.cpp b/ixwebsocket/IXProxySocket.cpp new file mode 100644 index 00000000..2d7fb3fb --- /dev/null +++ b/ixwebsocket/IXProxySocket.cpp @@ -0,0 +1,406 @@ +#include "IXProxySocket.h" + +#include "IXSocketConnect.h" +#include + +#ifdef IXWEBSOCKET_USE_TLS +#ifdef IXWEBSOCKET_USE_OPEN_SSL +#include "IXSocketOpenSSL.h" + + template <> + bool ix::ProxySocket::connect(const std::string& host, + int port, + std::string& errMsg, + const CancellationRequest& isCancellationRequested) + { + bool handshakeSuccessful = false; + { + std::lock_guard lock(_mutex); + + if (!_openSSLInitializationSuccessful) + { + errMsg = "OPENSSL_init_ssl failure"; + return false; + } + + _sockfd = SocketConnect::connect(_proxyOptions.host, _proxyOptions.port, errMsg, isCancellationRequested); + if (_sockfd == -1) return false; + + if (_proxyOptions.socksVersion == SocksVersion::SOCKS4 || _proxyOptions.socksVersion == SocksVersion::SOCKS4a) + { + if (!socks4Connect(host, port, errMsg, isCancellationRequested)) + { + Socket::close(); + return false; + } + } else + { + if (!socks5Connect(host, port, errMsg, isCancellationRequested)) + { + Socket::close(); + return false; + } + } + + _ssl_context = openSSLCreateContext(errMsg); + if (_ssl_context == nullptr) + { + return false; + } + + if (!handleTLSOptions(errMsg)) + { + return false; + } + + _ssl_connection = SSL_new(_ssl_context); + if (_ssl_connection == nullptr) + { + errMsg = "OpenSSL failed to connect"; + SSL_CTX_free(_ssl_context); + _ssl_context = nullptr; + return false; + } + SSL_set_fd(_ssl_connection, _sockfd); + + // SNI support + SSL_set_tlsext_host_name(_ssl_connection, host.c_str()); + +#if OPENSSL_VERSION_NUMBER >= 0x10002000L + // Support for server name verification + // (The docs say that this should work from 1.0.2, and is the default from + // 1.1.0, but it does not. To be on the safe side, the manual test + // below is enabled for all versions prior to 1.1.0.) + if (!_tlsOptions.disable_hostname_validation) + { + X509_VERIFY_PARAM* param = SSL_get0_param(_ssl_connection); + X509_VERIFY_PARAM_set1_host(param, host.c_str(), host.size()); + } +#endif + handshakeSuccessful = openSSLClientHandshake(host, errMsg, isCancellationRequested); + } + + if (!handshakeSuccessful) + { + close(); + return false; + } + + return true; +} + +template bool ix::ProxySocket::socks5Connect(const std::string& host, + + int port, + std::string& errMsg, + const CancellationRequest& isCancellationRequested); + + +#elif defined(IXWEBSOCKET_USE_MBED_TLS) +#include "IXSocketMbedTLS.h" + +template <> +bool ix::ProxySocket::connect(const std::string& host, + int port, + std::string& errMsg, + const CancellationRequest& isCancellationRequested) +{ + { + std::lock_guard lock(_mutex); + _sockfd = SocketConnect::connect(_proxyOptions.host, _proxyOptions.port, errMsg, isCancellationRequested); + if (_sockfd == -1) return false; + if (_proxyOptions.socksVersion == SocksVersion::SOCKS4 || _proxyOptions.socksVersion == SocksVersion::SOCKS4a) + { + if (!socks4Connect(host, port, errMsg, isCancellationRequested)) + { + Socket::close(); + return false; + } + } else + { + if (!socks5Connect(host, port, errMsg, isCancellationRequested)) + { + Socket::close(); + return false; + } + } + } + + bool isClient = true; + bool initialized = init(host, isClient, errMsg); + if (!initialized) + { + close(); + return false; + } + + mbedtls_ssl_set_bio(&_ssl, &_sockfd, mbedtls_net_send, mbedtls_net_recv, NULL); + + int res; + do + { + { + std::lock_guard lock(_mutex); + res = mbedtls_ssl_handshake(&_ssl); + } + + if (isCancellationRequested()) + { + errMsg = "Cancellation requested"; + close(); + return false; + } + } while (res == MBEDTLS_ERR_SSL_WANT_READ || res == MBEDTLS_ERR_SSL_WANT_WRITE); + + if (res != 0) + { + char buf[256]; + mbedtls_strerror(res, buf, sizeof(buf)); + + errMsg = "error in handshake : "; + errMsg += buf; + + close(); + return false; + } + + return true; +} + +template bool ix::ProxySocket::socks5Connect(const std::string& host, + + int port, + std::string& errMsg, + const CancellationRequest& isCancellationRequested); + + +#elif defined(IXWEBSOCKET_USE_SECURE_TRANSPORT) +#include "IXSocketAppleSSL.h" + +template <> +bool ix::ProxySocket::connect(const std::string& host, + int port, + std::string& errMsg, + const CancellationRequest& isCancellationRequested) +{ + OSStatus status; + { + std::lock_guard lock(_mutex); + _sockfd = SocketConnect::connect(_proxyOptions.host, _proxyOptions.port, errMsg, isCancellationRequested); + + if (_sockfd == -1) return false; + if (_proxyOptions.socksVersion == SocksVersion::SOCKS4 || _proxyOptions.socksVersion == SocksVersion::SOCKS4a) + { + if (!socks4Connect(host, port, errMsg, isCancellationRequested)) + { + Socket::close(); + return false; + } + } else + { + if (!socks5Connect(host, port, errMsg, isCancellationRequested)) + { + Socket::close(); + return false; + } + } + + _sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType); + + SSLSetIOFuncs( + _sslContext, SocketAppleSSL::readFromSocket, SocketAppleSSL::writeToSocket); + SSLSetConnection(_sslContext, (SSLConnectionRef)(long) _sockfd); + SSLSetProtocolVersionMin(_sslContext, kTLSProtocol12); + + if (!_tlsOptions.disable_hostname_validation) + SSLSetPeerDomainName(_sslContext, host.c_str(), host.size()); + + if (_tlsOptions.isPeerVerifyDisabled()) + { + Boolean option(1); + SSLSetSessionOption(_sslContext, kSSLSessionOptionBreakOnServerAuth, option); + + status = tlsHandShake(errMsg, isCancellationRequested); + + if (status == errSSLServerAuthCompleted) + { + // proceed with the handshake + status = tlsHandShake(errMsg, isCancellationRequested); + } + } + else + { + status = tlsHandShake(errMsg, isCancellationRequested); + } + } + + if (status != noErr) + { + errMsg = getSSLErrorDescription(status); + close(); + return false; + } + + return true; +} + +template bool ix::ProxySocket::socks5Connect(const std::string& host, + + int port, + std::string& errMsg, + const CancellationRequest& isCancellationRequested); + + +#endif +#endif + +template <> +bool ix::ProxySocket::socks4Connect(const std::string& host, + int port, + std::string& errMsg, + const CancellationRequest& isCancellationRequested) +{ + return false; +} + +template <> +bool ix::ProxySocket::connect(const std::string& host, + int port, + std::string& errMsg, + const CancellationRequest& isCancellationRequested) +{ + { + std::lock_guard lock(_socketMutex); + _sockfd = SocketConnect::connect(_proxyOptions.host, _proxyOptions.port, errMsg, isCancellationRequested); + } + if (_sockfd == -1) return false; + if (_proxyOptions.socksVersion == SocksVersion::SOCKS4 || _proxyOptions.socksVersion == SocksVersion::SOCKS4a) + { + if (!socks4Connect(host, port, errMsg, isCancellationRequested)) + { + Socket::close(); + return false; + } + } else + { + if (!socks5Connect(host, port, errMsg, isCancellationRequested)) + { + Socket::close(); + return false; + } + } + + return true; +} + +template +bool ix::ProxySocket::socks5Connect(const std::string& host, + int port, + std::string& errMsg, + const CancellationRequest& isCancellationRequested) + +{ + std::stringstream ss; + ss << '\x05'; + if (_proxyOptions.auth) + { + ss << '\x02' << '\x00' << '\x02'; + } else + { + ss << '\x01' << '\x00'; + } + + if (!rawWriteBytes(ss.str(), isCancellationRequested)) + { + errMsg = "Failed to perform socks5 handshake"; + return false; + } + auto resp = rawReadBytes(2, nullptr, nullptr, isCancellationRequested); + if (!resp.first) + { + errMsg = "Failed to perform socks5 handshake"; + return false; + } + ss.str(""); + if (resp.second.at(1) == '\xFF') + { + errMsg = "Socks server does not support this type of auth"; + return false; + } + if (resp.second.at(1) == '\x02') + { + auto size = static_cast(_proxyOptions.username.size()); + ss << '\x01'; + ss.write(reinterpret_cast(&size), sizeof(size)); + ss << _proxyOptions.username; + size = static_cast(_proxyOptions.password.size()); + ss.write(reinterpret_cast(&size), sizeof(size)); + ss << _proxyOptions.password; + if (!rawWriteBytes(ss.str(), isCancellationRequested)) + { + errMsg = "Failed to perform auth"; + return false; + } + resp = this->rawReadBytes(2, nullptr, nullptr, isCancellationRequested); + if (resp.second.at(1) != '\x00') + { + errMsg = "Invalid proxy auth"; + return false; + } + ss.str(""); + } + ss << '\x05' << '\x01' << '\x00' << '\x03'; + + auto rawSize = static_cast(host.size()); + ss.write(reinterpret_cast(&rawSize), sizeof(rawSize)); + ss << host; + + auto rawPort = static_cast(port); + rawPort = htons(rawPort); + + ss.write(reinterpret_cast(&rawPort), sizeof(rawPort)); + if (!rawWriteBytes (ss.str(), isCancellationRequested)) + { + errMsg = "Failed to connect to host"; + return false; + } + resp = this->rawReadBytes(4, nullptr, nullptr, isCancellationRequested); + if (resp.second.at(1) != '\x00') + { + errMsg = "Failed to connect to host"; + return false; + } + switch (resp.second.at(3)) + { + case '\x01': + { + rawReadBytes(6, nullptr, nullptr, isCancellationRequested); + break; + } + case '\x03': + { + rawReadBytes(8, nullptr, nullptr, isCancellationRequested); + break; + } + case '\x04': + { + resp = rawReadBytes(1, nullptr, nullptr, isCancellationRequested); + rawReadBytes(resp.second.at(0)+2, nullptr, nullptr, isCancellationRequested); + break; + } + default: + { + errMsg = "Somthing wrong happend"; + return false; + } + } + + return true; +} + +template bool ix::ProxySocket::socks5Connect(const std::string& host, + + int port, + std::string& errMsg, + const CancellationRequest& isCancellationRequested); + diff --git a/ixwebsocket/IXProxySocket.h b/ixwebsocket/IXProxySocket.h new file mode 100644 index 00000000..982b2578 --- /dev/null +++ b/ixwebsocket/IXProxySocket.h @@ -0,0 +1,143 @@ +#pragma once + +#include "IXProxyOptions.h" +#include "IXSocket.h" +#include "IXSocketTLSOptions.h" +#include +#include +#include +#include +#include + +namespace ix +{ + template + class ProxySocket final : public T + { + public: + static_assert(std::is_base_of::value, "T must derive from socket"); + template ::value>> + ProxySocket(const ProxyOptions& proxyOptions, int fd = -1) : + T(fd), + _proxyOptions(proxyOptions) + {} + + template ::value>> + ProxySocket(const ProxyOptions& proxyOptions, + const SocketTLSOptions& tlsOptions, + int fd = -1) : + T(tlsOptions, fd), + _proxyOptions(proxyOptions) + {} + + virtual bool connect(const std::string& host, + int port, + std::string& errMsg, + const CancellationRequest& isCancellationRequested) final; + private: + ProxyOptions _proxyOptions; + + bool socks5Connect(const std::string& host, + int port, + std::string& errMsg, + const CancellationRequest& isCancellationRequested); + + bool socks4Connect(const std::string& host, + int port, + std::string& errMsg, + const CancellationRequest& isCancellationRequested) {return false;} + + bool rawWriteBytes(const std::string& str, + const CancellationRequest& isCancellationRequested) + { + int offset = 0; + int len = (int) str.size(); + + while (true) + { + if (isCancellationRequested && isCancellationRequested()) return false; + + ssize_t ret = Socket::send((char*) &str[offset], len); + + // We wrote some bytes, as needed, all good. + if (ret > 0) + { + if (ret == len) + { + return true; + } + else + { + offset += ret; + len -= ret; + continue; + } + } + // There is possibly something to be writen, try again + else if (ret < 0 && Socket::isWaitNeeded()) + { + continue; + } + // There was an error during the write, abort + else + { + return false; + } + } + } + + std::pair rawReadBytes( + size_t length, + const OnProgressCallback& onProgressCallback, + const OnChunkCallback& onChunkCallback, + const CancellationRequest& isCancellationRequested) + { + std::array readBuffer; + std::vector output; + size_t bytesRead = 0; + + while (bytesRead != length) + { + if (isCancellationRequested && isCancellationRequested()) + { + const std::string errorMsg("Cancellation Requested"); + return std::make_pair(false, errorMsg); + } + + size_t size = std::min(readBuffer.size(), length - bytesRead); + ssize_t ret = Socket::recv((char*) &readBuffer[0], size); + + if (ret > 0) + { + if (onChunkCallback) + { + std::string chunk(readBuffer.begin(), readBuffer.begin() + ret); + onChunkCallback(chunk); + } + else + { + output.insert(output.end(), readBuffer.begin(), readBuffer.begin() + ret); + } + bytesRead += ret; + } + else if (ret <= 0 && !Socket::isWaitNeeded()) + { + const std::string errorMsg("Recv Error"); + return std::make_pair(false, errorMsg); + } + + if (onProgressCallback) onProgressCallback((int) bytesRead, (int) length); + + // Wait with a 1ms timeout until the socket is ready to read. + // This way we are not busy looping + if (Socket::isReadyToRead(1) == PollResultType::Error) + { + const std::string errorMsg("Poll Error"); + return std::make_pair(false, errorMsg); + } + } + + return std::make_pair(true, std::string(output.begin(), output.end())); + } + }; +} // namespace ix diff --git a/ixwebsocket/IXSocketAppleSSL.h b/ixwebsocket/IXSocketAppleSSL.h index a693a187..bdd64f5d 100644 --- a/ixwebsocket/IXSocketAppleSSL.h +++ b/ixwebsocket/IXSocketAppleSSL.h @@ -16,7 +16,7 @@ namespace ix { - class SocketAppleSSL final : public Socket + class SocketAppleSSL : public Socket { public: SocketAppleSSL(const SocketTLSOptions& tlsOptions, int fd = -1); @@ -27,13 +27,13 @@ namespace ix virtual bool connect(const std::string& host, int port, std::string& errMsg, - const CancellationRequest& isCancellationRequested) final; + const CancellationRequest& isCancellationRequested); virtual void close() final; virtual ssize_t send(char* buffer, size_t length) final; virtual ssize_t recv(void* buffer, size_t length) final; - private: + protected: static std::string getSSLErrorDescription(OSStatus status); static OSStatus writeToSocket(SSLConnectionRef connection, const void* data, size_t* len); static OSStatus readFromSocket(SSLConnectionRef connection, void* data, size_t* len); diff --git a/ixwebsocket/IXSocketFactory.cpp b/ixwebsocket/IXSocketFactory.cpp index e542871d..0ee6873d 100644 --- a/ixwebsocket/IXSocketFactory.cpp +++ b/ixwebsocket/IXSocketFactory.cpp @@ -6,6 +6,7 @@ #include "IXSocketFactory.h" +#include "IXProxySocket.h" #include "IXUniquePtr.h" #ifdef IXWEBSOCKET_USE_TLS @@ -28,30 +29,57 @@ namespace ix std::unique_ptr createSocket(bool tls, int fd, std::string& errorMsg, - const SocketTLSOptions& tlsOptions) + const SocketTLSOptions& tlsOptions, + bool proxy, + const ProxyOptions& proxyOptions) { (void) tlsOptions; errorMsg.clear(); std::unique_ptr socket; - if (!tls) + if (!proxy) { - socket = ix::make_unique(fd); + if (!tls) + { + socket = ix::make_unique(fd); + } + else + { +#ifdef IXWEBSOCKET_USE_TLS +#if defined(IXWEBSOCKET_USE_MBED_TLS) + socket = ix::make_unique(tlsOptions, fd); +#elif defined(IXWEBSOCKET_USE_OPEN_SSL) || defined(IXWEBSOCKET_USE_LIBRE_SSL) + socket = ix::make_unique(tlsOptions, fd); +#elif defined(__APPLE__) + socket = ix::make_unique(tlsOptions, fd); +#endif +#else + errorMsg = "TLS support is not enabled on this platform."; + return nullptr; +#endif + } } else { + if (!tls) + { + socket = ix::make_unique>(proxyOptions,fd); + } + else + { #ifdef IXWEBSOCKET_USE_TLS #if defined(IXWEBSOCKET_USE_MBED_TLS) - socket = ix::make_unique(tlsOptions, fd); + socket = ix::make_unique>(proxyOptions, tlsOptions, fd); #elif defined(IXWEBSOCKET_USE_OPEN_SSL) || defined(IXWEBSOCKET_USE_LIBRE_SSL) - socket = ix::make_unique(tlsOptions, fd); + socket = ix::make_unique>(proxyOptions, tlsOptions, fd); #elif defined(__APPLE__) - socket = ix::make_unique(tlsOptions, fd); + socket = ix::make_unique>(proxyOptions, tlsOptions, fd); #endif #else - errorMsg = "TLS support is not enabled on this platform."; - return nullptr; + errorMsg = "TLS support is not enabled on this platform."; + return nullptr; #endif + } } if (!socket->init(errorMsg)) diff --git a/ixwebsocket/IXSocketFactory.h b/ixwebsocket/IXSocketFactory.h index de1eeda6..ded570c7 100644 --- a/ixwebsocket/IXSocketFactory.h +++ b/ixwebsocket/IXSocketFactory.h @@ -7,6 +7,7 @@ #pragma once +#include "IXProxyOptions.h" #include "IXSocketTLSOptions.h" #include #include @@ -17,5 +18,7 @@ namespace ix std::unique_ptr createSocket(bool tls, int fd, std::string& errorMsg, - const SocketTLSOptions& tlsOptions); + const SocketTLSOptions& tlsOptions, + bool proxy = false, + const ProxyOptions& proxyOptions = {}); } // namespace ix diff --git a/ixwebsocket/IXSocketMbedTLS.h b/ixwebsocket/IXSocketMbedTLS.h index 9dd73f50..abc38c0b 100644 --- a/ixwebsocket/IXSocketMbedTLS.h +++ b/ixwebsocket/IXSocketMbedTLS.h @@ -21,7 +21,7 @@ namespace ix { - class SocketMbedTLS final : public Socket + class SocketMbedTLS : public Socket { public: SocketMbedTLS(const SocketTLSOptions& tlsOptions, int fd = -1); @@ -32,13 +32,13 @@ namespace ix virtual bool connect(const std::string& host, int port, std::string& errMsg, - const CancellationRequest& isCancellationRequested) final; + const CancellationRequest& isCancellationRequested); virtual void close() final; virtual ssize_t send(char* buffer, size_t length) final; virtual ssize_t recv(void* buffer, size_t length) final; - private: + protected: mbedtls_ssl_context _ssl; mbedtls_ssl_config _conf; mbedtls_entropy_context _entropy; diff --git a/ixwebsocket/IXSocketOpenSSL.h b/ixwebsocket/IXSocketOpenSSL.h index 9142bf82..5ed986dc 100644 --- a/ixwebsocket/IXSocketOpenSSL.h +++ b/ixwebsocket/IXSocketOpenSSL.h @@ -19,7 +19,7 @@ namespace ix { - class SocketOpenSSL final : public Socket + class SocketOpenSSL : public Socket { public: SocketOpenSSL(const SocketTLSOptions& tlsOptions, int fd = -1); @@ -30,13 +30,13 @@ namespace ix virtual bool connect(const std::string& host, int port, std::string& errMsg, - const CancellationRequest& isCancellationRequested) final; + const CancellationRequest& isCancellationRequested); virtual void close() final; virtual ssize_t send(char* buffer, size_t length) final; virtual ssize_t recv(void* buffer, size_t length) final; - private: + protected: void openSSLInitialize(); std::string getSSLError(int ret); SSL_CTX* openSSLCreateContext(std::string& errMsg); From 256f6c6bbbd3063fbe7d5eedfc584f3c5b3f2e24 Mon Sep 17 00:00:00 2001 From: wuraxor Date: Mon, 25 May 2026 12:40:40 +0300 Subject: [PATCH 2/3] Add websocket proxy support --- ixwebsocket/IXWebSocket.cpp | 10 ++++++++-- ixwebsocket/IXWebSocket.h | 3 +++ ixwebsocket/IXWebSocketTransport.cpp | 9 +++++++-- ixwebsocket/IXWebSocketTransport.h | 5 ++++- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/ixwebsocket/IXWebSocket.cpp b/ixwebsocket/IXWebSocket.cpp index f9b8075d..1dd2f1e7 100644 --- a/ixwebsocket/IXWebSocket.cpp +++ b/ixwebsocket/IXWebSocket.cpp @@ -98,6 +98,12 @@ namespace ix _socketTLSOptions = socketTLSOptions; } + void WebSocket::setProxyOptions(const ProxyOptions& proxyOptions) + { + std::lock_guard lock(_configMutex); + _proxyOptions = proxyOptions; + } + const WebSocketPerMessageDeflateOptions WebSocket::getPerMessageDeflateOptions() const { std::lock_guard lock(_configMutex); @@ -204,7 +210,7 @@ namespace ix { std::lock_guard lock(_configMutex); _ws.configure( - _perMessageDeflateOptions, _socketTLSOptions, _enablePong, _pingIntervalSecs); + _perMessageDeflateOptions, _socketTLSOptions, _enablePong, _pingIntervalSecs, _proxyOptions); } WebSocketHttpHeaders headers(_extraHeaders); @@ -262,7 +268,7 @@ namespace ix { std::lock_guard lock(_configMutex); _ws.configure( - _perMessageDeflateOptions, _socketTLSOptions, _enablePong, _pingIntervalSecs); + _perMessageDeflateOptions, _socketTLSOptions, _enablePong, _pingIntervalSecs, _proxyOptions); } WebSocketInitResult status = _ws.connectToSocket( diff --git a/ixwebsocket/IXWebSocket.h b/ixwebsocket/IXWebSocket.h index 1070836f..668fdbb0 100644 --- a/ixwebsocket/IXWebSocket.h +++ b/ixwebsocket/IXWebSocket.h @@ -11,6 +11,7 @@ #include "IXProgressCallback.h" #include "IXSocketTLSOptions.h" +#include "IXProxyOptions.h" #include "IXWebSocketCloseConstants.h" #include "IXWebSocketErrorInfo.h" #include "IXWebSocketHttpHeaders.h" @@ -54,6 +55,7 @@ namespace ix void setPerMessageDeflateOptions( const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions); void setTLSOptions(const SocketTLSOptions& socketTLSOptions); + void setProxyOptions(const ProxyOptions& proxyOptions); void setPingMessage(const std::string& sendMessage, SendMessageKind pingType = SendMessageKind::Ping); void setPingInterval(int pingIntervalSecs); @@ -146,6 +148,7 @@ namespace ix WebSocketPerMessageDeflateOptions _perMessageDeflateOptions; SocketTLSOptions _socketTLSOptions; + ProxyOptions _proxyOptions; mutable std::mutex _configMutex; // protect all config variables access diff --git a/ixwebsocket/IXWebSocketTransport.cpp b/ixwebsocket/IXWebSocketTransport.cpp index 39194f43..fbad36f3 100644 --- a/ixwebsocket/IXWebSocketTransport.cpp +++ b/ixwebsocket/IXWebSocketTransport.cpp @@ -92,13 +92,15 @@ namespace ix const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions, const SocketTLSOptions& socketTLSOptions, bool enablePong, - int pingIntervalSecs) + int pingIntervalSecs, + const ProxyOptions& proxyOptions) { _perMessageDeflateOptions = perMessageDeflateOptions; _enablePerMessageDeflate = _perMessageDeflateOptions.enabled(); _socketTLSOptions = socketTLSOptions; _enablePong = enablePong; _pingIntervalSecs = pingIntervalSecs; + _proxyOptions = proxyOptions; } // Client @@ -126,7 +128,10 @@ namespace ix std::string errorMsg; bool tls = protocol == "wss"; - _socket = createSocket(tls, -1, errorMsg, _socketTLSOptions); + + + bool proxy = !_proxyOptions.host.empty(); + _socket = createSocket(tls, -1, errorMsg, _socketTLSOptions, proxy, _proxyOptions); _perMessageDeflate = ix::make_unique(); if (!_socket) diff --git a/ixwebsocket/IXWebSocketTransport.h b/ixwebsocket/IXWebSocketTransport.h index f2391e15..ace16307 100644 --- a/ixwebsocket/IXWebSocketTransport.h +++ b/ixwebsocket/IXWebSocketTransport.h @@ -13,6 +13,7 @@ #include "IXCancellationRequest.h" #include "IXProgressCallback.h" #include "IXSocketTLSOptions.h" +#include "IXProxyOptions.h" #include "IXWebSocketCloseConstants.h" #include "IXWebSocketHandshake.h" #include "IXWebSocketHttpHeaders.h" @@ -77,7 +78,8 @@ namespace ix void configure(const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions, const SocketTLSOptions& socketTLSOptions, bool enablePong, - int pingIntervalSecs); + int pingIntervalSecs, + const ProxyOptions& proxyOptions = {}); // Client WebSocketInitResult connectToUrl(const std::string& url, @@ -216,6 +218,7 @@ namespace ix // Used to control TLS connection behavior SocketTLSOptions _socketTLSOptions; + ProxyOptions _proxyOptions; // Used to cancel dns lookup + socket connect + http upgrade std::atomic _requestInitCancellation; From ce1a556b8efd5644d9cf236aef09430aaffb3ba4 Mon Sep 17 00:00:00 2001 From: Benjamin Sergeant Date: Mon, 9 Nov 2026 17:54:44 -0800 Subject: [PATCH 3/3] Implement shared SOCKS5 connector for HTTP and WebSocket clients --- CMakeLists.txt | 2 + docs/usage.md | 35 ++ ixwebsocket/IXHttpClient.cpp | 7 +- ixwebsocket/IXHttpClient.h | 5 + ixwebsocket/IXProxyOptions.h | 28 ++ ixwebsocket/IXSocket.cpp | 24 +- ixwebsocket/IXSocket.h | 8 + ixwebsocket/IXSocketAppleSSL.cpp | 2 +- ixwebsocket/IXSocketFactory.cpp | 4 +- ixwebsocket/IXSocketFactory.h | 4 +- ixwebsocket/IXSocketMbedTLS.cpp | 2 +- ixwebsocket/IXSocketNetwork.cpp | 5 + ixwebsocket/IXSocketOpenSSL.cpp | 2 +- ixwebsocket/IXSocks5.cpp | 231 ++++++++++ ixwebsocket/IXSocks5.h | 29 ++ ixwebsocket/IXWebSocket.cpp | 20 +- ixwebsocket/IXWebSocket.h | 3 + ixwebsocket/IXWebSocketTransport.cpp | 6 +- ixwebsocket/IXWebSocketTransport.h | 5 +- test/CMakeLists.txt | 5 + test/IXSocks5Test.cpp | 630 +++++++++++++++++++++++++++ test/cmake/windows-dll/main.cpp | 5 + 22 files changed, 1048 insertions(+), 14 deletions(-) create mode 100644 ixwebsocket/IXProxyOptions.h create mode 100644 ixwebsocket/IXSocks5.cpp create mode 100644 ixwebsocket/IXSocks5.h create mode 100644 test/IXSocks5Test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4498070b..b32d3d0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,7 @@ set( IXWEBSOCKET_SOURCES ixwebsocket/IXSocketFactory.cpp ixwebsocket/IXSocketServer.cpp ixwebsocket/IXSocketTLSOptions.cpp + ixwebsocket/IXSocks5.cpp ixwebsocket/IXStrCaseCompare.cpp ixwebsocket/IXUdpSocket.cpp ixwebsocket/IXUrlParser.cpp @@ -102,6 +103,7 @@ set( IXWEBSOCKET_HEADERS ixwebsocket/IXHttpServer.h ixwebsocket/IXNetSystem.h ixwebsocket/IXProgressCallback.h + ixwebsocket/IXProxyOptions.h ixwebsocket/IXSelectInterrupt.h ixwebsocket/IXSelectInterruptFactory.h ixwebsocket/IXSelectInterruptPipe.h diff --git a/docs/usage.md b/docs/usage.md index ca8ef6fa..7fab6a2e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -712,3 +712,38 @@ bind address and uses the framework's TCP listen queue rather than the BSD backl setting. Pending connections are bounded by `maxConnections`. Server TLS handshakes have a ten-second limit and are interrupted when the server stops. Client connection timeouts use the caller's cancellation callback, including HTTP/WebSocket timeouts. + + +### SOCKS5 client proxies + +Configure an outbound SOCKS5 proxy before connecting a WebSocket or issuing HTTP +requests. The same options apply to HTTP, HTTPS, WS, and WSS: + +```cpp +ix::ProxyOptions proxy; +proxy.host = "127.0.0.1"; +proxy.port = 1080; // default +proxy.dnsMode = ix::ProxyOptions::DnsMode::Remote; // default: proxy resolves the destination + +webSocket.setProxyOptions(proxy); +httpClient.setProxyOptions(proxy); +``` + +Use `DnsMode::Local` to resolve the destination locally and send an IPv4 or IPv6 +address to the proxy. Numeric destinations are always sent as numeric addresses. +The proxy's own hostname is resolved locally in both modes. TLS continues to use +the original destination hostname for SNI and certificate verification. + +Set both `proxy.username` and `proxy.password` to offer SOCKS username/password +authentication as well as the unauthenticated method. Each credential must contain +1–255 bytes. Proxy authentication takes place before the destination TLS handshake. +An empty proxy host disables this proxy configuration; call `setProxyOptions({})` +to clear it. Configure options before use; WebSocket reconnections use the current +configuration. Proxy negotiation shares the existing connection timeout and +cancellation mechanism, and failures do not fall back to a direct connection. + +SOCKS5 CONNECT is supported for plain sockets, OpenSSL/LibreSSL, Mbed TLS, and +SecureTransport. SOCKS4 and HTTP CONNECT proxies are not implemented. The native +Network.framework TLS backend currently returns an explicit unsupported-proxy +error. Its native SOCKS API failed routing and fragmented-reply checks during +implementation, so it is not enabled by this feature. diff --git a/ixwebsocket/IXHttpClient.cpp b/ixwebsocket/IXHttpClient.cpp index 748797c0..1f2c7619 100644 --- a/ixwebsocket/IXHttpClient.cpp +++ b/ixwebsocket/IXHttpClient.cpp @@ -53,6 +53,11 @@ namespace ix _tlsOptions = tlsOptions; } + void HttpClient::setProxyOptions(const ProxyOptions& proxyOptions) + { + _proxyOptions = proxyOptions; + } + void HttpClient::setForceBody(bool value) { _forceBody = value; @@ -158,7 +163,7 @@ namespace ix bool tls = protocol == "https"; std::string errorMsg; - _socket = createSocket(tls, -1, errorMsg, _tlsOptions); + _socket = createSocket(tls, -1, errorMsg, _tlsOptions, _proxyOptions); if (!_socket) { diff --git a/ixwebsocket/IXHttpClient.h b/ixwebsocket/IXHttpClient.h index bffc52bf..629e4816 100644 --- a/ixwebsocket/IXHttpClient.h +++ b/ixwebsocket/IXHttpClient.h @@ -7,6 +7,7 @@ #pragma once #include "IXHttp.h" +#include "IXProxyOptions.h" #include "IXSocket.h" #include "IXSocketTLSOptions.h" #include "IXWebSocketExport.h" @@ -81,6 +82,9 @@ namespace ix // TLS void setTLSOptions(const SocketTLSOptions& tlsOptions); + // Configure the outbound SOCKS5 proxy before issuing requests. + void setProxyOptions(const ProxyOptions& proxyOptions); + std::string serializeHttpParameters(const HttpParameters& httpParameters); std::string serializeHttpFormDataParameters( @@ -118,6 +122,7 @@ namespace ix // might be called recursively to follow HTTP redirections SocketTLSOptions _tlsOptions; + ProxyOptions _proxyOptions; bool _forceBody; }; diff --git a/ixwebsocket/IXProxyOptions.h b/ixwebsocket/IXProxyOptions.h new file mode 100644 index 00000000..86bbba48 --- /dev/null +++ b/ixwebsocket/IXProxyOptions.h @@ -0,0 +1,28 @@ +/* + * IXProxyOptions.h + * Copyright (c) 2026 Machine Zone, Inc. All rights reserved. + */ +#pragma once + +#include + +namespace ix +{ + struct ProxyOptions + { + enum class DnsMode + { + Local, + Remote + }; + + // An empty host disables the SOCKS5 proxy. Configure before connecting. + std::string host; + int port = 1080; + DnsMode dnsMode = DnsMode::Remote; + + // Optional username/password authentication; supply both fields. + std::string username; + std::string password; + }; +} // namespace ix diff --git a/ixwebsocket/IXSocket.cpp b/ixwebsocket/IXSocket.cpp index a85f4307..b9cc7211 100644 --- a/ixwebsocket/IXSocket.cpp +++ b/ixwebsocket/IXSocket.cpp @@ -10,6 +10,7 @@ #include "IXSelectInterrupt.h" #include "IXSelectInterruptFactory.h" #include "IXSocketConnect.h" +#include "IXSocks5.h" #include #include #include @@ -236,10 +237,31 @@ namespace ix if (!_selectInterrupt->clear()) return false; - _sockfd = SocketConnect::connect(host, port, errMsg, isCancellationRequested); + _sockfd = connectToHost(host, port, errMsg, isCancellationRequested); return _sockfd != -1; } + void Socket::setProxyOptions(const ProxyOptions& options) + { + _proxyOptions = options; + } + + int Socket::connectToHost(const std::string& host, + int port, + std::string& error, + const CancellationRequest& cancelled) + { + if (_proxyOptions.host.empty()) return SocketConnect::connect(host, port, error, cancelled); + if (!Socks5::validate(_proxyOptions, error)) return -1; + int fd = SocketConnect::connect(_proxyOptions.host, _proxyOptions.port, error, cancelled); + if (fd != -1 && !Socks5::connect(fd, host, port, _proxyOptions, error, cancelled)) + { + Socket::closeSocket(fd); + return -1; + } + return fd; + } + void Socket::close() { std::lock_guard lock(_socketMutex); diff --git a/ixwebsocket/IXSocket.h b/ixwebsocket/IXSocket.h index 92fc1396..27027238 100644 --- a/ixwebsocket/IXSocket.h +++ b/ixwebsocket/IXSocket.h @@ -9,6 +9,7 @@ #include "IXCancellationRequest.h" #include "IXNetSystem.h" #include "IXProgressCallback.h" +#include "IXProxyOptions.h" #include "IXSelectInterrupt.h" #include "IXWebSocketExport.h" #include @@ -37,6 +38,8 @@ namespace ix Socket(int fd = -1); virtual ~Socket(); bool init(std::string& errorMsg); + // Configure before connecting; proxy options are immutable during connection setup. + void setProxyOptions(const ProxyOptions& options); // Functions to check whether there is activity on the socket PollResultType poll(int timeoutMs = kDefaultPollTimeout); @@ -86,6 +89,11 @@ namespace ix const SelectInterruptPtr& selectInterrupt); protected: + int connectToHost(const std::string& host, + int port, + std::string& error, + const CancellationRequest& cancelled); + ProxyOptions _proxyOptions; std::atomic _sockfd; std::mutex _socketMutex; diff --git a/ixwebsocket/IXSocketAppleSSL.cpp b/ixwebsocket/IXSocketAppleSSL.cpp index 84799aac..68b7261e 100644 --- a/ixwebsocket/IXSocketAppleSSL.cpp +++ b/ixwebsocket/IXSocketAppleSSL.cpp @@ -196,7 +196,7 @@ namespace ix { std::lock_guard lock(_mutex); - _sockfd = SocketConnect::connect(host, port, errMsg, isCancellationRequested); + _sockfd = connectToHost(host, port, errMsg, isCancellationRequested); if (_sockfd == -1) return false; _sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType); diff --git a/ixwebsocket/IXSocketFactory.cpp b/ixwebsocket/IXSocketFactory.cpp index 565bf29a..443abd76 100644 --- a/ixwebsocket/IXSocketFactory.cpp +++ b/ixwebsocket/IXSocketFactory.cpp @@ -30,7 +30,8 @@ namespace ix std::unique_ptr createSocket(bool tls, int fd, std::string& errorMsg, - const SocketTLSOptions& tlsOptions) + const SocketTLSOptions& tlsOptions, + const ProxyOptions& proxyOptions) { (void) tlsOptions; errorMsg.clear(); @@ -64,6 +65,7 @@ namespace ix #endif } + socket->setProxyOptions(proxyOptions); if (!socket->init(errorMsg)) { socket.reset(); diff --git a/ixwebsocket/IXSocketFactory.h b/ixwebsocket/IXSocketFactory.h index de1eeda6..93b6d026 100644 --- a/ixwebsocket/IXSocketFactory.h +++ b/ixwebsocket/IXSocketFactory.h @@ -7,6 +7,7 @@ #pragma once +#include "IXProxyOptions.h" #include "IXSocketTLSOptions.h" #include #include @@ -17,5 +18,6 @@ namespace ix std::unique_ptr createSocket(bool tls, int fd, std::string& errorMsg, - const SocketTLSOptions& tlsOptions); + const SocketTLSOptions& tlsOptions, + const ProxyOptions& proxyOptions = {}); } // namespace ix diff --git a/ixwebsocket/IXSocketMbedTLS.cpp b/ixwebsocket/IXSocketMbedTLS.cpp index d3369e00..7a85ef7d 100644 --- a/ixwebsocket/IXSocketMbedTLS.cpp +++ b/ixwebsocket/IXSocketMbedTLS.cpp @@ -308,7 +308,7 @@ namespace ix { { std::lock_guard lock(_mutex); - _sockfd = SocketConnect::connect(host, port, errMsg, isCancellationRequested); + _sockfd = connectToHost(host, port, errMsg, isCancellationRequested); if (_sockfd == -1) return false; } diff --git a/ixwebsocket/IXSocketNetwork.cpp b/ixwebsocket/IXSocketNetwork.cpp index f2d59917..58216a37 100644 --- a/ixwebsocket/IXSocketNetwork.cpp +++ b/ixwebsocket/IXSocketNetwork.cpp @@ -382,6 +382,11 @@ namespace ix error = "Invalid TLS port"; return false; } + if (!_proxyOptions.host.empty()) + { + error = "SOCKS5 is not supported by the Network.framework backend"; + return false; + } nw_parameters_t params = makeParameters(_options, false, host, error); if (!params) return false; nw_endpoint_t endpoint = diff --git a/ixwebsocket/IXSocketOpenSSL.cpp b/ixwebsocket/IXSocketOpenSSL.cpp index 8591015c..19a1c357 100644 --- a/ixwebsocket/IXSocketOpenSSL.cpp +++ b/ixwebsocket/IXSocketOpenSSL.cpp @@ -760,7 +760,7 @@ namespace ix return false; } - _sockfd = SocketConnect::connect(host, port, errMsg, isCancellationRequested); + _sockfd = connectToHost(host, port, errMsg, isCancellationRequested); if (_sockfd == -1) return false; _ssl_context = openSSLCreateContext(errMsg); diff --git a/ixwebsocket/IXSocks5.cpp b/ixwebsocket/IXSocks5.cpp new file mode 100644 index 00000000..de497d7a --- /dev/null +++ b/ixwebsocket/IXSocks5.cpp @@ -0,0 +1,231 @@ +/* + * IXSocks5.cpp + * Copyright (c) 2026 Machine Zone, Inc. All rights reserved. + */ +#include "IXSocks5.h" + +#include "IXDNSLookup.h" +#include "IXSocket.h" +#include +#include +#include +#include + +namespace ix +{ + namespace + { + bool transfer(socket_t fd, + uint8_t* data, + size_t size, + bool reading, + std::string& error, + const CancellationRequest& cancelled) + { + size_t offset = 0; + while (offset < size) + { + if (cancelled && cancelled()) + { + error = "SOCKS5 connection cancelled"; + return false; + } + auto ready = Socket::poll(reading, 10, fd, nullptr); + if (ready == PollResultType::Timeout) continue; + if (ready == PollResultType::Error) + { + error = "SOCKS5 socket readiness error"; + return false; + } + // The socket is nonblocking. Never request bytes beyond this protocol field. + int count = static_cast((std::min) (size - offset, size_t(65536))); + int flags = 0; +#ifndef _WIN32 +#ifdef MSG_NOSIGNAL + if (!reading) flags = MSG_NOSIGNAL; +#endif +#endif + auto result = + reading + ? ::recv(fd, reinterpret_cast(data + offset), count, 0) + : ::send(fd, reinterpret_cast(data + offset), count, flags); + if (result > 0) + offset += static_cast(result); + else if (result == 0) + { + error = "SOCKS5 proxy closed the connection"; + return false; + } + else if (!Socket::isWaitNeeded() && Socket::getErrno() != EINTR) + { + error = "SOCKS5 socket I/O error"; + return false; + } + } + return true; + } + + bool numericAddress(const std::string& host, std::vector& address) + { + std::array bytes; + if (inet_pton(AF_INET, host.c_str(), bytes.data()) == 1) + { + address.push_back(1); + address.insert(address.end(), bytes.begin(), bytes.begin() + 4); + return true; + } + if (inet_pton(AF_INET6, host.c_str(), bytes.data()) == 1) + { + address.push_back(4); + address.insert(address.end(), bytes.begin(), bytes.end()); + return true; + } + return false; + } + } // namespace + + bool Socks5::validate(const ProxyOptions& options, std::string& error) + { + if (options.host.empty()) return true; + if (options.port < 1 || options.port > 65535) + error = "Invalid SOCKS5 proxy port"; + else if (options.dnsMode != ProxyOptions::DnsMode::Local && + options.dnsMode != ProxyOptions::DnsMode::Remote) + error = "Invalid SOCKS5 DNS mode"; + else if ((!options.username.empty() || !options.password.empty()) && + (options.username.empty() || options.password.empty() || + options.username.size() > 255 || options.password.size() > 255)) + error = "SOCKS5 username and password must each contain 1 to 255 bytes"; + else + return true; + return false; + } + + std::string Socks5::resolveHost(const std::string& host, + int port, + std::string& error, + const CancellationRequest& cancelled) + { + auto lookup = std::make_shared(host, port); + auto addresses = lookup->resolve(error, cancelled); + if (!addresses) return {}; + for (auto address = addresses.get(); address; address = address->ai_next) + { + char result[INET6_ADDRSTRLEN]; + const void* bytes = nullptr; + if (address->ai_family == AF_INET) + bytes = &reinterpret_cast(address->ai_addr)->sin_addr; + else if (address->ai_family == AF_INET6) + bytes = &reinterpret_cast(address->ai_addr)->sin6_addr; + if (bytes && inet_ntop(address->ai_family, bytes, result, sizeof(result))) + return result; + } + error = "SOCKS5 destination has no IPv4 or IPv6 address"; + return {}; + } + + bool Socks5::connect(socket_t fd, + const std::string& host, + int port, + const ProxyOptions& options, + std::string& error, + const CancellationRequest& cancelled) + { + if (!validate(options, error)) return false; + if (port < 1 || port > 65535 || host.empty()) + { + error = "Invalid SOCKS5 destination"; + return false; + } + std::vector address; + if (!numericAddress(host, address)) + { + if (options.dnsMode == ProxyOptions::DnsMode::Local) + { + auto resolved = resolveHost(host, port, error, cancelled); + if (resolved.empty() || !numericAddress(resolved, address)) return false; + } + else + { + if (host.size() > 255) + { + error = "SOCKS5 destination hostname exceeds 255 bytes"; + return false; + } + address = {3, static_cast(host.size())}; + address.insert(address.end(), host.begin(), host.end()); + } + } + const bool credentials = !options.username.empty(); + std::vector greeting = {5, static_cast(credentials ? 2 : 1), 0}; + if (credentials) greeting.push_back(2); + uint8_t response[4]; + if (!transfer(fd, greeting.data(), greeting.size(), false, error, cancelled) || + !transfer(fd, response, 2, true, error, cancelled)) + return false; + if (response[0] != 5 || (response[1] != 0 && !(credentials && response[1] == 2))) + { + error = "SOCKS5 proxy rejected the offered authentication method"; + return false; + } + if (response[1] == 2) + { + std::vector authentication = {1, + static_cast(options.username.size())}; + authentication.insert( + authentication.end(), options.username.begin(), options.username.end()); + authentication.push_back(static_cast(options.password.size())); + authentication.insert( + authentication.end(), options.password.begin(), options.password.end()); + if (!transfer( + fd, authentication.data(), authentication.size(), false, error, cancelled) || + !transfer(fd, response, 2, true, error, cancelled)) + return false; + if (response[0] != 1 || response[1] != 0) + { + error = "SOCKS5 username/password authentication failed"; + return false; + } + } + std::vector request = {5, 1, 0}; + request.insert(request.end(), address.begin(), address.end()); + request.push_back(static_cast(port >> 8)); + request.push_back(static_cast(port & 255)); + if (!transfer(fd, request.data(), request.size(), false, error, cancelled) || + !transfer(fd, response, 4, true, error, cancelled)) + return false; + if (response[0] != 5 || response[2] != 0) + { + error = "Invalid SOCKS5 CONNECT reply"; + return false; + } + if (response[1] != 0) + { + error = "SOCKS5 CONNECT failed (reply " + std::to_string(response[1]) + ")"; + return false; + } + size_t size = 0; + if (response[3] == 1) + size = 4; + else if (response[3] == 4) + size = 16; + else if (response[3] == 3) + { + uint8_t length; + if (!transfer(fd, &length, 1, true, error, cancelled)) return false; + size = length; + if (!size) + { + error = "Invalid SOCKS5 reply hostname length"; + return false; + } + } + else + { + error = "Invalid SOCKS5 reply address type"; + return false; + } + std::array bound; + return transfer(fd, bound.data(), size + 2, true, error, cancelled); + } +} // namespace ix diff --git a/ixwebsocket/IXSocks5.h b/ixwebsocket/IXSocks5.h new file mode 100644 index 00000000..654019ec --- /dev/null +++ b/ixwebsocket/IXSocks5.h @@ -0,0 +1,29 @@ +/* + * IXSocks5.h + * Copyright (c) 2026 Machine Zone, Inc. All rights reserved. + */ +#pragma once + +#include "IXCancellationRequest.h" +#include "IXNetSystem.h" +#include "IXProxyOptions.h" +#include + +namespace ix +{ + class Socks5 + { + public: + static bool validate(const ProxyOptions& options, std::string& error); + static std::string resolveHost(const std::string& host, + int port, + std::string& error, + const CancellationRequest& cancelled); + static bool connect(socket_t fd, + const std::string& host, + int port, + const ProxyOptions& options, + std::string& error, + const CancellationRequest& cancelled); + }; +} // namespace ix diff --git a/ixwebsocket/IXWebSocket.cpp b/ixwebsocket/IXWebSocket.cpp index d9bcb6f9..7fd72fa3 100644 --- a/ixwebsocket/IXWebSocket.cpp +++ b/ixwebsocket/IXWebSocket.cpp @@ -104,6 +104,12 @@ namespace ix _socketTLSOptions = socketTLSOptions; } + void WebSocket::setProxyOptions(const ProxyOptions& proxyOptions) + { + std::lock_guard lock(_configMutex); + _proxyOptions = proxyOptions; + } + const WebSocketPerMessageDeflateOptions WebSocket::getPerMessageDeflateOptions() const { std::lock_guard lock(_configMutex); @@ -228,8 +234,11 @@ namespace ix std::string url; { std::lock_guard lock(_configMutex); - _ws.configure( - _perMessageDeflateOptions, _socketTLSOptions, _enablePong, _pingIntervalSecs); + _ws.configure(_perMessageDeflateOptions, + _socketTLSOptions, + _enablePong, + _pingIntervalSecs, + _proxyOptions); headers = _extraHeaders; disabledDefaultHeaders = _disabledDefaultHeaders; subProtocols = _subProtocols; @@ -295,8 +304,11 @@ namespace ix } { std::lock_guard lock(_configMutex); - _ws.configure( - _perMessageDeflateOptions, _socketTLSOptions, _enablePong, _pingIntervalSecs); + _ws.configure(_perMessageDeflateOptions, + _socketTLSOptions, + _enablePong, + _pingIntervalSecs, + _proxyOptions); } WebSocketInitResult status = _ws.connectToSocket( diff --git a/ixwebsocket/IXWebSocket.h b/ixwebsocket/IXWebSocket.h index 1ad0332b..0ee1d53a 100644 --- a/ixwebsocket/IXWebSocket.h +++ b/ixwebsocket/IXWebSocket.h @@ -10,6 +10,7 @@ #pragma once #include "IXProgressCallback.h" +#include "IXProxyOptions.h" #include "IXSocketTLSOptions.h" #include "IXWebSocketCloseConstants.h" #include "IXWebSocketErrorInfo.h" @@ -58,6 +59,7 @@ namespace ix void setPerMessageDeflateOptions( const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions); void setTLSOptions(const SocketTLSOptions& socketTLSOptions); + void setProxyOptions(const ProxyOptions& proxyOptions); void setPingMessage(const std::string& sendMessage, SendMessageKind pingType = SendMessageKind::Ping); void setPingInterval(int pingIntervalSecs); @@ -152,6 +154,7 @@ namespace ix WebSocketPerMessageDeflateOptions _perMessageDeflateOptions; SocketTLSOptions _socketTLSOptions; + ProxyOptions _proxyOptions; mutable std::mutex _configMutex; // protect all config variables access diff --git a/ixwebsocket/IXWebSocketTransport.cpp b/ixwebsocket/IXWebSocketTransport.cpp index 35333f05..1411595b 100644 --- a/ixwebsocket/IXWebSocketTransport.cpp +++ b/ixwebsocket/IXWebSocketTransport.cpp @@ -93,11 +93,13 @@ namespace ix const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions, const SocketTLSOptions& socketTLSOptions, bool enablePong, - int pingIntervalSecs) + int pingIntervalSecs, + const ProxyOptions& proxyOptions) { _perMessageDeflateOptions = perMessageDeflateOptions; _enablePerMessageDeflate = _perMessageDeflateOptions.enabled(); _socketTLSOptions = socketTLSOptions; + _proxyOptions = proxyOptions; _enablePong = enablePong; _pingIntervalSecs = pingIntervalSecs; } @@ -130,7 +132,7 @@ namespace ix std::string errorMsg; bool tls = protocol == "wss"; - _socket = createSocket(tls, -1, errorMsg, _socketTLSOptions); + _socket = createSocket(tls, -1, errorMsg, _socketTLSOptions, _proxyOptions); _perMessageDeflate = ix::make_unique(); if (!_socket) diff --git a/ixwebsocket/IXWebSocketTransport.h b/ixwebsocket/IXWebSocketTransport.h index 498159b5..51931e83 100644 --- a/ixwebsocket/IXWebSocketTransport.h +++ b/ixwebsocket/IXWebSocketTransport.h @@ -14,6 +14,7 @@ #include "IXCancellationRequest.h" #include "IXProgressCallback.h" +#include "IXProxyOptions.h" #include "IXSocketTLSOptions.h" #include "IXUtf8Validator.h" #include "IXWebSocketCloseConstants.h" @@ -80,7 +81,8 @@ namespace ix void configure(const WebSocketPerMessageDeflateOptions& perMessageDeflateOptions, const SocketTLSOptions& socketTLSOptions, bool enablePong, - int pingIntervalSecs); + int pingIntervalSecs, + const ProxyOptions& proxyOptions = {}); // Client WebSocketInitResult connectToUrl( @@ -229,6 +231,7 @@ namespace ix // Used to control TLS connection behavior SocketTLSOptions _socketTLSOptions; + ProxyOptions _proxyOptions; // Used to cancel dns lookup + socket connect + http upgrade std::atomic _requestInitCancellation; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 37d8ab3c..991abe64 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -10,6 +10,7 @@ option(USE_TLS "Add TLS support" ON) # Shared sources set (TEST_TARGET_NAMES IXSocketTest + IXSocks5Test IXSocketConnectTest IXWebSocketServerTest IXWebSocketTestConnectionDisconnection @@ -124,6 +125,8 @@ add_test(NAME IXWebSocketBuildSettings -G "${CMAKE_GENERATOR}") set_tests_properties(IXWebSocketBuildSettings PROPERTIES TIMEOUT 30) +set_tests_properties(IXSocks5Test PROPERTIES TIMEOUT 30) + set_tests_properties(IXWebSocketReconnectTest IXWebSocketDispatchTest IXWebSocketSendFailureTest PROPERTIES TIMEOUT 30) if (USE_NETWORK_FRAMEWORK OR (UNIX AND USE_TLS AND (USE_OPEN_SSL OR USE_LIBRE_SSL OR USE_MBED_TLS))) @@ -138,6 +141,8 @@ if (USE_NETWORK_FRAMEWORK OR (UNIX AND USE_TLS AND (USE_OPEN_SSL OR USE_LIBRE_SS DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/generate_network_certs.sh" VERBATIM) add_custom_target(tls_test_certs DEPENDS "${TLS_TEST_CERTS}/ready") + add_dependencies(IXSocks5Test tls_test_certs) + target_compile_definitions(IXSocks5Test PRIVATE IX_SOCKS_TEST_CERTS="${TLS_TEST_CERTS}") add_executable(IXSocketTLSServerTest IXSocketTLSServerTest.cpp) add_dependencies(IXSocketTLSServerTest tls_test_certs) target_compile_definitions(IXSocketTLSServerTest PRIVATE IX_TLS_SERVER_TEST_CERTS="${TLS_TEST_CERTS}") diff --git a/test/IXSocks5Test.cpp b/test/IXSocks5Test.cpp new file mode 100644 index 00000000..9e7f2e17 --- /dev/null +++ b/test/IXSocks5Test.cpp @@ -0,0 +1,630 @@ +/* + * IXSocks5Test.cpp + * Copyright (c) 2026 Machine Zone, Inc. All rights reserved. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + using Clock = std::chrono::steady_clock; + enum class Reply + { + IPv4, + Domain, + IPv6, + Truncated, + Version, + Method, + UnofferedAuth, + ConnectVersion, + Reserved, + Auth, + AuthVersion, + Rejected, + AddressType, + EmptyDomain, + Stall + }; + + class SocksProxy final : public ix::SocketServer + { + public: + struct Request + { + unsigned addressType = 0; + std::string host; + int port = 0; + std::string username; + std::string password; + }; + explicit SocksProxy(Reply reply = Reply::IPv4, bool auth = false, bool tunnel = false) + : SocketServer(ix::getFreePort(), "127.0.0.1") + , _reply(reply) + , _auth(auth) + , _tunnel(tunnel) + { + } + ~SocksProxy() override + { + stop(); + } + void stop() override + { + _stopping = true; + SocketServer::stop(); + } + void listenAndStart() + { + REQUIRE(listen().first); + start(); + } + ix::ProxyOptions options() + { + ix::ProxyOptions result; + result.host = "127.0.0.1"; + result.port = getPort(); + if (_auth) + { + result.username = "test-user"; + result.password = "test-password"; + } + return result; + } + Request request() + { + std::lock_guard lock(_mutex); + return _request; + } + std::atomic connections {0}; + std::atomic greetings {0}; + + private: + size_t getConnectedClientsCount() override + { + return 0; + } + void handleConnection(std::unique_ptr socket, + std::shared_ptr state) override + { + ++connections; + serve(*socket); + state->setTerminated(); + } + void serve(ix::Socket& socket) + { + const auto deadline = Clock::now() + std::chrono::seconds(5); + auto cancelled = [&] { return _stopping || Clock::now() >= deadline; }; + auto read = [&](size_t size, std::string& value) + { + auto result = socket.readBytes(size, nullptr, nullptr, cancelled); + value = result.second; + return result.first; + }; + auto write = [&](const std::string& value) + { + // Fragment unit-test replies; relay integration uses normal writes. + if (_tunnel) return socket.writeBytes(value, cancelled); + for (char byte : value) + { + if (!socket.writeBytes(std::string(1, byte), cancelled)) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return true; + }; + std::string header, value; + if (!read(2, header) || !read(static_cast(header[1]), value)) return; + ++greetings; + if (_reply == Reply::Stall) + { + while (!cancelled()) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + return; + } + if (_reply == Reply::Version) + { + write(std::string("\x04\x00", 2)); + return; + } + if (_reply == Reply::Method) + { + write(std::string("\x05\x7f", 2)); + return; + } + if (_reply == Reply::UnofferedAuth) + { + write(std::string("\x05\x02", 2)); + return; + } + Request request; + if (!write(std::string("\x05", 1) + char(_auth ? 2 : 0))) return; + if (_auth) + { + if (!read(2, header) || !read(static_cast(header[1]), request.username) || + !read(1, header) || !read(static_cast(header[0]), request.password)) + return; + if (!write(std::string(_reply == Reply::AuthVersion ? "\x02" : "\x01", 1) + + char(_reply == Reply::Auth ? 1 : 0))) + return; + if (_reply == Reply::Auth || _reply == Reply::AuthVersion) return; + } + if (!read(4, header)) return; + request.addressType = static_cast(header[3]); + if (request.addressType == 3) + { + if (!read(1, header) || !read(static_cast(header[0]), request.host)) + return; + } + else + { + if (!read(request.addressType == 1 ? 4 : 16, value)) return; + char numeric[INET6_ADDRSTRLEN]; + if (!ix::inet_ntop(request.addressType == 1 ? AF_INET : AF_INET6, + value.data(), + numeric, + sizeof(numeric))) + return; + request.host = numeric; + } + if (!read(2, value)) return; + request.port = (static_cast(value[0]) << 8) | static_cast(value[1]); + { + std::lock_guard lock(_mutex); + _request = request; + } + if (_reply == Reply::ConnectVersion) + { + write(std::string("\x04\x00\x00\x01", 4)); + return; + } + if (_reply == Reply::Reserved) + { + write(std::string("\x05\x00\x01\x01", 4)); + return; + } + if (_reply == Reply::Truncated) + { + write(std::string("\x05\x00\x00\x01\x7f", 5)); + return; + } + if (_reply == Reply::Rejected) + { + write(std::string("\x05\x05\x00\x01", 4)); + return; + } + if (_reply == Reply::AddressType) + { + write(std::string("\x05\x00\x00\x7f", 4)); + return; + } + if (_reply == Reply::EmptyDomain) + { + write(std::string("\x05\x00\x00\x03\x00", 5)); + return; + } + std::string bound; + if (_reply == Reply::IPv4) + bound = std::string("\x01\x7f\x00\x00\x01", 5); + else if (_reply == Reply::Domain) + bound = std::string("\x03\x09localhost", 11); + else + bound = std::string("\x04", 1) + std::string(15, '\0') + '\x01'; + std::string reply = std::string("\x05\x00\x00", 3) + bound + std::string("\x00\x50", 2); + std::string error; + auto upstream = _tunnel ? ix::createSocket(false, -1, error, {}) : nullptr; + if (_tunnel && + (!upstream || !upstream->connect("127.0.0.1", request.port, error, cancelled))) + return; + if (!write(reply)) return; + if (!_tunnel) + { + write("payload"); + return; + } + char buffer[16384]; + while (!cancelled()) + { + for (int direction = 0; direction != 2; ++direction) + { + auto& from = direction ? *upstream : socket; + auto& to = direction ? socket : *upstream; + auto count = from.recv(buffer, sizeof(buffer)); + if (count > 0) + { + if (!to.writeBytes(std::string(buffer, static_cast(count)), + cancelled)) + return; + } + else if (count == 0 || !ix::Socket::isWaitNeeded()) + return; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + Reply _reply; + bool _auth; + bool _tunnel; + std::atomic _stopping {false}; + std::mutex _mutex; + Request _request; + }; + + auto deadline() -> ix::CancellationRequest + { + auto end = Clock::now() + std::chrono::seconds(2); + return [end] { return Clock::now() >= end; }; + } +} // namespace + +TEST_CASE("SOCKS5 consumes exactly the proxy reply", "[socks5]") +{ + auto reply = GENERATE(Reply::IPv4, Reply::Domain, Reply::IPv6); + bool auth = GENERATE(false, true); + SocksProxy proxy(reply, auth); + proxy.listenAndStart(); + std::string error; + auto socket = ix::createSocket(false, -1, error, {}, proxy.options()); + REQUIRE(socket); + REQUIRE(socket->connect("destination.invalid", 80, error, deadline())); + auto body = socket->readBytes(7, nullptr, nullptr, deadline()); + REQUIRE(body.first); + CHECK(body.second == "payload"); + auto request = proxy.request(); + CHECK(request.addressType == 3); + CHECK(request.host == "destination.invalid"); + CHECK(request.port == 80); + if (auth) CHECK(request.username == "test-user"); +} + +TEST_CASE("SOCKS5 rejects malformed and incomplete proxy replies", "[socks5]") +{ + auto reply = GENERATE(Reply::Truncated, + Reply::Version, + Reply::Method, + Reply::UnofferedAuth, + Reply::ConnectVersion, + Reply::Reserved, + Reply::Auth, + Reply::AuthVersion, + Reply::Rejected, + Reply::AddressType, + Reply::EmptyDomain); + bool auth = reply == Reply::Auth || reply == Reply::AuthVersion; + SocksProxy proxy(reply, auth); + proxy.listenAndStart(); + std::string error; + auto socket = ix::createSocket(false, -1, error, {}, proxy.options()); + REQUIRE(socket); + CHECK_FALSE(socket->connect("destination.invalid", 443, error, deadline())); + CHECK_FALSE(error.empty()); +} + +TEST_CASE("SOCKS5 supports local DNS and numeric destinations", "[socks5]") +{ + const std::string host = + GENERATE(std::string("localhost"), std::string("127.0.0.1"), std::string("::1")); + SocksProxy proxy; + proxy.listenAndStart(); + auto options = proxy.options(); + options.dnsMode = ix::ProxyOptions::DnsMode::Local; + std::string error; + auto socket = ix::createSocket(false, -1, error, {}, options); + REQUIRE(socket->connect(host, 443, error, deadline())); + auto request = proxy.request(); + CHECK((request.addressType == 1 || request.addressType == 4)); + if (host != "localhost") CHECK(request.host == host); +} + +TEST_CASE("SOCKS5 validates options before connecting", "[socks5]") +{ + SocksProxy proxy; + proxy.listenAndStart(); + auto options = proxy.options(); + SECTION("Port") + { + options.port = GENERATE(-1, 0, 65536); + } + SECTION("Username length") + { + options.username.assign(256, 'u'); + options.password = "password"; + } + SECTION("Missing password") + { + options.username = "user"; + } + SECTION("DNS mode") + { + options.dnsMode = static_cast(42); + } + std::string error; + auto socket = ix::createSocket(false, -1, error, {}, options); + CHECK_FALSE(socket->connect("destination.invalid", 443, error, deadline())); + CHECK_FALSE(error.empty()); + CHECK(proxy.connections == 0); +} + +TEST_CASE("SOCKS5 connection cancellation remains responsive", "[socks5]") +{ +#if defined(IX_SOCKS_TEST_CERTS) && !defined(IXWEBSOCKET_USE_NETWORK_FRAMEWORK) + bool secure = GENERATE(false, true); +#else + bool secure = false; +#endif + SocksProxy proxy(Reply::Stall); + proxy.listenAndStart(); + ix::WebSocket client; + client.disableAutomaticReconnection(); + client.setUrl(std::string(secure ? "wss" : "ws") + "://destination.invalid/"); + client.setProxyOptions(proxy.options()); + client.setOnMessageCallback([](const ix::WebSocketMessagePtr&) { }); + client.start(); + auto end = Clock::now() + std::chrono::seconds(2); + while (!proxy.greetings && Clock::now() < end) + std::this_thread::yield(); + auto stop = std::async(std::launch::async, [&] { client.stop(); }); + auto ready = stop.wait_for(std::chrono::milliseconds(500)); + proxy.stop(); + stop.get(); + CHECK(proxy.greetings == 1); + CHECK(ready == std::future_status::ready); +} + +namespace +{ + bool secureAvailable() + { +#if defined(IX_SOCKS_TEST_CERTS) && !defined(IXWEBSOCKET_USE_NETWORK_FRAMEWORK) + return true; +#else + return false; +#endif + } + + void configureTLS(ix::HttpServer& server, bool secure) + { +#ifdef IX_SOCKS_TEST_CERTS + if (secure) + { + ix::SocketTLSOptions options; + options.tls = true; + // This certificate is valid only for wrong.invalid, not the proxy address. + options.certFile = std::string(IX_SOCKS_TEST_CERTS) + "/wrong.pem"; + options.keyFile = std::string(IX_SOCKS_TEST_CERTS) + "/wrong.key"; + options.caFile = "NONE"; + server.setTLSOptions(options); + } +#else + (void) server; + (void) secure; +#endif + } + + ix::SocketTLSOptions clientTLS() + { + ix::SocketTLSOptions options; +#ifdef IX_SOCKS_TEST_CERTS + options.caFile = std::string(IX_SOCKS_TEST_CERTS) + "/ca.pem"; +#endif + return options; + } +} // namespace + +TEST_CASE("HTTP and HTTPS retain the origin identity through SOCKS5", "[socks5][http]") +{ + bool secure = GENERATE(false, true); + if (secure && !secureAvailable()) return; + bool auth = GENERATE(false, true); + ix::HttpServer origin(ix::getFreePort(), "127.0.0.1"); + configureTLS(origin, secure); + origin.setOnConnectionCallback( + [](ix::HttpRequestPtr request, std::shared_ptr) + { + return std::make_shared( + 200, "OK", ix::HttpErrorCode::Ok, ix::WebSocketHttpHeaders(), request->uri); + }); + REQUIRE(origin.listen().first); + origin.start(); + SocksProxy proxy(Reply::IPv4, auth, true); + proxy.listenAndStart(); + ix::HttpClient client; + client.setTLSOptions(clientTLS()); + client.setProxyOptions(proxy.options()); + auto request = client.createRequest(); + request->connectTimeout = 2; + request->transferTimeout = 2; + const auto url = std::string(secure ? "https" : "http") + + "://wrong.invalid:" + std::to_string(origin.getPort()) + "/through-proxy"; + auto response = client.get(url, request); + proxy.stop(); + origin.stop(); + INFO(response->errorMsg); + INFO("proxy connections: " << proxy.connections << ", greetings: " << proxy.greetings); + INFO("target: " << proxy.request().host << ":" << proxy.request().port + << ", type: " << proxy.request().addressType); + CAPTURE(secure, auth); + REQUIRE(response->errorCode == ix::HttpErrorCode::Ok); + CHECK(response->statusCode == 200); + CHECK(response->body == "/through-proxy"); + CHECK(proxy.request().host == "wrong.invalid"); +} + +TEST_CASE("WebSocket reconnects through the configured SOCKS5 proxy", "[socks5][websocket]") +{ + bool secure = GENERATE(false, true); + if (secure && !secureAvailable()) return; + bool auth = GENERATE(false, true); + ix::HttpServer origin(ix::getFreePort(), "127.0.0.1"); + configureTLS(origin, secure); + origin.setOnClientMessageCallback( + [](std::shared_ptr, + ix::WebSocket& socket, + const ix::WebSocketMessagePtr& message) + { + if (message->type == ix::WebSocketMessageType::Message) + { + socket.send(message->str); + socket.close(); + } + }); + REQUIRE(origin.listen().first); + origin.start(); + SocksProxy proxy(Reply::IPv4, auth, true); + proxy.listenAndStart(); + std::promise received; + auto result = received.get_future(); + std::string error; + unsigned count = 0; + ix::WebSocket client; + client.setTLSOptions(clientTLS()); + client.setProxyOptions(proxy.options()); + client.setUrl(std::string(secure ? "wss" : "ws") + + "://wrong.invalid:" + std::to_string(origin.getPort()) + "/"); + client.setMinWaitBetweenReconnectionRetries(1); + client.setMaxWaitBetweenReconnectionRetries(10); + client.setOnMessageCallback( + [&](const ix::WebSocketMessagePtr& message) + { + if (message->type == ix::WebSocketMessageType::Open) client.sendText("echo"); + if (message->type == ix::WebSocketMessageType::Message && message->str == "echo" && + ++count == 2) + received.set_value(); + if (message->type == ix::WebSocketMessageType::Error) error = message->errorInfo.reason; + }); + client.start(); + auto ready = result.wait_for(std::chrono::seconds(3)); + client.stop(); + proxy.stop(); + origin.stop(); + INFO(error); + REQUIRE(ready == std::future_status::ready); + CHECK(proxy.connections >= 2); +} + +TEST_CASE("Rejected SOCKS5 tunnels never fall back to a direct connection", "[socks5]") +{ + bool secure = GENERATE(false, true); + if (secure && !secureAvailable()) return; + std::atomic requests {0}; + ix::HttpServer origin(ix::getFreePort(), "127.0.0.1"); + configureTLS(origin, secure); +#ifdef IX_SOCKS_TEST_CERTS + if (secure) + { + ix::SocketTLSOptions tls; + tls.tls = true; + tls.certFile = std::string(IX_SOCKS_TEST_CERTS) + "/server.pem"; + tls.keyFile = std::string(IX_SOCKS_TEST_CERTS) + "/server.key"; + tls.caFile = "NONE"; + origin.setTLSOptions(tls); + } +#endif + origin.setOnConnectionCallback( + [&](ix::HttpRequestPtr, std::shared_ptr) + { + ++requests; + return std::make_shared(200, "OK"); + }); + REQUIRE(origin.listen().first); + origin.start(); + SocksProxy proxy(Reply::Rejected); + proxy.listenAndStart(); + ix::HttpClient client; + client.setTLSOptions(clientTLS()); + client.setProxyOptions(proxy.options()); + auto request = client.createRequest(); + request->connectTimeout = 2; + request->transferTimeout = 2; + auto response = client.get(std::string(secure ? "https" : "http") + + "://localhost:" + std::to_string(origin.getPort()) + "/", + request); + proxy.stop(); + origin.stop(); + CHECK(response->errorCode != ix::HttpErrorCode::Ok); + CHECK(requests == 0); + CHECK(proxy.connections == 1); +} + +#if defined(IX_SOCKS_TEST_CERTS) && !defined(IXWEBSOCKET_USE_NETWORK_FRAMEWORK) +TEST_CASE("TLS through SOCKS5 still rejects the wrong destination certificate", "[socks5][tls]") +{ + ix::HttpServer origin(ix::getFreePort(), "127.0.0.1"); + configureTLS(origin, true); + origin.setOnConnectionCallback([](ix::HttpRequestPtr, std::shared_ptr) + { return std::make_shared(200, "OK"); }); + REQUIRE(origin.listen().first); + origin.start(); + SocksProxy proxy(Reply::IPv4, false, true); + proxy.listenAndStart(); + ix::HttpClient client; + client.setTLSOptions(clientTLS()); + client.setProxyOptions(proxy.options()); + auto request = client.createRequest(); + request->connectTimeout = 2; + auto response = + client.get("https://other.invalid:" + std::to_string(origin.getPort()) + "/", request); + proxy.stop(); + origin.stop(); + CHECK(response->errorCode != ix::HttpErrorCode::Ok); +} +#endif + +#ifdef IXWEBSOCKET_USE_NETWORK_FRAMEWORK +TEST_CASE("Network.framework rejects unsupported proxy configuration", "[socks5][network_tls]") +{ + SocksProxy proxy; + proxy.listenAndStart(); + std::string error; + auto socket = ix::createSocket(true, -1, error, clientTLS(), proxy.options()); + REQUIRE(socket); + CHECK_FALSE(socket->connect("localhost", 443, error, deadline())); + CHECK(error == "SOCKS5 is not supported by the Network.framework backend"); + CHECK(proxy.connections == 0); +} +#endif + +TEST_CASE("SOCKS5 bounds hostname and credential lengths", "[socks5]") +{ + const size_t size = GENERATE(size_t(255), size_t(256)); + SocksProxy proxy(Reply::IPv4, true); + proxy.listenAndStart(); + auto options = proxy.options(); + options.username.assign(255, 'u'); + options.password.assign(255, 'p'); + std::string error; + auto socket = ix::createSocket(false, -1, error, {}, options); + bool connected = socket->connect(std::string(size, 'a'), 443, error, deadline()); + CHECK(connected == (size == 255)); + if (connected) + { + CHECK(proxy.request().host.size() == 255); + CHECK(proxy.request().username == options.username); + CHECK(proxy.request().password == options.password); + } + else + CHECK_FALSE(error.empty()); +} + +TEST_CASE("SOCKS5 observes the connection deadline", "[socks5]") +{ + SocksProxy proxy(Reply::Stall); + proxy.listenAndStart(); + std::string error; + auto socket = ix::createSocket(false, -1, error, {}, proxy.options()); + const auto start = Clock::now(); + CHECK_FALSE(socket->connect("destination.invalid", + 443, + error, + [start] + { return Clock::now() - start >= std::chrono::milliseconds(80); })); + CHECK(Clock::now() - start < std::chrono::milliseconds(500)); + CHECK_FALSE(error.empty()); +} diff --git a/test/cmake/windows-dll/main.cpp b/test/cmake/windows-dll/main.cpp index 234a2ac0..b419f047 100644 --- a/test/cmake/windows-dll/main.cpp +++ b/test/cmake/windows-dll/main.cpp @@ -11,6 +11,11 @@ int main() if (!ix::initNetSystem()) return 1; { ix::WebSocket client; + ix::ProxyOptions proxy; + proxy.host = "127.0.0.1"; + client.setProxyOptions(proxy); + ix::HttpClient httpClient; + httpClient.setProxyOptions(proxy); client.close(); ix::WebSocketServer server; ix::HttpServer http;