2
votes

I'm using C++Builder 10.1 Berlin to write a simple WebSocket server application, which listens on a port for some commands sent from a web browser, like Google Chrome.

On my Form, I have a TMemo, TButton and TIdHTTPServer, and I have this code:

void __fastcall TForm1::Button1Click(TObject *Sender)
{
 IdHTTPServer1->Bindings->DefaultPort = 55555;
 IdHTTPServer1->Active = true;
}

void __fastcall TForm1::IdHTTPServer1Connect(TIdContext *AContext)
{

  Memo1->Lines->Add(AContext->Binding->PeerIP);
  Memo1->Lines->Add( AContext->Connection->IOHandler->ReadLn(enUTF8));
  Memo1->Lines->Add( AContext->Data->ToString());

}


void __fastcall TForm5::IdHTTPServer1CommandOther(TIdContext *AContext, TIdHTTPRequestInfo *ARequestInfo,
          TIdHTTPResponseInfo *AResponseInfo)
{

UnicodeString svk,sValue;
TIdHashSHA1 *FHash;
TMemoryStream *strmRequest;

FHash = new TIdHashSHA1;
strmRequest  =  new TMemoryStream;

strmRequest->Position = 0;
svk = ARequestInfo->RawHeaders->Values["Sec-WebSocket-Key"];

Memo1->Lines->Add("Get:"+svk);
      AResponseInfo->ResponseNo         = 101;
      AResponseInfo->ResponseText       = "Switching Protocols";
      AResponseInfo->CloseConnection    = False;
      //Connection: Upgrade
      AResponseInfo->Connection         = "Upgrade";
      //Upgrade: websocket
      AResponseInfo->CustomHeaders->Values["Upgrade"] = "websocket";

      sValue = svk + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
      sValue = TIdEncoderMIME::EncodeBytes( FHash->HashString(sValue) );
      AResponseInfo->CustomHeaders->Values["Sec-WebSocket-Accept"] = sValue;
      AResponseInfo->ContentText = "Welcome here!";

      AResponseInfo->WriteHeader();

 UnicodeString URLstr = "http://"+ARequestInfo->Host+ARequestInfo->Document;
      if (ARequestInfo->UnparsedParams != "") URLstr = URLstr+"?"+ARequestInfo->UnparsedParams;
      Memo1->Lines->Add(URLstr);
      Memo1->Lines->Add(ARequestInfo->Command );
      Memo1->Lines->Add("--------");
      Memo1->Lines->Add(ARequestInfo->RawHeaders->Text );
      Memo1->Lines->Add(AContext->Data->ToString() );
}

From Chrome, I execute this Javascript code:

var connection = new WebSocket('ws://localhost:55555');

connection.onopen = function () {
  connection.send('Ping');
};

But I get this error from Chrome:

VM77:1 WebSocket connection to 'ws://localhost:55555/' failed: One or more reserved bits are on: reserved1 = 1, reserved2 = 0, reserved3 = 0

I expect the WebSocket connection to be successful, and then I can send data between the web browser and my server application.

Maybe somebody already knows what is wrong and can show a full example of how to achieve this?


Here is what my application's Memo1 shows:

192.168.0.25
GET / HTTP/1.1
Get:TnBN9qjOJiwka2eJe7mR0A==
http://
HOST:
--------
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://bcbjournal.org
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8
Sec-WebSocket-Key: TnBN9qjOJiwka2eJe7mR0A==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

Here is what Chrome shows:

Response Request:

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Content-Type: text/html; charset=ISO-8859-1
Content-Length: 13
Date: Thu, 08 Jun 2017 15:04:00 GMT
Upgrade: websocket
Sec-WebSocket-Accept: 2coLmtu++HmyY8PRTNuaR320KPE=

Request Headers

GET ws://192.168.0.25:55555/ HTTP/1.1
Host: 192.168.0.25:55555
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://bcbjournal.org
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8
Sec-WebSocket-Key: TnBN9qjOJiwka2eJe7mR0A==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
1
Sidenote, do not access VCL components from those events. They are running in context of worker threads.Victoria

1 Answers

3
votes

You are misusing TIdHTTPServer

You are making two big mistakes:

  1. Your OnConnect event handler is reading the client's initial HTTP request line (the GET line). It should not be reading anything from the client at all, as doing so interfers with TIdHTTPServer's handling of the HTTP protocol.

    After the event handler reads the request line and exits, TIdHTTPServer then reads the next line (the Host header) and interprets that as the request line instead, which is why:

    • the ARequestInfo->Command property is "HOST:" instead of "GET".

    • the ARequestInfo->Host, ARequestInfo->Document, ARequestInfo->Version, ARequestInfo->VersionMajor, ARequestInfo->VersionMinor properties are all wrong.

    • you end up having to use the OnCommandOther event when you should be using the OnCommandGet event instead.

  2. You are accessing the TMemo in your TIdHTTPServer events without synchronizing with the main UI thread. TIdHTTPServer is a multi-threaded component. Its events are fired in the context of worker threads. VCL/FMX UI controls are not thread-safe, so you must synchronize properly with the main UI thread.

