TLS Provider

Supported by

 

  TsgcWebSocketServer

  TsgcWebSocketHTTPServer

  TsgcHTTPServer

  TsgcWebSocketLoadBalancerServer

 

*Requires sgcWebSockets Enterprise Edition and the custom Indy version.

 

The IOCP and EPOLL server engines do not call a TLS library directly. They drive TLS through a small provider layer, so any TLS library that can encrypt and decrypt over byte buffers can be plugged into them. OpenSSL is the provider that ships with the components and it is still the default, nothing changes for a server that does not replace the SSL handler.

This extension point exists because the engines used to accept only a handler inheriting from the sgc OpenSSL handlers. A handler built on another TLS library, for example one based on SecureBlackbox, could never inherit from those classes, so such a server always fell back to the classic thread per connection engine. With a TLS provider the same server runs on IOCP or EPOLL with its own TLS library.

 

The two classes

 

Both classes are declared in sgcIndy_IOHandler_IO_TLS.pas.

 

TsgcTLSProviderContext is created once per listening socket and is shared by every connection accepted on it. It carries the credentials and creates the per connection sessions.

 

TsgcTLSProviderSession is created once per connection and holds the TLS state of that connection. It is the crypto core and nothing else.

 

The contract

 

A provider that respects these rules runs on both engines without any further work.

 

How to plug in a TLS library

 

1. Descend from TsgcIdServerIOHandlerTLSIOCP on Windows or from TsgcIdServerIOHandlerTLSEPOLL on Linux. Both are declared in sgcIndy_IOHandler_IO_Server_TLS.pas and both descend from TsgcIdServerIOHandlerIOTLS, which already contains all the engine wiring.

2. Override DoCreateTLSContext and return your own TsgcTLSProviderContext. That single method is the whole extension point.

3. Assign an instance of your handler in the server OnSSLGetHandler event. The server takes ownership of the handler and frees it.

 

Nothing else is required. The IOHandlerOptions values, such as threads, timeouts and TCPKeepAlive, are applied to your handler exactly as they are to the OpenSSL one.

 

Example

 

A minimal provider, the handler that returns it, and the server wiring. Replace the MyTLSLib calls with the calls of your own TLS library.


uses
  sgcIndy_IOHandler_IO_TLS, sgcIndy_IOHandler_IO_Server_TLS;

type
  TMyTLSSession = class(TsgcTLSProviderSession)
  private
    FStarted: Boolean;
  public
    procedure BeginHandShake; override;
    function FeedHandShake(const aBuffer: TIdBytes): TsgcTLSHandshakeState; override;
    function Decrypt(var VBuffer: TIdBytes): Integer; override;
    function Encrypt(const aBuffer: TIdBytes; const aOffset, aLength: Integer): Integer; override;
    function HandShakeDone: Boolean; override;
    procedure Shutdown; override;
  end;

  TMyTLSContext = class(TsgcTLSProviderContext)
  public
    procedure Init; override;
    procedure Shutdown; override;
    function CreateSession: TsgcTLSProviderSession; override;
  end;

  TMyTLSIOHandler = class(TsgcIdServerIOHandlerTLSIOCP)
  protected
    function DoCreateTLSContext: TsgcTLSProviderContext; override;
  end;

implementation

procedure TMyTLSSession.BeginHandShake;
begin
  // idempotent, the engine calls it until the handshake is done
  if FStarted then
    Exit;
  FStarted := True;
  MyTLSLib_AcceptStart(SocketHandle);
end;

function TMyTLSSession.FeedHandShake(const aBuffer: TIdBytes): TsgcTLSHandshakeState;
var
  vOut: TIdBytes;
begin
  // hand the received flight to the library, send back what it produced
  vOut := MyTLSLib_HandShake(aBuffer);
  if Length(vOut) > 0 then
    DoHandShakeData(vOut);

  if MyTLSLib_Failed then
    Result := tlshsError
  else if MyTLSLib_HandShakeDone then
    Result := tlshsDone
  else
    Result := tlshsContinue; // no answer yet, feed the next flight
end;

function TMyTLSSession.Decrypt(var VBuffer: TIdBytes): Integer;
begin
  // in place: VBuffer arrives with ciphertext and is left with the plaintext.
  // 0 means the record is not complete yet, it is not a closed peer.
  Result := MyTLSLib_Decrypt(VBuffer);
end;

function TMyTLSSession.Encrypt(const aBuffer: TIdBytes;
  const aOffset, aLength: Integer): Integer;
begin
  // the ciphertext leaves through OnWriteData, the result is the number of
  // plaintext bytes consumed
  DoWriteData(MyTLSLib_Encrypt(aBuffer, aOffset, aLength));
  Result := aLength;
end;

function TMyTLSSession.HandShakeDone: Boolean;
begin
  Result := MyTLSLib_HandShakeDone;
end;

procedure TMyTLSSession.Shutdown;
begin
  MyTLSLib_Close;
end;

procedure TMyTLSContext.Init;
begin
  // CertFile, KeyFile, RootCertFile and Password are already set here
  MyTLSLib_CreateContext(CertFile, KeyFile, Password);
end;

procedure TMyTLSContext.Shutdown;
begin
  // must leave this object ready for a later Init
  MyTLSLib_FreeContext;
end;

function TMyTLSContext.CreateSession: TsgcTLSProviderSession;
begin
  Result := TMyTLSSession.Create;
end;

function TMyTLSIOHandler.DoCreateTLSContext: TsgcTLSProviderContext;
begin
  Result := TMyTLSContext.Create;
end;

 

And the server wiring.


procedure TForm1.FormCreate(Sender: TObject);
begin
  sgcWebSocketServer1.SSL := True;
  sgcWebSocketServer1.SSLOptions.CertFile := 'certificate.pem';
  sgcWebSocketServer1.SSLOptions.KeyFile := 'key.pem';
  sgcWebSocketServer1.IOHandlerOptions.IOHandlerType := iohIOCP;
  sgcWebSocketServer1.OnSSLGetHandler := OnSSLGetHandler;
  sgcWebSocketServer1.Active := True;
end;

procedure TForm1.OnSSLGetHandler(Sender: TObject; aType: TwsSSLHandler;
  var aSSLHandler: TIdServerIOHandlerSSLBase);
begin
  aSSLHandler := TMyTLSIOHandler.Create(nil);
end;

 

On Linux, descend from TsgcIdServerIOHandlerTLSEPOLL instead and set IOHandlerType to iohEPOLL. Everything else stays the same, the provider classes are identical on both platforms.

 

When the handler is not a provider handler

 

A handler that descends from neither TsgcIdServerIOHandlerTLSIOCP nor TsgcIdServerIOHandlerTLSEPOLL still works. The server detects it when it starts, runs with the classic thread per connection engine instead of the event-driven one, and reports it through the OnError event. Read the ActiveIOHandlerType property to know which engine the server is really running.