chronoxor / CppServer

Ultra fast and low latency asynchronous socket server & client C++ library with support TCP, SSL, UDP, HTTP, HTTPS, WebSocket protocols and 10K connections problem solution
MIT License
1.39k stars 282 forks source link

How to set SNI & connect to wss:// ? #71

Open davidtwomey opened 2 years ago

davidtwomey commented 2 years ago

Hi @chronoxor,

Really liking the design of CppServer!....however, unfortunately, I am running into issues attempting to connect to a wss:// endpoint.

In general, I am unable to get a simple example of connecting working so any help explaining how to correctly resolve and connect to a websocket endpoint specified by a wss:// uri would be greatly appreciated!

For example to connect to wss://ws.okx.com:8443/ws/v5/public (taken from here) I attempted the following based on the wss client example:

 // ...int main() {

  // Create a new Asio service
  auto service = std::make_shared<AsioService>();
  service->Start();

  // Create and prepare a new SSL client context
  auto context = std::make_shared<CppServer::Asio::SSLContext>(asio::ssl::context::tlsv12);
  context->set_default_verify_paths();
  context->set_root_certs();
  context->set_verify_mode(asio::ssl::verify_peer | asio::ssl::verify_fail_if_no_peer_cert);
  context->load_verify_file("../external/CppServer/tools/certificates/ca.pem");

  // Use TCPResolver to lookup DNS
  // See: https://github.com/chronoxor/CppServer/issues/55
  auto resolver = std::make_shared<CppServer::Asio::TCPResolver>(service);

 // @NOTE ClientWebsocket is a child class of `public CppServer::WS::WSSClient`
  auto ws = std::make_shared<ClientWebsocket>(service, context, "ws.okx.com", 8443);
  ws->Connect(resolver);

and the upgrade request attempt:


class ClientWebsocket : public CppServer::WS::WSSClient
{
public:
  using CppServer::WS::WSSClient::WSSClient;
protected:
  void onWSConnecting(CppServer::HTTP::HTTPRequest &request) override
  {
    request.SetBegin("GET", "/ws/v5/public", "HTTP/2");
    request.SetHeader("Host", address() + ":8443"); // @todo
    request.SetHeader("Upgrade", "websocket");
    request.SetHeader("Connection", "upgrade");
    request.SetHeader("Sec-WebSocket-Key", CppCommon::Encoding::Base64Encode(ws_nonce()));
    request.SetHeader("Accept", "/");
    // request.SetHeader("Sec-WebSocket-Version", "13");
    request.SetHeader("User-Agent", "beast.v1");
  }

This did not work, and my comments are:

(1) Do I need to set SNI somewhere on the stream native handle (using SSL_set_tlsext_host_name) (2) Why (once SNI is set) the upgrade request is not working? If you have any experience with this.


NOTE: something like this seemed to get round initial SSL v3 handshake errors

bool SSLClient::Connect(const std::shared_ptr<TCPResolver>& resolver) {
 // ...

    std::cout << "Setting SNI Hostname: " << _address << std::endl;
    if(SSL_set_tlsext_host_name(_stream.native_handle(), _address.c_str())) {
        std::cout << "Success!" << std::endl;
    }
    else {
        std::cout << "Failed!" << std::endl;
    }
// ...
AlexSilver9 commented 1 year ago

Hi,

same here. SNI doesn't seem to work. I tried with your URL @davidtwomey , but the same happens with wss://api.huobi.com:443/ws, while same implementation works fine with wss://stream.binance.com:443.

I managed to set the server address manually like this, but without effect. No matter if I set it before the connection is started, or later. It prints SNI Failed!:

    if (SSL_CTX_ctrl(_ssl_context->native_handle(),
        SSL_CTRL_SET_TLSEXT_HOSTNAME,TLSEXT_NAMETYPE_host_name,
        (void *) _configuration->getWebSocketServerHost().c_str()))
    {
        std::cout << "SNI Success!" << std::endl;
    }
    else {
        std::cout << "SNI Failed!" << std::endl;
    }

The error on connecting is always the same: WebSocketClient caught an error with code 167773200 and category 'asio.ssl': sslv3 alert handshake failure (SSL routines)

I don't even get to void onWSConnecting(CppServer::HTTP::HTTPRequest &request) callback. I just get disconnected right on the connection attempt.

Any idea?

Thank you, Alex

AlexSilver9 commented 1 year ago

After more research I found out that the following piece of Code now says that SNI is successful, but it is still not working:

if (SSL_set_tlsext_host_name(stream().native_handle(), host.c_str())) {
        std::cout << "SNI Success!" << std::endl;
    } else {
        std::cout << "SNI Failed!" << std::endl;
    }

Btw... same code is working on Boost::Beast and connection can be established and data received from the same source:

if ( ! SSL_set_tlsext_host_name(ws.next_layer().native_handle(), host.c_str()))
            throw beast::system_error(
                    beast::error_code(
                            static_cast<int>(::ERR_get_error()),
                            net::error::get_ssl_category()),
                    "Failed to set SNI Hostname");

Both native_handle() function documentations say that a pointer of type SSL* is returned:

Example The native_handle() function returns a pointer of type SSL* that is suitable for passing
to functions such as SSL_get_verify_result and
SSL_get_peer_certificate: asio::ssl::stream  sock(my_context, ctx); // ... establish connection
and perform handshake ... 

if (X509* cert = SSL_get_peer_certificate(sock.native_handle())) {
    if (SSL_get_verify_result(sock.native_handle()) == X509_V_OK) {
         // ... 
    }
}