jspurefix 5.10.5 → 5.10.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/transport/ascii/ascii-session-msg-factory.d.ts +1 -1
- package/dist/transport/ascii/ascii-session-msg-factory.js +2 -2
- package/dist/transport/ascii/ascii-session-msg-factory.js.map +1 -1
- package/dist/transport/ascii/ascii-session.d.ts +6 -0
- package/dist/transport/ascii/ascii-session.js +72 -8
- package/dist/transport/ascii/ascii-session.js.map +1 -1
- package/dist/transport/session/fix-session.d.ts +1 -1
- package/dist/transport/session/fix-session.js.map +1 -1
- package/jsfix.test_client.txt +62 -62
- package/jsfix.test_server.txt +72 -72
- package/package.json +1 -1
- package/src/transport/ascii/ascii-session-msg-factory.ts +8 -2
- package/src/transport/ascii/ascii-session.ts +115 -7
- package/src/transport/session/fix-session.ts +12 -2
|
@@ -37,6 +37,7 @@ export abstract class AsciiSession extends FixSession {
|
|
|
37
37
|
protected readonly wildcardTarget: boolean
|
|
38
38
|
private identityBound: boolean
|
|
39
39
|
private bindInProgress: boolean = false
|
|
40
|
+
private logonVerdictPending: boolean = false
|
|
40
41
|
private readonly pendingRx: Array<{ msgType: string, view: MsgView }> = []
|
|
41
42
|
private readonly clock: DefaultFixClock = new DefaultFixClock()
|
|
42
43
|
|
|
@@ -548,6 +549,13 @@ export abstract class AsciiSession extends FixSession {
|
|
|
548
549
|
this.onMsgBeforeIdentity(msgType, view)
|
|
549
550
|
return
|
|
550
551
|
}
|
|
552
|
+
if (this.logonVerdictPending) {
|
|
553
|
+
// the application is still deciding whether to accept this peer - hold anything
|
|
554
|
+
// it sends in the meantime rather than acting on it (see awaitLogonVerdict)
|
|
555
|
+
this.sessionLogger.info(`queueing '${msgType}' received while awaiting logon verdict`)
|
|
556
|
+
this.pendingRx.push({ msgType, view: view.clone() })
|
|
557
|
+
return
|
|
558
|
+
}
|
|
551
559
|
this.dispatchMsg(msgType, view)
|
|
552
560
|
}
|
|
553
561
|
|
|
@@ -587,9 +595,11 @@ export abstract class AsciiSession extends FixSession {
|
|
|
587
595
|
this.bindIdentity(peerCompId).then(() => {
|
|
588
596
|
this.bindInProgress = false
|
|
589
597
|
this.identityBound = true
|
|
598
|
+
// dispatching the logon can itself open a second gate - an application whose
|
|
599
|
+
// onLogon returns a promise is now deciding - so let resumeRx work out whether
|
|
600
|
+
// it is actually safe to let the socket run again
|
|
590
601
|
this.dispatchMsg(MsgType.Logon, logon)
|
|
591
|
-
this.
|
|
592
|
-
this.transport?.duplex.readable?.resume()
|
|
602
|
+
this.resumeRx()
|
|
593
603
|
}).catch((e: Error) => {
|
|
594
604
|
this.bindInProgress = false
|
|
595
605
|
this.pendingRx.length = 0
|
|
@@ -597,8 +607,34 @@ export abstract class AsciiSession extends FixSession {
|
|
|
597
607
|
})
|
|
598
608
|
}
|
|
599
609
|
|
|
610
|
+
/**
|
|
611
|
+
* True while an asynchronous step in the logon handshake is still running and no
|
|
612
|
+
* received message may be acted on: either the identity bind of a wildcard acceptor
|
|
613
|
+
* or an application still deciding the logon verdict.
|
|
614
|
+
*/
|
|
615
|
+
private rxGated (): boolean {
|
|
616
|
+
return this.bindInProgress || this.logonVerdictPending
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Let the socket run again now a gate has opened - but only if no other gate is
|
|
621
|
+
* still shut. Draining can re-gate (a replayed message could start another async
|
|
622
|
+
* step) so the check is repeated after the queue is played out; whichever gate
|
|
623
|
+
* closes last is the one that resumes.
|
|
624
|
+
*/
|
|
625
|
+
private resumeRx (): void {
|
|
626
|
+
if (this.rxGated()) return
|
|
627
|
+
this.drainPendingRx()
|
|
628
|
+
if (this.rxGated()) return
|
|
629
|
+
this.transport?.duplex.readable?.resume()
|
|
630
|
+
}
|
|
631
|
+
|
|
600
632
|
private drainPendingRx (): void {
|
|
601
633
|
while (this.pendingRx.length > 0) {
|
|
634
|
+
if (this.rxGated()) {
|
|
635
|
+
this.sessionLogger.info('suspending replay - logon handshake gated again')
|
|
636
|
+
return
|
|
637
|
+
}
|
|
602
638
|
const queued = this.pendingRx.shift()
|
|
603
639
|
if (!queued) break
|
|
604
640
|
if (this.sessionState.state === SessionState.Stopped) {
|
|
@@ -734,9 +770,66 @@ export abstract class AsciiSession extends FixSession {
|
|
|
734
770
|
const state = this.sessionState
|
|
735
771
|
state.peerHeartBeatSecs = view.getTyped(MsgTag.HeartBtInt) as number
|
|
736
772
|
state.peerCompId = view.getTyped(MsgTag.SenderCompID) as string
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
773
|
+
|
|
774
|
+
const verdict = this.onLogon(view, userName as string, password as string)
|
|
775
|
+
if (typeof verdict === 'boolean') {
|
|
776
|
+
// the common case - finish the handshake in this same event, so an application
|
|
777
|
+
// with a synchronous onLogon sees exactly the timing it always has
|
|
778
|
+
this.completePeerLogon(view, verdict, resetSeqNumFlag)
|
|
779
|
+
return
|
|
780
|
+
}
|
|
781
|
+
this.awaitLogonVerdict(view, verdict, resetSeqNumFlag)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* onLogon handed back a promise - the application is checking the credentials
|
|
786
|
+
* against something asynchronous. Nothing may be sent or acted on until it
|
|
787
|
+
* answers, so pause the socket and queue whatever the peer says in the meantime.
|
|
788
|
+
*
|
|
789
|
+
* The view has to be cloned: AsciiParser recycles its tag index and buffer as soon
|
|
790
|
+
* as the current event returns, so the original is meaningless by the time the
|
|
791
|
+
* promise settles. Anything already read off it - peerReset here - is carried
|
|
792
|
+
* across as a value rather than read again from the clone.
|
|
793
|
+
*/
|
|
794
|
+
private awaitLogonVerdict (view: MsgView, verdict: Promise<boolean>, peerReset: boolean | undefined): void {
|
|
795
|
+
const logger = this.sessionLogger
|
|
796
|
+
logger.info('onLogon returned a promise - awaiting logon verdict')
|
|
797
|
+
this.logonVerdictPending = true
|
|
798
|
+
const logon = view.clone()
|
|
799
|
+
this.transport?.duplex.readable?.pause()
|
|
800
|
+
|
|
801
|
+
verdict.then((accepted: boolean) => {
|
|
802
|
+
this.logonVerdictPending = false
|
|
803
|
+
if (this.sessionState.state === SessionState.Stopped) {
|
|
804
|
+
logger.info('session stopped while awaiting logon verdict - discarding')
|
|
805
|
+
this.pendingRx.length = 0
|
|
806
|
+
return
|
|
807
|
+
}
|
|
808
|
+
this.completePeerLogon(logon, accepted, peerReset)
|
|
809
|
+
if (!accepted) {
|
|
810
|
+
// completePeerLogon has torn the session down - nothing queued can be acted on
|
|
811
|
+
this.pendingRx.length = 0
|
|
812
|
+
return
|
|
813
|
+
}
|
|
814
|
+
this.resumeRx()
|
|
815
|
+
}).catch((e: Error) => {
|
|
816
|
+
this.logonVerdictPending = false
|
|
817
|
+
this.pendingRx.length = 0
|
|
818
|
+
// a credential check that throws is a refusal, not a reason to let the peer in
|
|
819
|
+
this.rejectLogon(`onLogon failed: ${e.message}`)
|
|
820
|
+
})
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* The application has accepted or refused the peer - finish the handshake either way.
|
|
825
|
+
*/
|
|
826
|
+
private completePeerLogon (view: MsgView, accepted: boolean, peerReset: boolean | undefined): void {
|
|
827
|
+
const logger = this.sessionLogger
|
|
828
|
+
logger.info(`peerLogon onLogon returns ${accepted}`)
|
|
829
|
+
if (!accepted) {
|
|
830
|
+
this.rejectLogon('logon rejected by application')
|
|
831
|
+
return
|
|
832
|
+
}
|
|
740
833
|
if (this.acceptor) {
|
|
741
834
|
this.setState(SessionState.InitiationLogonResponse)
|
|
742
835
|
logger.info('acceptor responds to logon request')
|
|
@@ -745,7 +838,7 @@ export abstract class AsciiSession extends FixSession {
|
|
|
745
838
|
// reset our sequences before sending our logon response.
|
|
746
839
|
// This handles the broker-reset pattern where client sends N, we respond with Y.
|
|
747
840
|
const weReset = this.config.description.ResetSeqNumFlag
|
|
748
|
-
if (weReset &&
|
|
841
|
+
if (weReset && peerReset !== true) {
|
|
749
842
|
logger.info('acceptor sending ResetSeqNumFlag=Y (peer sent N), resetting sequences')
|
|
750
843
|
// Fire-and-forget async coordinator call, set values synchronously
|
|
751
844
|
this.coordinator.resetAsAcceptor().catch((e: Error) => {
|
|
@@ -764,7 +857,7 @@ export abstract class AsciiSession extends FixSession {
|
|
|
764
857
|
}
|
|
765
858
|
}
|
|
766
859
|
|
|
767
|
-
this.sendLogon()
|
|
860
|
+
this.sendLogon()
|
|
768
861
|
} else { // as an initiator the acceptor has responded
|
|
769
862
|
logger.info('initiator receives logon response')
|
|
770
863
|
this.setState(SessionState.InitiationLogonReceived)
|
|
@@ -779,6 +872,21 @@ export abstract class AsciiSession extends FixSession {
|
|
|
779
872
|
this.onReady(view)
|
|
780
873
|
}
|
|
781
874
|
|
|
875
|
+
/**
|
|
876
|
+
* The application refused this peer. FIX expects the refusing side to say why and
|
|
877
|
+
* then hang up rather than simply going quiet, so send a Logout carrying the reason
|
|
878
|
+
* before dropping the transport - the order matters, send() discards anything
|
|
879
|
+
* offered once the session has stopped.
|
|
880
|
+
*
|
|
881
|
+
* onReady is never reached and no heartbeat timer starts; the application learns the
|
|
882
|
+
* outcome through onStopped(error).
|
|
883
|
+
*/
|
|
884
|
+
private rejectLogon (reason: string): void {
|
|
885
|
+
this.sessionLogger.warning(`refusing peer logon: ${reason}`)
|
|
886
|
+
this.sendLogout(reason)
|
|
887
|
+
this.terminate(new Error(reason))
|
|
888
|
+
}
|
|
889
|
+
|
|
782
890
|
private sendTestRequest (): void {
|
|
783
891
|
const factory = this.config.factory
|
|
784
892
|
this.setState(SessionState.AwaitingProcessingResponseToTestRequest)
|
|
@@ -631,11 +631,21 @@ export abstract class FixSession extends events.EventEmitter {
|
|
|
631
631
|
*/
|
|
632
632
|
protected abstract onStopped (error?: Error): void
|
|
633
633
|
/**
|
|
634
|
-
*
|
|
634
|
+
* Inform the application of a peer login attempt and ask it to accept or reject.
|
|
635
|
+
*
|
|
636
|
+
* Return true to let the session continue. Return false to refuse the peer: a
|
|
637
|
+
* Logout carrying a Text reason is sent and the transport is dropped, so onReady
|
|
638
|
+
* is never reached and no heartbeat is started.
|
|
639
|
+
*
|
|
640
|
+
* The verdict may be returned as a promise, for applications that must check the
|
|
641
|
+
* credentials against an async source such as an http api or a database. While
|
|
642
|
+
* that promise is pending the transport is paused and any message the peer sends
|
|
643
|
+
* is queued, then replayed in order once the verdict arrives.
|
|
644
|
+
*
|
|
635
645
|
* @param view the login message
|
|
636
646
|
* @param user extracted user from message
|
|
637
647
|
* @param password extracted password from the message.
|
|
638
648
|
* @protected
|
|
639
649
|
*/
|
|
640
|
-
protected abstract onLogon (view: MsgView, user: string, password: string): boolean
|
|
650
|
+
protected abstract onLogon (view: MsgView, user: string, password: string): boolean | Promise<boolean>
|
|
641
651
|
}
|