You are not implementing the WebSocket protocol correctly

Your server is not validating everything in the handshake that the WebSocket protocol requires a server to validate (which is fine for testing, but make sure you do it for production).

But more importantly, TIdHTTPServer is not well-suited for implementing WebSockets (that is a TODO item). The only thing about the WebSocket protocol that involves HTTP is the handshake. After the handshake is finished, everything else is WebSocket framing, not HTTP. To handle that in TIdHTTPServer requires you to implement the entire WebSocket session inside of the OnCommandGet event, reading and sending all WebSocket frames, preventing the event handler from exiting, until the connection is closed. For that kind of logic, I would suggest using TIdTCPServer directly instead, and just handle the HTTP handshake manually at the beginning of its OnExecute event, and then loop the rest of the event handling the WebSocket frames.

Your OnCommandOther event handler is not currently performing any WebSocket I/O after the handshake is finished. It is returning control to TIdHTTPServer, which will then attempt to read a new HTTP request. As soon as the client sends a WebSocket frame to the server, TIdHTTPServer will fail to process it since it is not HTTP, and will likely send an HTTP response back to the client, which will get misinterpreted, causing the client to fail the WebSocket session and close the socket connection.


With that said, try something more like this instead:

#include ...
#include <IdSync.hpp>

class TLogNotify : public TIdNotify
{
protected:
    String FMsg;

    void __fastcall DoNotify()
    {
        Form1->Memo1->Lines->Add(FMsg);
    }

public:
    __fastcall TLogNotify(const String &S) : TIdNotify(), FMsg(S) {}
};

__fastcall TForm1::TForm1(TComponent *Owner)
    : TForm(Owner)
{
    IdHTTPServer1->DefaultPort = 55555;
}

void __fastcall TForm1::Log(const String &S)
{
    (new TLogNotify(S))->Notify();
}

void __fastcall TForm1::Button1Click(TObject *Sender)
{
    IdHTTPServer1->Active = true;
}

void __fastcall TForm1::IdHTTPServer1Connect(TIdContext *AContext)
{
    Log(_D("Connected: ") + AContext->Binding->PeerIP);
}

void __fastcall TForm1::IdHTTPServer1Disconnect(TIdContext *AContext)
{
    Log(_D("Disconnected: ") + AContext->Binding->PeerIP);
}

void __fastcall TForm5::IdHTTPServer1CommandGet(TIdContext *AContext, TIdHTTPRequestInfo *ARequestInfo, TIdHTTPResponseInfo *AResponseInfo)
{
    Log(ARequestInfo->RawHTTPCommand);

    if (ARequestInfo->Document != _D("/"))
    {
        AResponseInfo->ResponseNo = 404;
        return;
    }

    if ( !(ARequestInfo->IsVersionAtLeast(1, 1) &&
          TextIsSame(ARequestInfo->RawHeaders->Values[_D("Upgrade")], _D("websocket")) &&
          TextIsSame(ARequestInfo->Connection, _D("Upgrade")) ) )
    {
        AResponseInfo->ResponseNo         = 426;
        AResponseInfo->ResponseText       = _D("upgrade required");
        return;
    }

    String svk = ARequestInfo->RawHeaders->Values[_D("Sec-WebSocket-Key")];

    if ( (ARequestInfo->RawHeaders->Values[_D("Sec-WebSocket-Version")] != _D("13")) ||
          svk.IsEmpty() )
    {
        AResponseInfo->ResponseNo = 400;
        return;
    }

    // validate Origin, Sec-WebSocket-Protocol, and Sec-WebSocket-Extensions as needed...

    Log(_D("Get:") + svk);

    AResponseInfo->ResponseNo         = 101;
    AResponseInfo->ResponseText       = _D("Switching Protocols");
    AResponseInfo->CloseConnection    = false;
    AResponseInfo->Connection         = _D("Upgrade");
    AResponseInfo->CustomHeaders->Values[_D("Upgrade")] = _D("websocket");

    TIdHashSHA1 *FHash = new TIdHashSHA1;
    try {
        String sValue = svk + _D("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
        sValue = TIdEncoderMIME::EncodeBytes( FHash->HashString(sValue) );
        AResponseInfo->CustomHeaders->Values[_D("Sec-WebSocket-Accept")] = sValue;
    }
    __finally {
        delete FHash;
    }

    AResponseInfo->WriteHeader();

    String URLstr = _D("http://") + ARequestInfo->Host + ARequestInfo->Document;
    if (!ARequestInfo->UnparsedParams.IsEmpty()) URLstr = URLstr + _D("?") + ARequestInfo->UnparsedParams;
    Log(URLstr);
    Log(_D("--------"));
    Log(ARequestInfo->RawHeaders->Text);

    // now send/receive WebSocket frames here as needed,
    // using AContext->Connection->IOHandler directly...
}

That being said, there are plenty of 3rd party WebSocket libraries available. You should use one of them instead of implementing WebSockets manually. Some libraries even build on top of Indy.