Supported by
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.
CertFile, KeyFile, RootCertFile, Password: the credentials, filled from the server SSLOptions before Init is called. A provider that does not read certificates from files simply ignores them.
ALPNProtocols: the protocol names offered through the ALPN extension.
Init: called when the server goes active, after the credentials are set. Build the TLS library context here.
Shutdown: called when the server stops. It must leave the context reusable by a later Init, because restarting a server calls Init again on the same object.
CreateSession: returns a new session for one accepted connection.
TsgcTLSProviderSession is created once per connection and holds the TLS state of that connection. It is the crypto core and nothing else.
BeginHandShake: starts the server side handshake.
FeedHandShake: receives one flight of handshake ciphertext and returns tlshsContinue, tlshsDone or tlshsError.
Decrypt: turns received ciphertext into plaintext and returns the plaintext length.
Encrypt: encrypts a range of plaintext and returns the number of plaintext bytes consumed.
HandShakeDone: True once the connection is ready to carry application data.
Shutdown: closes the TLS session and releases whatever the TLS library allocated for it.
OnHandShakeData, OnReadData, OnWriteData: the three events the session uses to hand bytes back to the IOHandler. OnHandShakeData carries ciphertext produced while the handshake is running, OnWriteData carries ciphertext produced by Encrypt, and OnReadData carries plaintext the provider decoded on its own initiative, typically application data that arrived in the same flight that completed the handshake. The IOHandler assigns all three, a provider only raises them.
SocketHandle: the socket descriptor, set before BeginHandShake. Some TLS libraries want it for their own bookkeeping, OpenSSL for instance still asks for it. It is there for that reason only.
The contract
A provider that respects these rules runs on both engines without any further work.
A session never reads from or writes to the socket. The IOHandler owns the socket. Ciphertext that has to go out leaves through the three events, and plaintext comes back from Decrypt.
tlshsContinue returned by FeedHandShake means there is no answer yet and the next flight should be fed in. It is not an error and it does not mean the peer closed.
Decrypt works in place. VBuffer arrives holding ciphertext and is left holding the plaintext. Returning 0 means the ciphertext received so far did not complete a TLS record, which happens whenever a record is split across TCP segments. It does not mean the peer closed, so the engine waits for the rest of the record.
Encrypt returns the number of plaintext bytes it consumed. The ciphertext it produced leaves through OnWriteData.
BeginHandShake must be idempotent. The engine calls it on every task until the handshake reports done, so a second call has to do nothing.
The engine can read and write the same connection from different threads, but never at the same time. Calls into a session are serialized by the IOHandler.
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.