123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724 |
- {
- $Id: header,v 1.1 2000/07/13 06:33:45 michael Exp $
- This file is part of the Free Component Library (FCL)
- Copyright (c) 2021 - by the Free Pascal development team
- Websocket client implementation.
- See the file COPYING.FPC, included in this distribution,
- for details about the copyright.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- **********************************************************************}
- {$mode ObjFPC}{$H+}
- unit fpwebsocketclient;
- interface
- uses
- sysutils, classes, fpwebsocket, ssockets, sslsockets, fpopenssl;
- Type
- EWebSocketClient = Class(EWebSocket);
- TWSClientHandShakeEvent = Procedure(Sender : TObject; aHeaders : TStrings) of Object;
- TWSClientHandShakeResponseEvent = Procedure(Sender : TObject; aResponse : TWSHandShakeResponse; Var aAllow : Boolean) of Object;
- TWSErrorEvent = Procedure (Sender : TObject; E : Exception) of object;
- { TWSMessagePump }
- TWSMessagePump = Class (TComponent)
- private
- FInterval:Integer;
- FList: TThreadList;
- FReads: TSocketStreamArray;
- FExceptions : TSocketStreamArray;
- FOnError: TWSErrorEvent;
- procedure SetInterval(AValue: Integer);
- Protected
- function WaitForData: Boolean;
- Function CheckConnections : Boolean; virtual;
- Procedure ReadConnections;
- Property List : TThreadList Read FList;
- Public
- Constructor Create(aOwner : TComponent); override;
- Destructor Destroy; override;
- Procedure AddClient(aConnection : TWSClientConnection);
- Procedure RemoveClient(aConnection : TWSClientConnection);
- Procedure Execute; virtual; abstract;
- Procedure Terminate; virtual; abstract;
- Property Interval : Integer Read FInterval Write SetInterval;
- Property OnError : TWSErrorEvent Read FOnError Write FOnError;
- End;
- // Default message driver, works with thread that checks sockets for available data
- TWSThreadMessagePump = Class(TWSMessagePump)
- Private
- FThread : TThread;
- Procedure ThreadTerminated(Sender : TObject);
- Protected
- Type
- TMessageDriverThread = Class(TThread)
- Public
- FPump : TWSThreadMessagePump;
- Constructor Create(aPump : TWSThreadMessagePump; aTerminate : TNotifyEvent);
- Procedure Execute;override;
- End;
- Public
- Procedure Execute; override;
- Procedure Terminate; override;
- End;
- TCustomWebsocketClient = class;
- { TWebSocketClientConnection }
- TWebSocketClientConnection = class(TWSClientConnection)
- protected
- Procedure DoDisconnect; override;
- function GetClient: TCustomWebsocketClient; virtual;
- Public
- Property WebsocketClient : TCustomWebsocketClient Read GetClient;
- end;
- { TCustomWebsocketClient }
- TCustomWebsocketClient = Class(TComponent)
- private
- FOutGoingFrameMask: Integer;
- FPort: Integer;
- FActive: Boolean;
- FLoadActive : Boolean;
- FHostName: String;
- FUseSSL: Boolean;
- FResource: string;
- FConnectTimeout: Integer;
- FOptions: TWSOptions;
- FSocket : TInetSocket;
- FTransport : TWSClientTransport;
- FCheckTimeOut: Integer;
- FAutoCheckMessages: Boolean;
- FHandShake : TWSHandShakeRequest;
- FMessagePump: TWSMessagePump; // Do not free
- FHandshakeResponse: TWSHandShakeResponse;
- FOnSendHandShake: TWSClientHandshakeEvent;
- FOnHandshakeResponse: TWSClientHandshakeResponseEvent;
- FConnection: TWebSocketClientConnection;
- FOnMessageReceived: TWSMessageEvent;
- FOnControl: TWSControlEvent;
- FOnDisconnect: TNotifyEvent;
- FOnConnect: TNotifyEvent;
- procedure FreeConnectionObjects;
- procedure SetActive(const Value: Boolean);
- procedure SetHostName(const Value: String);
- procedure SetMessagePump(AValue: TWSMessagePump);
- procedure SetPort(const Value: Integer);
- procedure SetUseSSL(const Value: Boolean);
- procedure SetConnectTimeout(const Value: Integer);
- procedure SetResource(const Value: string);
- procedure SetCheckTimeOut(const Value: Integer);
- procedure SetOptions(const Value: TWSOptions);
- procedure SetAutoCheckMessages(const Value: Boolean);
- procedure SendHeaders(aHeaders: TStrings);
- procedure ConnectionDisconnected(Sender: TObject);
- Protected
- Procedure CheckInactive;
- Procedure Loaded; override;
- function CreateClientConnection(aTransport : TWSClientTransport): TWebSocketClientConnection; virtual;
- procedure MessageReceived(Sender: TObject; const aMessage : TWSMessage);
- Procedure ControlReceived(Sender: TObject; aType : TFrameType; const aData: TBytes);virtual;
- function CheckHandShakeResponse(aHeaders: TStrings): Boolean; virtual;
- function CreateHandShakeRequest: TWSHandShakeRequest; virtual;
- function CreateHandshakeResponse(aHeaders: TStrings): TWSHandShakeResponse; virtual;
- procedure SendHandShakeRequest; virtual;
- function ReadHandShakeResponse: Boolean; virtual;
- Function DoHandShake: Boolean;
- Property Transport: TWSClientTransport Read FTransport;
- Public
- Property Connection: TWebSocketClientConnection Read FConnection;
- Public
- Destructor Destroy; override;
- // Check for incoming messages
- Function CheckIncoming : TIncomingResult;
- // Connect and perform handshake
- Procedure Connect;
- // Disconnect from server.
- Procedure Disconnect(SendClose : boolean = true);
- // Send a ping message
- Procedure Ping(aMessage: UTF8String);
- // Send a pong message
- Procedure Pong(aMessage: UTF8String);
- // Send raw data (ftBinary)
- Procedure SendData(aBytes : TBytes);
- // Send a string message
- Procedure SendMessage(Const aMessage : String);
- Public
- // Connect/Disconnect
- Property Active : Boolean Read FActive Write SetActive;
- // Check for message timeout
- Property CheckTimeOut : Integer Read FCheckTimeOut Write SetCheckTimeOut;
- // Timeout for connect
- Property ConnectTimeout : Integer Read FConnectTimeout Write SetConnectTimeout;
- // Host to connect to
- Property HostName : String Read FHostName Write SetHostName;
- // Message driver
- Property MessagePump : TWSMessagePump Read FMessagePump Write SetMessagePump;
- // Options
- Property Options : TWSOptions Read FOptions Write SetOptions;
- // Mask to use for outgoing frames
- Property OutGoingFrameMask : Integer Read FOutGoingFrameMask Write FOutGoingFrameMask;
- // Port to connect to
- Property Port : Integer Read FPort Write SetPort;
- // Path/Document in HTTP URL for GET request
- Property Resource : string Read FResource Write SetResource;
- // User SSL when connecting
- Property UseSSL : Boolean Read FUseSSL Write SetUseSSL;
- // Events
- // Called when handshake is about to be sent
- Property OnSendHandShake : TWSClientHandshakeEvent Read FOnSendHandShake Write FOnSendHandshake;
- // Called when handshake response is received
- Property OnHandshakeResponse : TWSClientHandshakeResponseEvent Read FOnHandshakeResponse Write FOnHandshakeResponse;
- // Called when a text message is received.
- property OnMessageReceived: TWSMessageEvent read FOnMessageReceived write FOnMessageReceived;
- // Called when a connection is disconnected.
- property OnDisconnect: TNotifyEvent read FOnDisconnect write FOnDisconnect;
- // Called when a connection is established
- property OnConnect: TNotifyEvent read FOnConnect write FOnConnect;
- // Called when a control message is received.
- property OnControl: TWSControlEvent read FOnControl write FOnControl;
- End;
- TWebsocketClient = Class(TCustomWebsocketClient)
- Published
- Property HostName;
- Property Port;
- Property CheckTimeOut;
- Property ConnectTimeout;
- Property MessagePump;
- Property Options;
- Property Resource;
- Property UseSSL;
- Property OnSendHandShake;
- Property OnHandshakeResponse;
- property OnMessageReceived;
- property OnDisconnect;
- property OnConnect;
- property OnControl;
- Property OutGoingFrameMask;
- End;
- implementation
- uses sha1;
- { TWebSocketClientConnection }
- procedure TWebSocketClientConnection.DoDisconnect;
- begin
- If Assigned(WebSocketClient) then
- WebSocketClient.ConnectionDisconnected(Self);
- end;
- function TWebSocketClientConnection.GetClient: TCustomWebsocketClient;
- begin
- Result:=Owner as TCustomWebsocketClient;
- end;
- { TCustomWebsocketClient }
- procedure TCustomWebsocketClient.CheckInactive;
- begin
- If Active then
- Raise EWebSocketClient.Create(SErrConnectionActive);
- end;
- Function TCustomWebsocketClient.CheckIncoming : TIncomingResult;
- begin
- If Not Active then
- Raise EWebSocketClient.Create(SErrConnectionInActive);
- if Not Connection.HandshakeCompleted then
- Raise EWebSocketClient.Create(SErrHandshakeInComplete);
- Result:=Connection.CheckIncoming(CheckTimeout);
- if (Result=irClose) then
- begin
- Disconnect(False);
- end;
- end;
- procedure TCustomWebsocketClient.ControlReceived(Sender: TObject; aType : TFrameType; const aData: TBytes);
- begin
- If Assigned(FOnControl) then
- FOnControl(Sender, aType, aData);
- end;
- function TCustomWebsocketClient.CreateClientConnection(aTransport: TWSClientTRansport): TWebsocketClientConnection;
- begin
- Result:=TWebSocketClientConnection.Create(Self,aTransport,FOptions);
- end;
- procedure TCustomWebsocketClient.ConnectionDisconnected(Sender : TObject);
- begin
- FActive:=False;
- If Assigned(MessagePump) then
- MessagePump.RemoveClient(FConnection);
- If Assigned(OnDisconnect) then
- OnDisconnect(Self);
- // We cannot free the connection here, because it still needs to call it's own OnDisconnect.
- end;
- procedure TCustomWebsocketClient.Connect;
- var
- SSLHandler: TSSLSocketHandler;
- begin
- If Active then
- Exit;
- // Safety: Free any dangling objects before recreating
- FreeConnectionObjects;
- SSLHandler := nil;
- if UseSSL then
- begin
- SSLHandler := TSSLSocketHandler.GetDefaultHandler;
- SSLHandler.VerifyPeerCert := False;
- end;
- FSocket:=TInetSocket.Create(HostName,Port,ConnectTimeout, SSLHandler);
- FTransport:=TWSClientTransport.Create(FSocket);
- FConnection:=CreateClientConnection(FTransport);
- FConnection.OnMessageReceived:=@MessageReceived;
- FConnection.OnControl:=@ControlReceived;
- FCOnnection.OutgoingFrameMask:=Self.OutGoingFrameMask;
- if UseSSL then
- FSocket.Connect;
- FActive:=True;
- if not DoHandShake then
- Disconnect(False)
- else
- begin
- If Assigned(MessagePump) then
- MessagePump.AddClient(FConnection);
- if Assigned(OnConnect) then
- OnConnect(Self);
- end;
- end;
- destructor TCustomWebsocketClient.Destroy;
- begin
- DisConnect(False);
- FreeAndNil(FHandShake);
- FreeAndNil(FHandshakeResponse);
- FreeConnectionObjects;
- Inherited;
- end;
- Function TCustomWebsocketClient.CreateHandShakeRequest : TWSHandShakeRequest;
- begin
- Result:=TWSHandShakeRequest.Create('',Nil);
- end;
- procedure TCustomWebsocketClient.SendData(aBytes: TBytes);
- begin
- Connection.Send(aBytes);
- end;
- procedure TCustomWebsocketClient.SendHeaders(aHeaders : TStrings);
- Var
- S : String;
- B : TBytes;
- begin
- for S in AHeaders do
- begin
- B:=TEncoding.UTF8.GetAnsiBytes(S+#13#10);
- Connection.Transport.WriteBytes(B,Length(B));
- end;
- B:=TEncoding.UTF8.GetAnsiBytes(#13#10);
- Connection.Transport.WriteBytes(B,Length(B));
- end;
- procedure TCustomWebsocketClient.SendHandShakeRequest;
- Var
- aRequest : TWSHandShakeRequest;
- aHeaders : TStrings;
- begin
- aHeaders:=Nil;
- FreeAndNil(FHandShake);
- aRequest:=CreateHandShakeRequest;
- try
- aRequest.Host:=HostName;
- aRequest.Port:=Port;
- aRequest.Resource:=Resource;
- aHeaders:=TStringList.Create;
- aHeaders.NameValueSeparator:=':';
- aRequest.ToStrings(aHeaders);
- if Assigned(FOnSendHandshake) then
- FOnSendHandshake(self,aHeaders);
- // Do not use FClient.WriteHeader, it messes up the strings !
- SendHeaders(aHeaders);
- FHandShake:=aRequest;
- finally
- aHeaders.Free;
- if FhandShake<>aRequest then
- aRequest.Free;
- end;
- end;
- procedure TCustomWebsocketClient.SendMessage(const aMessage: String);
- begin
- Connection.Send(aMessage);
- end;
- Function TCustomWebsocketClient.CreateHandshakeResponse(aHeaders : TStrings) : TWSHandShakeResponse;
- begin
- Result:=TWSHandShakeResponse.Create('',aHeaders);
- end;
- Function TCustomWebsocketClient.CheckHandShakeResponse(aHeaders : TStrings) : Boolean;
- Var
- K : String;
- {%H-}hash : TSHA1Digest;
- B : TBytes;
- begin
- B:=[];
- FreeAndNil(FHandshakeResponse);
- FHandshakeResponse:=CreateHandshakeResponse(aHeaders);
- k := Trim(FHandshake.Key) + SSecWebSocketGUID;
- hash:=sha1.SHA1String(k);
- SetLength(B,SizeOf(hash));
- Move(hash[0],B[0],SizeOf(hash));
- k:=EncodeBytesBase64(B);
- Result:=SameText(K,FHandshakeResponse.Accept)
- and SameText(FHandshakeResponse.Upgrade,'websocket');
- end;
- Function TCustomWebsocketClient.ReadHandShakeResponse : Boolean;
- Var
- S : String;
- aHeaders : TStrings;
- begin
- Result:=False;
- aHeaders:=TStringList.Create;
- Try
- aHeaders.NameValueSeparator:=':';
- Repeat
- S:=Connection.Transport.ReadLn;
- aHeaders.Add(S);
- Until (S='');
- Result:=CheckHandShakeResponse(aHeaders);
- if Result and Assigned(FOnHandshakeResponse) then
- FOnHandshakeResponse(Self,FHandShakeResponse,Result);
- if Result then
- FConnection.HandshakeResponse:=FHandShakeResponse
- Finally
- aHeaders.Free;
- End;
- end;
- Function TCustomWebsocketClient.DoHandShake : Boolean;
- begin
- SendHandShakeRequest;
- Result:=ReadHandShakeResponse;
- end;
- procedure TCustomWebsocketClient.Loaded;
- begin
- inherited;
- if FLoadActive then
- Connect;
- end;
- procedure TCustomWebsocketClient.MessageReceived(Sender: TObject; const aMessage : TWSMessage) ;
- begin
- if Assigned(OnMessageReceived) and (TWSClientConnection(Sender).HandshakeCompleted) then
- OnMessageReceived(Self, AMessage);
- end;
- procedure TCustomWebsocketClient.Ping(aMessage: UTF8String);
- begin
- FConnection.Send(ftPing,TEncoding.UTF8.GetAnsiBytes(aMessage));
- end;
- procedure TCustomWebsocketClient.Pong(aMessage: UTF8String);
- begin
- FConnection.Send(ftPong,TEncoding.UTF8.GetAnsiBytes(aMessage));
- end;
- procedure TCustomWebsocketClient.FreeConnectionObjects;
- begin
- FreeAndNil(FConnection);
- FreeAndNil(FTransport);
- FreeAndNil(FSocket);
- end;
- procedure TCustomWebsocketClient.Disconnect(SendClose : boolean = true);
- begin
- if Not Active then
- Exit;
- if SendClose then
- Connection.Close('');
- if Assigned(MessagePump) then
- MessagePump.RemoveClient(Connection);
- If Assigned(OnDisconnect) then
- OnDisconnect(Self);
- FreeConnectionObjects;
- FActive:=False;
- end;
- procedure TCustomWebsocketClient.SetActive(const Value: Boolean);
- begin
- FLoadActive := Value;
- if (csDesigning in ComponentState) then
- exit;
- if Value then
- Connect
- else
- Disconnect;
- end;
- procedure TCustomWebsocketClient.SetAutoCheckMessages(const Value: Boolean);
- begin
- CheckInactive;
- FAutoCheckMessages := Value;
- end;
- procedure TCustomWebsocketClient.SetCheckTimeOut(const Value: Integer);
- begin
- CheckInactive;
- FCheckTimeOut := Value;
- end;
- procedure TCustomWebsocketClient.SetConnectTimeout(const Value: Integer);
- begin
- CheckInactive;
- FConnectTimeout := Value;
- end;
- procedure TCustomWebsocketClient.SetHostName(const Value: String);
- begin
- CheckInactive;
- FHostName := Value;
- end;
- procedure TCustomWebsocketClient.SetMessagePump(AValue: TWSMessagePump);
- begin
- if FMessagePump=AValue then Exit;
- If Assigned(FMessagePump) then
- FMessagePump.RemoveFreeNotification(Self);
- FMessagePump:=AValue;
- If Assigned(FMessagePump) then
- FMessagePump.FreeNotification(Self);
- end;
- procedure TCustomWebsocketClient.SetOptions(const Value: TWSOptions);
- begin
- CheckInactive;
- FOptions := Value;
- end;
- procedure TCustomWebsocketClient.SetPort(const Value: Integer);
- begin
- CheckInactive;
- FPort := Value;
- end;
- procedure TCustomWebsocketClient.SetResource(const Value: string);
- begin
- CheckInactive;
- FResource := Value;
- end;
- procedure TCustomWebsocketClient.SetUseSSL(const Value: Boolean);
- begin
- CheckInactive;
- FUseSSL := Value;
- end;
- { TTMSClientWebSocketConnection }
- { TWSMessagePump }
- procedure TWSMessagePump.AddClient(aConnection: TWSClientConnection);
- begin
- List.Add(aConnection);
- end;
- procedure TWSMessagePump.RemoveClient(aConnection: TWSClientConnection);
- begin
- FList.Remove(aConnection);
- end;
- procedure TWSMessagePump.SetInterval(AValue: Integer);
- begin
- if FInterval=AValue then Exit;
- FInterval:=AValue;
- end;
- Function TWSMessagePump.WaitForData : Boolean;
- Var
- dummy1,dummy2 : TSocketStreamArray;
- begin
- Dummy1:=Nil;
- Dummy2:=Nil;
- Result:=False;
- // FReadSet was populated by checkconnections
- SetLength(FExceptions,0);
- if Length(FReads)=0 then
- begin
- TThread.Sleep(FInterval);
- end
- else
- begin
- try
- // We take the first ont in the list.
- Result := FReadS[0].Select(FReads,dummy1,dummy2,FInterval);
- except
- Result := False;
- end;
- end;
- end;
- function TWSMessagePump.CheckConnections: Boolean;
- Var
- aList : TList;
- aClient: TWSClientConnection;
- aTrans : TWSClientTransport;
- I,aLen : Integer;
- begin
- Result:=False;
- aList := List.LockList;
- try
- aLen:=0;
- SetLength(FReads,aList.Count);
- for I := 0 to aList.Count - 1 do
- begin
- aClient := TWSClientConnection(aList.Items[I]);
- if assigned(aClient) then
- aTrans:=aClient.ClientTransport
- else
- aTrans:=Nil;
- if (aTrans<>nil) then
- begin
- // There is already data
- FReads[aLen]:=aTrans.Socket;
- Inc(aLen);
- end;
- end;
- finally
- List.UnlockList;
- end;
- if Not Result then
- Result:=WaitForData;
- end;
- constructor TWSMessagePump.Create(aOwner : TComponent);
- begin
- FList:=TThreadList.Create;
- FReads:=[];
- FExceptions:=[];
- Finterval:=50;
- end;
- destructor TWSMessagePump.Destroy;
- begin
- FreeAndNil(FList);
- inherited;
- end;
- procedure TWSMessagePump.ReadConnections;
- Var
- aList : TList;
- aClient: TWSClientConnection;
- I : Integer;
- begin
- try
- aList := List.LockList;
- try
- FReads:=[];
- for I := 0 to aList.Count - 1 do
- begin
- aClient:= TWSClientConnection(aList.Items[I]);
- if assigned(aClient.Transport) then
- aClient.CheckIncoming(1);
- end;
- finally
- List.UnlockList;
- end;
- except
- on E: Exception do
- if Assigned(OnError) then
- OnError(Self,E);
- end;
- end;
- { TWSThreadMessagePump }
- procedure TWSThreadMessagePump.Execute;
- begin
- FThread:=TMessageDriverThread.Create(Self,@ThreadTerminated);
- end;
- procedure TWSThreadMessagePump.ThreadTerminated(Sender: TObject);
- begin
- FThread:=Nil;
- end;
- procedure TWSThreadMessagePump.Terminate;
- begin
- FThread.Terminate;
- if Assigned(FThread) then
- FThread.WaitFor;
- end;
- { TWSThreadMessagePump.TMessageDriverThread }
- constructor TWSThreadMessagePump.TMessageDriverThread.Create(aPump: TWSThreadMessagePump; aTerminate : TNotifyEvent);
- begin
- FPump:=aPump;
- OnTerminate:=aTerminate;
- Inherited Create(False);
- end;
- procedure TWSThreadMessagePump.TMessageDriverThread.Execute;
- begin
- While Not Terminated do
- if FPump.CheckConnections then
- FPump.ReadConnections
- else
- TThread.Sleep(FPump.Interval);
- end;
- end.
|