WebSocket connect to TIdHTTPServer, handshake issue

喜夏-厌秋 提交于 2019-12-06 11:19:11

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!