-
-
Notifications
You must be signed in to change notification settings - Fork 135
/
mormot.net.ws.core.pas
4181 lines (3880 loc) · 145 KB
/
mormot.net.ws.core.pas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/// WebSockets Shared Process Classes and Definitions
// - this unit is a part of the Open Source Synopse mORMot framework 2,
// licensed under a MPL/GPL/LGPL three license - see LICENSE.md
unit mormot.net.ws.core;
{
*****************************************************************************
WebSockets Abstract Processing for Client and Server
- WebSockets Frames Definitions
- WebSockets Protocols Implementation
- WebSockets Asynchronous Frames Parsing
- WebSockets Client and Server Shared Process
- TWebSocketProtocolChat Simple Protocol
- Socket.IO / Engine.IO Raw Protocols
*****************************************************************************
}
interface
{$I ..\mormot.defines.inc}
uses
sysutils,
classes,
mormot.core.base,
mormot.core.os,
mormot.core.unicode,
mormot.core.text,
mormot.core.variants,
mormot.core.datetime,
mormot.core.data,
mormot.core.log,
mormot.core.threads,
mormot.core.rtti,
mormot.core.json,
mormot.core.buffers,
mormot.crypt.core,
mormot.crypt.ecc,
mormot.crypt.jwt,
mormot.crypt.secure, // IProtocol definition
mormot.net.sock,
mormot.net.http;
{ ******************** WebSockets Frames Definitions }
type
/// Exception raised when processing WebSockets
EWebSockets = class(ESynException);
/// defines the interpretation of the WebSockets frame data
// - match order expected by the WebSockets RFC
TWebSocketFrameOpCode = (
focContinuation,
focText,
focBinary,
focReserved3,
focReserved4,
focReserved5,
focReserved6,
focReserved7,
focConnectionClose,
focPing,
focPong,
focReservedB,
focReservedC,
focReservedD,
focReservedE,
focReservedF);
/// set of WebSockets frame interpretation
TWebSocketFrameOpCodes = set of TWebSocketFrameOpCode;
/// define one attribute of a WebSockets frame data
TWebSocketFramePayload = (
fopAlreadyCompressed);
/// define the attributes of a WebSockets frame data
TWebSocketFramePayloads = set of TWebSocketFramePayload;
/// stores a WebSockets frame
// - see @http://tools.ietf.org/html/rfc6455 for reference
TWebSocketFrame = record
/// the content of the frame data, typically focText or focBinary
opcode: TWebSocketFrameOpCode;
/// what is stored in the frame data, i.e. in payload field
content: TWebSocketFramePayloads;
/// equals GetTickCount64 shr 10, as used for TWebSocketFrameList timeout
tix: cardinal;
/// the frame data itself
// - is plain UTF-8 for focText kind of frame
// - is raw binary for focBinary or any other frames
payload: RawByteString;
end;
/// points to a WebSockets frame
PWebSocketFrame = ^TWebSocketFrame;
/// a dynamic list of WebSockets frames
TWebSocketFrameDynArray = array of TWebSocketFrame;
const
FRAME_OPCODE_FIN = 128;
// https://tools.ietf.org/html/rfc6455#section-10.3
// client-to-server masking is mandatory (but not from server to client)
FRAME_LEN_MASK = 128;
FRAME_LEN_2BYTES = 126;
FRAME_LEN_8BYTES = 127;
/// used to return the text corresponding to a specified WebSockets frame type
function ToText(opcode: TWebSocketFrameOpCode): PShortString; overload;
/// low-level intitialization of a TWebSocketFrame for proper REST content
procedure FrameInit(opcode: TWebSocketFrameOpCode;
const Content, ContentType: RawByteString; out frame: TWebSocketFrame);
/// low-level encoding of an output frame
// - don't make any fProtocol.BeforeSendFrame virtual encoding (like
// binary compression or encryption), but prepare the outgoing frame, ready
// to be sent over the socket
// - encoded buffer is a TSynTempBuffer to avoid most memory allocations
// - caller should always finally perform an eventual ToSend.Done
procedure FrameSendEncode(const Frame: TWebSocketFrame;
MaskSentFrames: cardinal; var ToSend: TSynTempBuffer);
/// compute the SHA-1 signature of the given WebSockets upgrade challenge
procedure ComputeChallenge(const Base64: RawByteString; out Digest: TSha1Digest);
{ ******************** WebSockets Protocols Implementation }
type
{$M+}
TWebSocketProcess = class;
{$M-}
/// used by TWebSocketProcessSettings for WebSockets process logging settings
TWebSocketProcessSettingsLogDetails = set of (
logHeartbeat,
logTextFrameContent,
logBinaryFrameContent,
logCallback);
/// points to parameters to be used for WebSockets process
// - using a pointer/reference type will allow in-place modification of
// any TWebSocketProcess.Settings, TWebSocketServer.Settings or
// THttpClientWebSockets.Settings property
PWebSocketProcessSettings = ^TWebSocketProcessSettings;
/// parameters to be used for WebSockets processing
// - those settings are used by all protocols running on a given
// TWebSocketServer or a THttpClientWebSockets
{$ifdef USERECORDWITHMETHODS}
TWebSocketProcessSettings = record
{$else}
TWebSocketProcessSettings = object
{$endif USERECORDWITHMETHODS}
public
/// time in milliseconds between each focPing commands sent to the other end
// - default is 0, i.e. no automatic ping sending on client side, and
// 20000, i.e. 20 seconds, on server side
HeartbeatDelay: cardinal;
/// maximum period time in milliseconds when ProcessLoop thread will stay
// idle before checking for the next pending requests
// - default is 500 ms, but you may put a lower value, if you expects e.g.
// REST commands or NotifyCallback(wscNonBlockWithoutAnswer) to be processed
// with a lower delay
LoopDelay: cardinal;
/// milliseconds delay between sending pending frames
// - allow to gather output frames in ProcessLoopStepSend
// - GetTickCount64 resolution is around 16ms on Windows and 4ms on Linux,
// so default 10 ms value seems fine for a cross-platform similar behavior
// (resulting in a <16ms period on Windows, and <12ms period on Linux)
SendDelay: cardinal;
/// will close the connection after a given number of invalid Heartbeat sent
// - when a Hearbeat is failed to be transmitted, the class will start
// counting how many ping/pong did fail: when this property value is
// reached, it will release and close the connection
// - client could then try to reestablish the weak connection, e.g. if a
// mobile connection reconnects after a white zone and may change its IP
// - default value is 5
DisconnectAfterInvalidHeartbeatCount: cardinal;
/// how many milliseconds the callback notification should wait acquiring
// the connection before failing
// - defaut is 5000, i.e. 5 seconds
CallbackAcquireTimeOutMS: cardinal;
/// how many milliseconds the callback notification should wait for the
// client to return its answer
// - defaut is 30000, i.e. 30 seconds
CallbackAnswerTimeOutMS: cardinal;
/// callback run when a WebSockets client is just connected
// - triggerred by TWebSocketProcess.ProcessStart
OnClientConnected: TNotifyEvent;
/// callback run when a WebSockets client is just disconnected
// - triggerred by TWebSocketProcess.ProcessStop
OnClientDisconnected: TNotifyEvent;
/// if the WebSockets Client should be upgraded after socket reconnection
// - default is TRUE
ClientAutoUpgrade: boolean;
/// notify the server to move any callbacks to the renewed connection
// - default is FALSE
ClientRestoreCallbacks: boolean;
/// by default, contains [] to minimize the logged information
// - set logHeartbeat if you want the ping/pong frames to be logged
// - set logTextFrameContent if you want the text frame content to be logged
// - set logBinaryFrameContent if you want the binary frame content to be logged
// - set logCallback for each TWebSocketAsyncServerRest callback notification
// - used only if WebSocketLog global variable is set to a TSynLog class
LogDetails: TWebSocketProcessSettingsLogDetails;
/// TWebSocketProtocol.SetEncryptKey PBKDF2-SHA-3 salt for TProtocolAes
// - default is some fixed value - you may customize it for a project
AesSalt: RawUtf8;
/// TWebSocketProtocol.SetEncryptKey PBKDF2-SHA-3 rounds for TProtocolAes
// - default is 1024 which takes around 0.5 ms to compute
// - 0 would use Sha256Weak() derivation function, as mORMot 1.18
AesRounds: integer;
/// TWebSocketProtocol.SetEncryptKey AES class for TProtocolAes
// - default is TAesFast[mCtr]
AesCipher: TAesAbstractClass;
/// TWebSocketProtocol.SetEncryptKey AES key size in bits, for TProtocolAes
// - default is 128 for efficient 'aes-128-ctr' at 2.5GB/s
// - for mORMot 1.18 compatibility, set for your custom settings:
// $ AesClass := TAesCfb;
// $ AesBits := 256;
// $ AesRounds := 0; // Sha256Weak() deprecated function
AesBits: integer;
/// TWebSocketProtocol.SetEncryptKey 'password#xxxxxx.private' ECDHE algo
// - default is efAesCtr128 as set to TEcdheProtocol.FromPasswordSecureFile
EcdheCipher: TEcdheEF;
/// TWebSocketProtocol.SetEncryptKey 'password#xxxxxx.private' ECDHE auth
// - default is the safest authMutual
EcdheAuth: TEcdheAuth;
/// TWebSocketProtocol.SetEncryptKey 'password#xxxxxx.private' password rounds
// - default is 60000, i.e. DEFAULT_ECCROUNDS
EcdheRounds: integer;
/// will set the default values
// - will also reset the HeartbeatDelay to 0, as expected on client side
procedure SetDefaults;
/// will set LogDetails to its highest level of verbosity
// - used only if WebSocketLog global variable is set
function SetFullLog: PWebSocketProcessSettings;
end;
/// callback event triggered by TWebSocketProtocol for any incoming message
// - called before TWebSocketProtocol.ProcessIncomingFrame for incoming
// focText/focBinary frames
// - should return true if the frame has been handled, or false if the
// regular processing should take place
TOnWebSocketProtocolIncomingFrame = function(Sender: TWebSocketProcess;
var Frame: TWebSocketFrame): boolean of object;
/// one instance implementing application-level WebSockets protocol
// - shared by TWebSocketServer and TWebSocketClient classes
// - once upgraded to WebSockets, a HTTP link could be used e.g. to transmit our
// proprietary 'synopsejson' or 'synopsebin' application content, as stated
// by this typical handshake:
// $ GET /myservice HTTP/1.1
// $ Host: server.example.com
// $ Upgrade: websocket
// $ Connection: Upgrade
// $ Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
// $ Sec-WebSocket-Protocol: synopsejson
// $ Sec-WebSocket-Version: 13
// $ Origin: http://example.com
// $
// $ HTTP/1.1 101 Switching Protocols
// $ Upgrade: websocket
// $ Connection: Upgrade
// $ Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
// $ Sec-WebSocket-Protocol: synopsejson
// - the TWebSocketProtocolJson inherited class will implement
// $ Sec-WebSocket-Protocol: synopsejson
// - the TWebSocketProtocolBinary inherited class will implement
// $ Sec-WebSocket-Protocol: synopsebin
TWebSocketProtocol = class(TSynPersistent)
protected
fConnectionID: THttpServerConnectionID;
fFramesInCount: integer;
fFramesOutCount: integer;
fFramesInBytes: QWord;
fFramesOutBytes: QWord;
fOnBeforeIncomingFrame: TOnWebSocketProtocolIncomingFrame;
fRemoteLocalhost: boolean;
fConnectionFlags: THttpServerRequestFlags;
fConnectionOpaque: PHttpServerConnectionOpaque;
fRemoteIP: RawUtf8;
fUpgradeUri: RawUtf8;
fUpgradeBearerToken: RawUtf8;
fName: RawUtf8;
fUri: RawUtf8;
fLastError: string;
fEncryption: IProtocol;
// focText/focBinary or focContinuation/focConnectionClose from ProcessStart/Stop
procedure ProcessIncomingFrame(Sender: TWebSocketProcess;
var Request: TWebSocketFrame; const Info: RawUtf8); virtual; abstract;
function SendFrames(Owner: TWebSocketProcess;
var Frames: TWebSocketFrameDynArray; var FramesCount: integer): boolean; virtual;
procedure AfterGetFrame(var frame: TWebSocketFrame); virtual;
procedure BeforeSendFrame(var frame: TWebSocketFrame); virtual;
function FrameData(const frame: TWebSocketFrame; const Head: RawUtf8;
HeadFound: PRawUtf8 = nil; PMax: PPByte = nil): pointer; virtual;
function FrameType(const frame: TWebSocketFrame): TShort31; virtual;
function GetRemoteIP: RawUtf8;
function GetEncrypted: boolean;
{$ifdef HASINLINE}inline;{$endif}
public
/// abstract constructor to initialize the protocol
// - the protocol should be named, so that the client may be able to request
// for a given protocol
// - if aUri is '', any URI would potentially upgrade to this protocol; you can
// specify an URI to limit the protocol upgrade to a single resource
constructor Create(const aName, aUri: RawUtf8); reintroduce;
/// compute a new instance of the WebSockets protocol, with same parameters
// - by default, will return nil, as expected for Client-side only
function Clone(const aClientUri: RawUtf8): TWebSocketProtocol; virtual;
/// the sub-protocols supported by this client (not used on server side)
// - as transmitted in the 'Sec-WebSocket-Protocol:' header during upgrade
// - returns Name by default, but could be e.g. 'synopsebin, synopsebinary'
// - some protocols have no sub-protocol, so would return '' here
function GetSubprotocols: RawUtf8; virtual;
/// recognize a supported sub-protocol (on both client and server sides)
// - should return true on success, i.e. if aProtocolName has been recognized
// - check against Name by default, but could be e.g. 'synopsebin, synopsebinary'
function SetSubprotocol(const aProtocolName: RawUtf8): boolean; virtual;
/// create the internal Encryption: IProtocol according to the supplied key
// - any asymmetric algorithm needs to know its side, i.e. client or server
// - use aKey='password#xxxxxx.private' for efAesCtr128 calling
// TEcdheProtocol.FromPasswordSecureFile() - FromKeySetCA() should have been
// called to set the global PKI
// - use aKey='a=mutual;e=aesctc128;p=34a2;pw=password;ca=..' full
// TEcdheProtocol.FromKey(aKey) format
// - or aKey will be derivated using aSettings to call
// SetEncryptKeyAes - default as 1024 PBKDF2-SHA-3 rounds into aes-128-ctr
// - you can disable encryption by setting aKey=''
procedure SetEncryptKey(aServer: boolean; const aKey: RawUtf8;
aSettings: PWebSocketProcessSettings);
/// set the fEncryption: IProtocol from TProtocolAes.Create()
// - if aClass is nil, TAesFast[mCtr] will be used as default
// - AEAD Cfc,mOfc,mCtc,mGcm modes will be rejected since unsupported
procedure SetEncryptKeyAes(aCipher: TAesAbstractClass;
const aKey; aKeySize: cardinal);
/// set the fEncryption: IProtocol from TEcdheProtocol.Create()
// - as default, we use efAesCtr128 which is the fastest on x86_64 (2.5GB/s)
procedure SetEncryptKeyEcdhe(aAuth: TEcdheAuth; aPKI: TEccCertificateChain;
aPrivate: TEccCertificateSecret; aServer: boolean;
aEF: TEcdheEF = efAesCtr128; aPrivateOwned: boolean = false);
/// redirect to Encryption.ProcessHandshake, if defined
function ProcessHandshake(const ExtIn: TRawUtf8DynArray;
out ExtOut: RawUtf8; ErrorMsg: PRawUtf8): boolean; virtual;
/// called e.g. for authentication during the WebSockets handshake
function ProcessHandshakeUri(const aClientUri: RawUtf8): boolean; virtual;
/// allow low-level interception before ProcessIncomingFrame is done
property OnBeforeIncomingFrame: TOnWebSocketProtocolIncomingFrame
read fOnBeforeIncomingFrame write fOnBeforeIncomingFrame;
/// access low-level frame encryption
property Encryption: IProtocol
read fEncryption;
/// contains either [hsrSecured, hsrWebsockets] or [hsrWebsockets]
property ConnectionFlags: THttpServerRequestFlags
read fConnectionFlags;
/// the associated low-level WebSocket connection numerical identifier
property ConnectionID: THttpServerConnectionID
read fConnectionID;
/// associated low-level opaque pointer maintained during the connection
property ConnectionOpaque: PHttpServerConnectionOpaque
read fConnectionOpaque;
/// if the associated 'Remote-IP' HTTP header value maps the local host
property RemoteLocalhost: boolean
read fRemoteLocalhost write fRemoteLocalhost;
published
/// the Sec-WebSocket-Protocol application name currently involved
// - e.g. 'synopsejson', 'synopsebin' or 'synopsebinary'
property Name: RawUtf8
read fName write fName;
/// the optional URI on which this protocol would be enabled
// - leave to '' if any URI should match
property URI: RawUtf8
read fUri;
/// the associated 'Remote-IP' HTTP header value
// - returns '' if self=nil or RemoteLocalhost=true
property RemoteIP: RawUtf8
read GetRemoteIP write fRemoteIP;
/// the URI on which this protocol has been upgraded
property UpgradeUri: RawUtf8
read fUpgradeUri write fUpgradeUri;
/// the "Bearer" HTTP header value on which this protocol has been upgraded
property UpgradeBearerToken: RawUtf8
read fUpgradeBearerToken;
/// the last error message, during frame processing
property LastError: string
read fLastError;
/// returns TRUE if encryption is enabled during the transmission
// - is currently only available for TWebSocketProtocolBinary
property Encrypted: boolean
read GetEncrypted;
/// how many frames have been received by this instance
property FramesInCount: integer
read fFramesInCount;
/// how many frames have been sent by this instance
property FramesOutCount: integer
read fFramesOutCount;
/// how many (uncompressed) bytes have been received by this instance
property FramesInBytes: QWord
read fFramesInBytes;
/// how many (uncompressed) bytes have been sent by this instance
property FramesOutBytes: QWord
read fFramesOutBytes;
end;
/// handle a REST application-level bi-directional WebSockets protocol
// - will emulate a bi-directional REST process, using THttpServerRequest to
// store and handle the request parameters: clients would be able to send
// regular REST requests to the server, but the server could use the same
// communication channel to push REST requests to the client
// - a local THttpServerRequest will be used on both client and server sides,
// to store REST parameters and compute the corresponding WebSockets frames
TWebSocketProtocolRest = class(TWebSocketProtocol)
protected
fSequencing: boolean;
fSequence: integer;
procedure ProcessIncomingFrame(Sender: TWebSocketProcess;
var Request: TWebSocketFrame; const Info: RawUtf8); override;
procedure FrameCompress(const Head: RawUtf8; const Values: array of const;
const Content, ContentType: RawByteString; var frame: TWebSocketFrame);
virtual; abstract;
function FrameDecompress(const frame: TWebSocketFrame;
const Head: RawUtf8; const values: array of PRawUtf8;
var contentType, content: RawUtf8): boolean; virtual; abstract;
/// convert the input information of REST request to a WebSocket frame
procedure InputToFrame(Ctxt: THttpServerRequestAbstract; aNoAnswer: boolean;
out request: TWebSocketFrame; out head: RawUtf8); virtual;
/// convert a WebSocket frame to the input information of a REST request
function FrameToInput(var request: TWebSocketFrame; out aNoAnswer: boolean;
Ctxt: THttpServerRequestAbstract): boolean; virtual;
/// convert a WebSocket frame to the output information of a REST request
function FrameToOutput(var answer: TWebSocketFrame;
Ctxt: THttpServerRequestAbstract): cardinal; virtual;
/// convert the output information of REST request to a WebSocket frame
procedure OutputToFrame(Ctxt: THttpServerRequestAbstract; Status: cardinal;
var outhead: RawUtf8; out answer: TWebSocketFrame); virtual;
end;
/// used to store the class of a TWebSocketProtocol type
TWebSocketProtocolClass = class of TWebSocketProtocol;
/// handle a REST application-level WebSockets protocol using JSON for transmission
// - could be used e.g. for AJAX or non Delphi remote access
// - this class will implement then following application-level protocol:
// $ Sec-WebSocket-Protocol: synopsejson
TWebSocketProtocolJson = class(TWebSocketProtocolRest)
protected
procedure FrameCompress(const Head: RawUtf8; const Values: array of const;
const Content, ContentType: RawByteString; var frame: TWebSocketFrame); override;
function FrameDecompress(const frame: TWebSocketFrame; const Head: RawUtf8;
const values: array of PRawUtf8;
var contentType, content: RawUtf8): boolean; override;
function FrameData(const frame: TWebSocketFrame; const Head: RawUtf8;
HeadFound: PRawUtf8 = nil; PMax: PPByte = nil): pointer; override;
function FrameType(const frame: TWebSocketFrame): TShort31; override;
public
/// initialize the WebSockets JSON protocol
// - if aUri is '', any URI would potentially upgrade to this protocol; you can
// specify an URI to limit the protocol upgrade to a single resource
constructor Create(const aUri: RawUtf8); reintroduce;
/// compute a new instance of the WebSockets protocol, with same parameters
function Clone(const aClientUri: RawUtf8): TWebSocketProtocol; override;
/// overriden method to compute and send all output as a single buffer
function SendFrames(Owner: TWebSocketProcess;
var Frames: TWebSocketFrameDynArray;
var FramesCount: integer): boolean; override;
end;
/// handle a REST application-level WebSockets protocol using compressed and
// optionally AES-CTR encrypted binary
// - this class will implement then following application-level protocol:
// $ Sec-WebSocket-Protocol: synopsebin
// or fallback to the previous subprotocol
// $ Sec-WebSocket-Protocol: synopsebinary
// - 'synopsebin' will expect requests sequenced as 'r000001','r000002',...
// headers matching 'a000001','a000002',... instead of 'request'/'answer'
TWebSocketProtocolBinary = class(TWebSocketProtocolRest)
protected
fFramesInBytesSocket: QWord;
fFramesOutBytesSocket: QWord;
fOptions: TWebSocketProtocolBinaryOptions;
procedure FrameCompress(const Head: RawUtf8;
const Values: array of const; const Content, ContentType: RawByteString;
var frame: TWebSocketFrame); override;
function FrameDecompress(const frame: TWebSocketFrame;
const Head: RawUtf8; const values: array of PRawUtf8;
var contentType, content: RawUtf8): boolean; override;
procedure AfterGetFrame(var frame: TWebSocketFrame); override;
procedure BeforeSendFrame(var frame: TWebSocketFrame); override;
function FrameData(const frame: TWebSocketFrame; const Head: RawUtf8;
HeadFound: PRawUtf8 = nil; PMax: PPByte = nil): pointer; override;
function FrameType(const frame: TWebSocketFrame): TShort31; override;
function SendFrames(Owner: TWebSocketProcess;
var Frames: TWebSocketFrameDynArray;
var FramesCount: integer): boolean; override;
procedure ProcessIncomingFrames(Sender: TWebSocketProcess; P, PMax: PByte);
procedure ProcessIncomingFrame(Sender: TWebSocketProcess;
var Request: TWebSocketFrame; const Info: RawUtf8); override;
function GetFramesInCompression: integer;
function GetFramesOutCompression: integer;
public
/// initialize the WebSockets binary protocol with no encryption
// - if aUri is '', any URI would potentially upgrade to this protocol; you
// can specify an URI to limit the protocol upgrade to a single resource
// - SynLZ compression is enabled by default, for all frames
constructor Create(const aUri: RawUtf8;
aOptions: TWebSocketProtocolBinaryOptions = [pboSynLzCompress]);
reintroduce; overload; virtual;
/// initialize the WebSockets binary protocol with a symmetric AES key
// - if aUri is '', any URI would potentially upgrade to this protocol; you
// can specify an URI to limit the protocol upgrade to a single resource
// - if aKeySize if 128, 192 or 256, TProtocolAes (i.e. AES-CTR encryption)
// will be used to secure the transmission
// - SynLZ compression is enabled by default, before encryption
constructor Create(const aUri: RawUtf8; const aKey; aKeySize: cardinal;
aOptions: TWebSocketProtocolBinaryOptions = [pboSynLzCompress];
aCipher: TAesAbstractClass = nil);
reintroduce; overload;
/// initialize the WebSockets binary protocol from a textual key
// - if aUri is '', any URI would potentially upgrade to this protocol; you
// can specify an URI to limit the protocol upgrade to a single resource
// - will create a TProtocolAes or TEcdheProtocol instance, corresponding to
// the supplied aKey and aServer values, to secure the transmission using
// a symmetric or assymmetric algorithm
// - SynLZ compression is enabled by default, unless aCompressed is false
constructor Create(const aUri: RawUtf8; aServer: boolean;
const aKey: RawUtf8; aSettings: PWebSocketProcessSettings;
aOptions: TWebSocketProtocolBinaryOptions = [pboSynLzCompress]);
reintroduce; overload;
/// compute a new instance of the WebSockets protocol, with same parameters
function Clone(const aClientUri: RawUtf8): TWebSocketProtocol; override;
/// overriden to return 'synopsebin, synopsebinary' sub-protocols
function GetSubprotocols: RawUtf8; override;
/// recognize our 'synopsebin, synopsebinary' sub-protocols
function SetSubprotocol(const aProtocolName: RawUtf8): boolean; override;
published
/// how compression / encryption is implemented during the transmission
// - is set to [pboSynLzCompress] by default
property Options: TWebSocketProtocolBinaryOptions
read fOptions write fOptions;
/// how many bytes have been received by this instance from the wire
property FramesInBytesSocket: QWord
read fFramesInBytesSocket;
/// how many bytes have been sent by this instance to the wire
property FramesOutBytesSocket: QWord
read fFramesOutBytesSocket;
/// compression ratio of frames received by this instance
property FramesInCompression: integer
read GetFramesInCompression;
/// compression ratio of frames Sent by this instance
property FramesOutCompression: integer
read GetFramesOutCompression;
end;
/// event signature trigerred from TWebSocketProtocolList.ServerUpgrade()
// - allow e.g. to verify a JWT bearer before returning the WS 101 response
// - Protocol.UpgradeUri/UpgradeBearerToken/RemoteIP/RemoteLocalhost and
// ConnectionID/ConnectionOpaque fields have already been populated
// - should return HTTP_SUCCESS to continue, or an error code to abort
TOnWebSocketProtocolUpgraded =
function(Protocol: TWebSocketProtocol): integer of object;
/// event signature trigerred on WS connection close
TOnWebSocketProtocolClosed =
procedure(Protocol: TWebSocketProtocol) of object;
/// used to maintain a list of websocket protocols (for the server side)
TWebSocketProtocolList = class(TObjectRWLightLock)
protected
fProtocols: array of TWebSocketProtocol;
fOnUpgraded: TOnWebSocketProtocolUpgraded;
// caller should make fSafe.ReadOnlyLock/WriteLock
function LockedFindIndex(const aName, aUri: RawUtf8): PtrInt;
public
/// add a protocol to the internal list
// - returns TRUE on success
// - if this protocol is already existing for this given name and URI,
// returns FALSE: it is up to the caller to release aProtocol if needed
function Add(aProtocol: TWebSocketProtocol): boolean;
/// add once a protocol to the internal list
// - if this protocol is already existing for this given name and URI, any
// previous one will be released - so it may be confusing on a running server
// - returns TRUE if the protocol was added for the first time, or FALSE
// if the protocol has been replaced or is invalid (e.g. aProtocol=nil)
function AddOnce(aProtocol: TWebSocketProtocol): boolean;
/// erase a protocol from the internal list, specified by its name
function Remove(const aProtocolName, aUri: RawUtf8): boolean;
/// finalize the list storage
destructor Destroy; override;
/// create a new protocol instance, from the internal list
function CloneByName(
const aProtocolName, aClientUri: RawUtf8): TWebSocketProtocol;
/// create a new protocol instance, from the internal list
function CloneByUri(const aClientUri: RawUtf8): TWebSocketProtocol;
/// how many protocols are stored
function Count: integer;
/// server-side HTTP Upgrade to one supported WebSockets protocols
// - if returns, HTTP_SUCCESS caller should send the Response headers
// and use the Protocol - or free it and close the connection
function ServerUpgrade(const Http: THttpRequestContext;
const RemoteIp: RawUtf8; ConnectionID: THttpServerConnectionID;
ConnectionOpaque: PHttpServerConnectionOpaque;
out Protocol: TWebSocketProtocol; out Response: RawUtf8): integer;
/// callback event run from ServerUpgrade
property OnUpgraded: TOnWebSocketProtocolUpgraded
read fOnUpgraded write fOnUpgraded;
end;
/// indicates which kind of process did occur in the main WebSockets loop
TWebSocketProcessOne = (
wspNone,
wspPing,
wspDone,
wspAnswer,
wspError,
wspClosed);
/// indicates how TWebSocketProcess.NotifyCallback() will work
TWebSocketProcessNotifyCallback = (
wscBlockWithAnswer,
wscBlockWithoutAnswer,
wscNonBlockWithoutAnswer);
/// used to manage a thread-safe list of WebSockets frames
// - TSynLocked because SendPendingOutgoingFrames() locks it and may take time
TWebSocketFrameList = class(TSynLocked)
protected
fTimeoutSec: cardinal;
fAnswerToIgnore: integer;
procedure Delete(i: PtrInt);
public
/// low-level access to the WebSocket frames list
List: TWebSocketFrameDynArray;
/// current number of WebSocket frames in the list
Count: integer;
/// initialize the list
constructor Create(timeoutsec: cardinal); reintroduce;
/// add a WebSocket frame in the list
// - this method is thread-safe
procedure Push(const frame: TWebSocketFrame; currentSec: cardinal);
/// add a void WebSocket frame in the list
// - this method is thread-safe
procedure PushVoidFrame(opcode: TWebSocketFrameOpCode; currentSec: cardinal);
/// retrieve a WebSocket frame from the list, oldest first
// - you should specify a frame type to search for, according to the
// specified WebSockets protocl
// - this method is thread-safe
function Pop(protocol: TWebSocketProtocol; const head: RawUtf8;
out frame: TWebSocketFrame; currentSec: cardinal): boolean;
/// how many 'answer' frames are to be ignored
// - incdec should be either 0, -1 or +1
// - this method is thread-safe
function AnswerToIgnore(incdec: integer = 0): integer;
end;
/// a WebSocket protocol able to generate ephemeral connection URI
// - on JavaScript, it is not possible to set a HTTP header bearer to
// authenticate: so this class generates and recognizes formatted URIs
// - inherited classes should override the ProcessIncomingFrame() method
TWebSocketProtocolUri = class(TWebSocketProtocol)
protected
fSession: TBinaryCookieGeneratorSessionID;
fCreated: cardinal;
fGenerator: PBinaryCookieGenerator;
fRecordTypeInfo: PRttiInfo;
fGeneratorOwned: boolean;
fRecordData: pointer;
fPublicUri: RawUtf8;
public
/// initialize the protocol for a given Jwt
// - if aExpirationMinutes is set, will own a new URI generator
// - aPublicUri is mandatory and will be used by NewUri, typically equals
// '127.0.0.1:888' or 'publicdomain.com/websockgateway'
// - each time this protocol is setup, a random seed is used for NewUri
// - aRecordTypeInfo can optionally associate a record to each URI
constructor Create(const aName, aPublicUri: RawUtf8;
aExpirationMinutes: integer; aRecordTypeInfo: PRttiInfo);
reintroduce; virtual;
/// finalize the protocol definition
destructor Destroy; override;
/// validate the URI supplied during connection upgrade on server side
function ProcessHandshakeUri(const aClientUri: RawUtf8): boolean; override;
/// called when a new connection upgrade attempt is received on server side
function Clone(const aClientUri: RawUtf8): TWebSocketProtocol; override;
/// high-level code should call this method to generate a valid URI
// - WebSockets connection on this URI will be upgraded by this protocol
// - URI are obfuscated and signed with an ephemeral TBinaryCookieGenerator,
// so will have an unique Session ID, and can be associated with a record
// - this method is thread-safe
function NewUri(out SessionID: TBinaryCookieGeneratorSessionID;
PRecordData: pointer = nil): RawUtf8; virtual;
/// access to the low-level ephemeral URI generator
property Generator: PBinaryCookieGenerator
read fGenerator;
/// optional associated record, as recognized by ProcessHandshakeUri()
// - is a pointer to a RecordTypeInfo record, owned by this instance
property RecordData: pointer
read fRecordData;
/// access to the associated record RTTI definition
property RecordTypeInfo: PRttiInfo
read fRecordTypeInfo;
published
/// the public Root URI as used by NewUri
// - e.g. '127.0.0.1:888' or 'publicdomain.com/wsgateway'
property PublicUri: RawUtf8
read fPublicUri;
/// the session 32-bit identifier, as generated by NewUri and recognized
// by ProcessHandshakeUri() during WebSockets upgrade
property Session: TBinaryCookieGeneratorSessionID
read fSession;
/// server-side UnixTimeUtc value when the recognized URI was generated
property Created: cardinal
read fCreated;
end;
TWebSocketProtocolUriClass = class of TWebSocketProtocolUri;
{ ******************** WebSockets Client and Server Shared Process }
/// the current state of the WebSockets process
TWebSocketProcessState = (
wpsCreate,
wpsRun,
wpsClose,
wpsDestroy);
/// abstract WebSockets process, used on both client or server sides
// - CanGetFrame/ReceiveBytes/SendBytes abstract methods should be overriden with
// actual communication, and fState and ProcessStart/ProcessStop should be
// updated from the actual processing thread (e.g. as in TWebCrtSocketProcess)
TWebSocketProcess = class(TSynPersistent)
protected
fProtocol: TWebSocketProtocol;
fConnectionID: THttpServerConnectionID;
fIncoming: TWebSocketFrameList;
fOutgoing: TWebSocketFrameList;
fOwnerThread: TSynThread;
fState: TWebSocketProcessState;
fMaskSentFrames: byte;
fProcessEnded: boolean;
fConnectionCloseWasSent: boolean;
fNoLastSocketTicks: boolean;
fProcessCount: integer;
fInvalidPingSendCount: cardinal;
fSettings: PWebSocketProcessSettings;
fSafeIn, fSafeOut: TRTLCriticalSection;
fLastSocketTicks: Int64;
fProcessName: RawUtf8;
procedure MarkAsInvalid;
function LastPingDelay: Int64;
procedure SetLastPingTicks;
procedure SendPing;
/// callback methods run by ProcessLoop
procedure ProcessStart; virtual;
procedure ProcessStop; virtual;
// called by ProcessLoop - TRUE=continue, FALSE=ended
// - caller may have checked that some data is pending to read
function ProcessLoopStepReceive(FrameProcessed: PBoolean): boolean;
procedure ProcessLoopReceived(var request: TWebSocketFrame);
// called by ProcessLoop - TRUE=continue, FALSE=ended
// - caller may check that LastPingDelay>fSettings.SendDelay and Socket is writable
function ProcessLoopStepSend: boolean;
// blocking process, for one thread handling all WebSocket connection process
procedure ProcessLoop;
function ComputeContext(
out RequestProcess: TOnHttpServerRequest): THttpServerRequestAbstract;
virtual; abstract;
procedure Log(const frame: TWebSocketFrame; const aMethodName: ShortString;
aEvent: TSynLogLevel = sllTrace; DisableRemoteLog: boolean = false); virtual;
function SendPendingOutgoingFrames: integer;
function HiResDelay(var start: Int64): Int64;
public
/// initialize the WebSockets process on a given connection
// - the supplied TWebSocketProtocol will be owned by this instance
// - other parameters should reflect the client or server expectations
constructor Create(aProtocol: TWebSocketProtocol; aOwnerThread: TSynThread;
aSettings: PWebSocketProcessSettings;
const aProcessName: RawUtf8); reintroduce;
/// finalize the context
// - if needed, will notify the other end with a focConnectionClose frame
// - will release the TWebSocketProtocol associated instance
destructor Destroy; override;
/// abstract low-level method to retrieve pending input data
// - should return the number of bytes (<=count) received and written to P
// - is defined separated to allow multi-thread pooling
function ReceiveBytes(P: PAnsiChar; count: PtrInt): integer; virtual; abstract;
/// abstract low-level method to send pending output data
// - returns false on any error, try on success
// - is defined separated to allow multi-thread pooling
function SendBytes(P: pointer; Len: PtrInt): boolean; virtual; abstract;
/// abstract low-level method to check if there is some pending input data
// in the input Socket ready for GetFrame/ReceiveBytes
// - is defined separated to allow multi-thread pooling
function CanGetFrame(TimeOut: cardinal;
ErrorWithoutException: PInteger): boolean; virtual; abstract;
/// (blocking) process incoming WebSockets framing protocol
// - CanGetFrame should have been called and returned true before
// - will call overriden ReceiveBytes() for the actual communication
function GetFrame(out Frame: TWebSocketFrame; Blocking: boolean;
ErrorWithoutException: PInteger): boolean;
/// process outgoing WebSockets framing protocol
// - will call overriden SendBytes() for immediate transmission
// - use SendFrameAsync() to send frames asynchronously S(with optional
// jumboframes gathering)
function SendFrame(var Frame: TWebSocketFrame): boolean;
/// delayed process of outgoing WebSockets framing protocol
// - by default, store the frame in Outgoing.Push() internal list
// - some protocols could implement optional jumboframe gathering
procedure SendFrameAsync(const Frame: TWebSocketFrame); virtual;
/// will push a request or notification to the other end of the connection
// - caller should set the aRequest with the outgoing parameters, and
// optionally receive a response from the other end
// - the request may be sent in blocking or non blocking mode
// - returns the HTTP Status code (e.g. HTTP_SUCCESS=200 for success)
function NotifyCallback(aRequest: THttpServerRequestAbstract;
aMode: TWebSocketProcessNotifyCallback): cardinal; virtual;
/// send a focConnectionClose frame (if not already sent) and set wpsClose
procedure Shutdown(waitForPong: boolean);
/// returns the current state of the underlying connection
function State: TWebSocketProcessState;
{$ifdef HASINLINE}inline;{$endif}
/// the associated 'Remote-IP' HTTP header value
// - returns '' if Protocol=nil or Protocol.RemoteLocalhost=true
function RemoteIP: RawUtf8;
{$ifdef HASINLINE}inline;{$endif}
/// the settings currently used during the WebSockets process
// - points to the owner instance, e.g. TWebSocketServer.Settings or
// THttpClientWebSockets.Settings field
property Settings: PWebSocketProcessSettings
read fSettings;
/// direct access to the low-level incoming frame stack
property Incoming: TWebSocketFrameList
read fIncoming;
/// direct access to the low-level outgoing frame stack
// - you should not use this property, but SendFrameAsync() virtual method
property Outgoing: TWebSocketFrameList
read fOutgoing;
/// the associated low-level processing thread
property OwnerThread: TSynThread
read fOwnerThread;
/// how many frames are currently processed by this connection
property ProcessCount: integer
read fProcessCount;
/// may be set to TRUE before Destroy to force raw socket disconnection
property ConnectionCloseWasSent: boolean
read fConnectionCloseWasSent write fConnectionCloseWasSent;
published
/// the Sec-WebSocket-Protocol application protocol currently involved
// - TWebSocketProtocolJson or TWebSocketProtocolBinary in the mORMot context
// - could be nil if the connection is in standard HTTP/1.1 mode
property Protocol: TWebSocketProtocol
read fProtocol;
/// the associated process name
property ProcessName: RawUtf8
read fProcessName write fProcessName;
/// how many invalid heartbeat frames have been sent
// - a non 0 value indicates a connection problem
property InvalidPingSendCount: cardinal
read fInvalidPingSendCount;
end;
/// TCrtSocket-based WebSockets process, used on both client or server sides
// - will use the socket in blocking mode, so expects its own processing thread
TWebCrtSocketProcess = class(TWebSocketProcess)
protected
fSocket: TCrtSocket;
public
/// initialize the WebSockets process on a given TCrtSocket connection
// - the supplied TWebSocketProtocol will be owned by this instance
// - other parameters should reflect the client or server expectations
constructor Create(aSocket: TCrtSocket; aProtocol: TWebSocketProtocol;
aOwnerThread: TSynThread; aSettings: PWebSocketProcessSettings;
const aProcessName: RawUtf8); reintroduce; virtual;
/// first step of the low level incoming WebSockets framing protocol over TCrtSocket
// - call fSocket.SockInPending to check for pending data
function CanGetFrame(TimeOut: cardinal;
ErrorWithoutException: PInteger): boolean; override;
/// low level receive incoming WebSockets frame data over TCrtSocket
// - call fSocket.SockInRead to check for pending data
function ReceiveBytes(P: PAnsiChar; count: PtrInt): integer; override;
/// low-level method to send pending output data over TCrtSocket
// - call fSocket.TrySndLow to send pending data
function SendBytes(P: pointer; Len: PtrInt): boolean; override;
/// the associated communication socket
// - on the server side, is a THttpServerSocket
// - access to this instance is protected by Safe.Lock/Unlock
property Socket: TCrtSocket
read fSocket;
end;
/// returns the text corresponding to a specified WebSockets sending mode
function ToText(mode: TWebSocketProcessNotifyCallback): PShortString; overload;
/// returns the text corresponding to a specified WebSockets state
function ToText(st: TWebSocketProcessState): PShortString; overload;
{ ******************** WebSockets Asynchronous Frames Parsing }
type
/// define our work memory buffer for low-level WebSockets frame headers
TFrameHeader = packed record
first: byte;
len8: byte;
len32: cardinal;
len64: cardinal;
mask: cardinal; // mask=0 indicates no payload masking
end;
/// states of the WebSockets parsing asynchronous machine
TWebProcessInFrameState = (
pfsHeader1,
pfsData1,
pfsHeaderN,
pfsDataN,
pfsDone,
pfsError);
/// asynchronous state machine to process WebSockets incoming frames
{$ifdef USERECORDWITHMETHODS}
TWebProcessInFrame = record
{$else}
TWebProcessInFrame = object
{$endif USERECORDWITHMETHODS}
public
hdr: TFrameHeader;
opcode: TWebSocketFrameOpCode;
masked: boolean;
state: TWebProcessInFrameState;
process: TWebSocketProcess;
outputframe: PWebSocketFrame;
len: integer;
data: RawByteString; // will eventually be appended to outputframe.payload
procedure Init(Owner: TWebSocketProcess; output: PWebSocketFrame);
function HasBytes(P: PAnsiChar; count: integer): boolean;
{$ifdef HASINLINE} inline; {$endif}
function GetHeader: boolean;
function GetData: boolean;
function Step(ErrorWithoutException: PInteger): TWebProcessInFrameState;
end;
/// reusable encoder for WebSockets outgoing frames
{$ifdef USERECORDWITHMETHODS}
TWebSocketFrameEncoder = record
{$else}
TWebSocketFrameEncoder = object
{$endif USERECORDWITHMETHODS}
public
hdr: TFrameHeader;
hdrlen, len: cardinal;
function Prepare(const Frame: TWebSocketFrame; MaskSentFrames: cardinal): integer;
function Encode(const Frame: TWebSocketFrame; Dest: PAnsiChar): integer;
end;
{ ******************** TWebSocketProtocolChat Simple Protocol }
type
/// callback event triggered by TWebSocketProtocolChat for any incoming message
// - a first call with frame.opcode=focContinuation will take place when
// the connection will be upgraded to WebSockets
// - then any incoming focText/focBinary events will trigger this callback
// - eventually, a focConnectionClose will notify the connection ending
TOnWebSocketProtocolChatIncomingFrame = procedure(
Sender: TWebSocketProcess; const Frame: TWebSocketFrame) of object;
/// simple chatting protocol, allowing to receive and send WebSocket frames
// - you can use this protocol to implement simple asynchronous communication
// with events expecting no answers, e.g. from or as AJAX applications
// - as used e.g. by sample ex/rest-websockets/restws_simpleechoserver.dpr
// - see TWebSocketProtocolRest for bi-directional events expecting answers,
// as between mORMot client and server
TWebSocketProtocolChat = class(TWebSocketProtocol)
protected
fOnIncomingFrame: TOnWebSocketProtocolChatIncomingFrame;
procedure ProcessIncomingFrame(Sender: TWebSocketProcess;
var Request: TWebSocketFrame; const Info: RawUtf8); override;
public
/// initialize the chat protocol with an incoming frame callback
// - if you need no "Sec-WebSocket-Protocol:" header, specify aName = ''
constructor Create(const aName, aUri: RawUtf8;
const aOnIncomingFrame: TOnWebSocketProtocolChatIncomingFrame); overload;
/// compute a new instance of this WebSockets protocol, with same parameters
function Clone(const aClientUri: RawUtf8): TWebSocketProtocol; override;
/// allows to send a message over the wire to a specified connection
// - a temporary copy of the Frame content will be made for safety