jspurefix 5.8.5 → 5.9.2
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/.gitattributes +4 -0
- package/BACKPORT_PLAN.md +84 -0
- package/DEMO_PORT_PLAN.md +10 -0
- package/README.md +3 -0
- package/WORKLOG.md +119 -0
- package/dist/benchmark/bench-report.d.ts +5 -0
- package/dist/benchmark/bench-report.js +91 -0
- package/dist/benchmark/bench-report.js.map +1 -0
- package/dist/benchmark/bench-runner.d.ts +12 -0
- package/dist/benchmark/bench-runner.js +193 -0
- package/dist/benchmark/bench-runner.js.map +1 -0
- package/dist/benchmark/bench-stats.d.ts +10 -0
- package/dist/benchmark/bench-stats.js +56 -0
- package/dist/benchmark/bench-stats.js.map +1 -0
- package/dist/benchmark/bench-types.d.ts +74 -0
- package/dist/benchmark/bench-types.js +3 -0
- package/dist/benchmark/bench-types.js.map +1 -0
- package/dist/benchmark/gc-probe.d.ts +18 -0
- package/dist/benchmark/gc-probe.js +103 -0
- package/dist/benchmark/gc-probe.js.map +1 -0
- package/dist/benchmark/index.d.ts +6 -0
- package/dist/benchmark/index.js +23 -0
- package/dist/benchmark/index.js.map +1 -0
- package/dist/benchmark/parse-bench.d.ts +28 -0
- package/dist/benchmark/parse-bench.js +85 -0
- package/dist/benchmark/parse-bench.js.map +1 -0
- package/dist/buffer/ascii/ascii-parser.d.ts +1 -0
- package/dist/buffer/ascii/ascii-parser.js +4 -1
- package/dist/buffer/ascii/ascii-parser.js.map +1 -1
- package/dist/buffer/ascii/ascii-segment-parser.js +3 -2
- package/dist/buffer/ascii/ascii-segment-parser.js.map +1 -1
- package/dist/buffer/ascii/tag-index.js.map +1 -1
- package/dist/buffer/msg-view.d.ts +1 -0
- package/dist/buffer/msg-view.js +25 -19
- package/dist/buffer/msg-view.js.map +1 -1
- package/dist/config/js-fix-config.d.ts +4 -0
- package/dist/config/js-fix-config.js.map +1 -1
- package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +1 -1
- package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -1
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +1 -1
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +1 -2
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -1
- package/dist/dictionary/parser/quickfix/validation-error.d.ts +2 -2
- package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -1
- package/dist/jsfix-bench.d.ts +1 -0
- package/dist/jsfix-bench.js +213 -0
- package/dist/jsfix-bench.js.map +1 -0
- package/dist/runtime/session-launcher.js +1 -1
- package/dist/runtime/session-launcher.js.map +1 -1
- package/dist/runtime/session-scope.d.ts +3 -0
- package/dist/runtime/session-scope.js +47 -0
- package/dist/runtime/session-scope.js.map +1 -0
- package/dist/sample/http/oms/app.js +3 -3
- package/dist/sample/http/oms/app.js.map +1 -1
- package/dist/sample/tcp/qf-md/app.js +3 -3
- package/dist/sample/tcp/qf-md/app.js.map +1 -1
- package/dist/sample/tcp/skeleton/app.js +1 -1
- package/dist/sample/tcp/skeleton/app.js.map +1 -1
- package/dist/sample/tcp/trade-capture/app-launcher.js +3 -3
- package/dist/sample/tcp/trade-capture/app-launcher.js.map +1 -1
- package/dist/store/file-session-store.js +1 -1
- package/dist/store/file-session-store.js.map +1 -1
- package/dist/store/file-session-stream-provider.d.ts +1 -0
- package/dist/store/file-session-stream-provider.js +13 -13
- package/dist/store/file-session-stream-provider.js.map +1 -1
- package/dist/store/fix-session-store-factory.d.ts +1 -1
- package/dist/store/fix-session-store-factory.js.map +1 -1
- package/dist/store/fix-session-store.d.ts +11 -11
- package/dist/store/fix-session-store.js.map +1 -1
- package/dist/store/memory-session-store.js.map +1 -1
- package/dist/store/session-stream-provider.d.ts +13 -13
- package/dist/store/session-stream-provider.js.map +1 -1
- package/dist/transport/ascii/ascii-session.d.ts +17 -3
- package/dist/transport/ascii/ascii-session.js +121 -8
- package/dist/transport/ascii/ascii-session.js.map +1 -1
- package/dist/transport/duplex/fix-duplex.d.ts +1 -0
- package/dist/transport/duplex/fix-duplex.js +3 -0
- package/dist/transport/duplex/fix-duplex.js.map +1 -1
- package/dist/transport/duplex/tcp-duplex.d.ts +1 -0
- package/dist/transport/duplex/tcp-duplex.js +5 -0
- package/dist/transport/duplex/tcp-duplex.js.map +1 -1
- package/dist/transport/factory/msg-transport.d.ts +2 -0
- package/dist/transport/factory/msg-transport.js +17 -2
- package/dist/transport/factory/msg-transport.js.map +1 -1
- package/dist/transport/session/a-session-msg-factory.d.ts +1 -0
- package/dist/transport/session/a-session-msg-factory.js +4 -0
- package/dist/transport/session/a-session-msg-factory.js.map +1 -1
- package/dist/transport/session/clone-session-description.d.ts +6 -0
- package/dist/transport/session/clone-session-description.js +11 -0
- package/dist/transport/session/clone-session-description.js.map +1 -0
- package/dist/transport/session/fix-clock.d.ts +1 -1
- package/dist/transport/session/fix-clock.js.map +1 -1
- package/dist/transport/session/fix-session.d.ts +5 -1
- package/dist/transport/session/fix-session.js +93 -24
- package/dist/transport/session/fix-session.js.map +1 -1
- package/dist/transport/session/index.d.ts +2 -0
- package/dist/transport/session/index.js +2 -0
- package/dist/transport/session/index.js.map +1 -1
- package/dist/transport/session/resend-request-manager.d.ts +2 -2
- package/dist/transport/session/resend-request-manager.js.map +1 -1
- package/dist/transport/session/session-msg-factory.d.ts +1 -0
- package/dist/transport/session/session-msg-factory.js.map +1 -1
- package/dist/transport/session/session-registry.d.ts +23 -0
- package/dist/transport/session/session-registry.js +59 -0
- package/dist/transport/session/session-registry.js.map +1 -0
- package/dist/transport/session/session-sequence-coordinator.d.ts +1 -1
- package/dist/transport/session/session-sequence-coordinator.js.map +1 -1
- package/dist/transport/session/session-sequence-store.d.ts +3 -3
- package/dist/transport/session/session-sequence-store.js.map +1 -1
- package/dist/transport/tcp/tcp-acceptor-listener.d.ts +3 -0
- package/dist/transport/tcp/tcp-acceptor-listener.js +26 -4
- package/dist/transport/tcp/tcp-acceptor-listener.js.map +1 -1
- package/dist/transport/tcp/tcp-acceptor.d.ts +5 -0
- package/dist/transport/tcp/tcp-acceptor.js +74 -20
- package/dist/transport/tcp/tcp-acceptor.js.map +1 -1
- package/dist/transport/tcp/tcp-initiator.js +15 -5
- package/dist/transport/tcp/tcp-initiator.js.map +1 -1
- package/dist/transport/tcp/tcp-transport-description.d.ts +1 -0
- package/dist/transport/tcp/tcp-transport-description.js.map +1 -1
- package/dist/transport/tcp/tls-diagnostics.d.ts +5 -0
- package/dist/transport/tcp/tls-diagnostics.js +64 -0
- package/dist/transport/tcp/tls-diagnostics.js.map +1 -0
- package/dist/transport/tcp/tls-options-factory.d.ts +1 -0
- package/dist/transport/tcp/tls-options-factory.js +24 -2
- package/dist/transport/tcp/tls-options-factory.js.map +1 -1
- package/docs/acceptor.md +207 -0
- package/docs/benchmarking.md +117 -0
- package/eslint.config.mjs +15 -1
- package/jsfix.test_client.txt +62 -75
- package/jsfix.test_server.txt +72 -72
- package/package.json +19 -10
- package/script/pack-local.js +68 -0
- package/src/benchmark/bench-report.ts +101 -0
- package/src/benchmark/bench-runner.ts +196 -0
- package/src/benchmark/bench-stats.ts +86 -0
- package/src/benchmark/bench-types.ts +110 -0
- package/src/benchmark/gc-probe.ts +145 -0
- package/src/benchmark/index.ts +6 -0
- package/src/benchmark/parse-bench.ts +144 -0
- package/src/buffer/ascii/ascii-parser.ts +16 -1
- package/src/buffer/ascii/ascii-segment-parser.ts +9 -6
- package/src/buffer/ascii/tag-index.ts +3 -3
- package/src/buffer/msg-view.ts +35 -18
- package/src/config/js-fix-config.ts +8 -0
- package/src/dictionary/parser/quickfix/dictionary-validator.ts +1 -1
- package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +2 -3
- package/src/dictionary/parser/quickfix/validation-error.ts +1 -1
- package/src/jsfix-bench.ts +204 -0
- package/src/runtime/session-launcher.ts +5 -1
- package/src/runtime/session-scope.ts +84 -0
- package/src/sample/http/oms/app.ts +3 -3
- package/src/sample/tcp/qf-md/app.ts +3 -3
- package/src/sample/tcp/skeleton/app.ts +1 -1
- package/src/sample/tcp/trade-capture/app-launcher.ts +3 -3
- package/src/store/file-session-store.ts +2 -2
- package/src/store/file-session-stream-provider.ts +17 -11
- package/src/store/fix-session-store-factory.ts +1 -1
- package/src/store/fix-session-store.ts +11 -11
- package/src/store/memory-session-store.ts +1 -1
- package/src/store/session-stream-provider.ts +13 -13
- package/src/transport/ascii/ascii-session.ts +186 -11
- package/src/transport/duplex/fix-duplex.ts +9 -0
- package/src/transport/duplex/tcp-duplex.ts +10 -0
- package/src/transport/factory/msg-transport.ts +20 -0
- package/src/transport/session/a-session-msg-factory.ts +18 -0
- package/src/transport/session/clone-session-description.ts +41 -0
- package/src/transport/session/fix-clock.ts +1 -1
- package/src/transport/session/fix-session.ts +136 -25
- package/src/transport/session/index.ts +2 -0
- package/src/transport/session/resend-request-manager.ts +4 -4
- package/src/transport/session/session-msg-factory.ts +13 -0
- package/src/transport/session/session-registry.ts +112 -0
- package/src/transport/session/session-sequence-coordinator.ts +1 -1
- package/src/transport/session/session-sequence-store.ts +3 -3
- package/src/transport/tcp/tcp-acceptor-listener.ts +43 -5
- package/src/transport/tcp/tcp-acceptor.ts +92 -18
- package/src/transport/tcp/tcp-initiator.ts +26 -5
- package/src/transport/tcp/tcp-transport-description.ts +7 -0
- package/src/transport/tcp/tls-diagnostics.ts +76 -0
- package/src/transport/tcp/tls-options-factory.ts +34 -2
|
@@ -5,8 +5,10 @@ import { IJsFixConfig, IJsFixLogger } from '../../config'
|
|
|
5
5
|
import { createServer as netCreateServer, Server, Socket } from 'net'
|
|
6
6
|
import { createServer as tlsCreateServer, TlsOptions, TLSSocket } from 'tls'
|
|
7
7
|
import { TlsOptionsFactory } from './tls-options-factory'
|
|
8
|
+
import { describeNegotiated, describePeerCertificate, describeServerOptions } from './tls-diagnostics'
|
|
8
9
|
import { inject, injectable } from 'tsyringe'
|
|
9
10
|
import { DITokens } from '../../runtime/di-tokens'
|
|
11
|
+
import { makeSessionScope } from '../../runtime/session-scope'
|
|
10
12
|
|
|
11
13
|
@injectable()
|
|
12
14
|
export class TcpAcceptor extends FixAcceptor {
|
|
@@ -40,19 +42,29 @@ export class TcpAcceptor extends FixAcceptor {
|
|
|
40
42
|
const tlsOptions: TlsOptions | null = tcp?.tls ? TlsOptionsFactory.getTlsOptions(tcp.tls) : null
|
|
41
43
|
this.logger.info('create tls server')
|
|
42
44
|
if (!tlsOptions) return
|
|
45
|
+
this.logger.info(`tls server options ${describeServerOptions(tlsOptions)}`)
|
|
43
46
|
this.server = tlsCreateServer(tlsOptions, (tlsSocket: TLSSocket) => {
|
|
44
47
|
if (tcp?.tls?.enableTrace) {
|
|
45
48
|
this.logger.info('enabling tls session trace')
|
|
46
49
|
tlsSocket.enableTrace()
|
|
47
50
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.logger.
|
|
51
|
+
this.logger.info(`tls negotiated ${describeNegotiated(tlsSocket)}`)
|
|
52
|
+
this.logger.info(`tls peer certificate ${describePeerCertificate(tlsSocket)}`)
|
|
53
|
+
// mirrors the initiator fix for #94 - an unauthorized peer is only fatal when the
|
|
54
|
+
// caller has not explicitly opted out. the default stays secure: rejectUnauthorized
|
|
55
|
+
// undefined means reject, exactly as before.
|
|
56
|
+
if (!tlsSocket.authorized && tlsOptions.rejectUnauthorized !== false) {
|
|
57
|
+
this.logger.warning(`no transport created, unauthorized tls connection: ${String(tlsSocket.authorizationError)}`)
|
|
58
|
+
tlsSocket.end()
|
|
59
|
+
return
|
|
55
60
|
}
|
|
61
|
+
if (!tlsSocket.authorized) {
|
|
62
|
+
this.logger.warning(`accepting unauthorized tls peer (rejectUnauthorized: false): ${String(tlsSocket.authorizationError)}`)
|
|
63
|
+
}
|
|
64
|
+
tlsSocket.setEncoding('utf8')
|
|
65
|
+
const id: number = this.getId()
|
|
66
|
+
this.logger.info(`tls creates session ${id} authorized ${tlsSocket.authorized}`)
|
|
67
|
+
this.onSocket(id, tlsSocket, config)
|
|
56
68
|
})
|
|
57
69
|
} catch (e) {
|
|
58
70
|
this.logger.error(e as Error)
|
|
@@ -82,17 +94,50 @@ export class TcpAcceptor extends FixAcceptor {
|
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
private onSocket (id: number, socket: Socket, config: IJsFixConfig): void {
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
// Every accepted connection gets its own DI scope: its own parse and transmit
|
|
98
|
+
// buffers, its own session description (hence its own SessionId and message
|
|
99
|
+
// store) and its own session message factory. Without this, concurrent clients
|
|
100
|
+
// on one listener share buffers and collide on a single store.
|
|
101
|
+
const sessionConfig = makeSessionScope(config)
|
|
102
|
+
const remote = `${socket.remoteAddress ?? '?'}:${socket.remotePort ?? 0}`
|
|
103
|
+
this.applyKeepAlive(id, socket)
|
|
104
|
+
this.logger.info(`transport ${id} from ${remote} given its own session scope`)
|
|
105
|
+
const transport: MsgTransport = new MsgTransport(id, sessionConfig, new TcpDuplex(socket))
|
|
106
|
+
this.saveTransport(id, transport, remote)
|
|
87
107
|
transport.receiver.on('end', () => {
|
|
88
|
-
this.harvestTransport(id)
|
|
108
|
+
this.harvestTransport(id, 'peer ended the connection')
|
|
89
109
|
})
|
|
90
110
|
transport.receiver.on('error', (e: Error) => {
|
|
91
111
|
this.logger.error(e)
|
|
92
|
-
this.harvestTransport(id)
|
|
112
|
+
this.harvestTransport(id, `receiver error: ${e.message}`)
|
|
113
|
+
})
|
|
114
|
+
// 'end' only fires on a clean FIN. A socket dropped by a firewall, or one this
|
|
115
|
+
// side destroyed after a failed graceful close, arrives here instead - without
|
|
116
|
+
// it the transport would sit in the map forever.
|
|
117
|
+
socket.on('close', (hadError: boolean) => {
|
|
118
|
+
this.harvestTransport(id, hadError ? 'socket closed after error' : 'socket closed')
|
|
93
119
|
})
|
|
94
120
|
}
|
|
95
121
|
|
|
122
|
+
/**
|
|
123
|
+
* TCP keep-alive is the only mechanism that detects a peer which has vanished
|
|
124
|
+
* without sending FIN or RST - the half open socket described in issue #153. The
|
|
125
|
+
* session level TestRequest timeout is the protocol level backstop; this is the
|
|
126
|
+
* transport level one.
|
|
127
|
+
*/
|
|
128
|
+
private applyKeepAlive (id: number, socket: Socket): void {
|
|
129
|
+
const configured = this.config.description.application?.tcp?.keepAliveMs
|
|
130
|
+
const keepAliveMs = configured ?? TcpAcceptor.defaultKeepAliveMs
|
|
131
|
+
if (keepAliveMs <= 0) {
|
|
132
|
+
this.logger.info(`transport ${id} keep-alive disabled`)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
socket.setKeepAlive(true, keepAliveMs)
|
|
136
|
+
this.logger.info(`transport ${id} keep-alive enabled, initial delay ${keepAliveMs}ms`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public static defaultKeepAliveMs: number = 30000
|
|
140
|
+
|
|
96
141
|
public listen (): void {
|
|
97
142
|
const port = this.config.description.application?.tcp?.port
|
|
98
143
|
this.logger.info(`start to listen ${port}`)
|
|
@@ -107,20 +152,49 @@ export class TcpAcceptor extends FixAcceptor {
|
|
|
107
152
|
|
|
108
153
|
public close (callback?: (err?: Error) => void): void {
|
|
109
154
|
const port = this.config.description.application?.tcp?.port ?? -1
|
|
110
|
-
this.logger.info(`close listener on port ${port}`)
|
|
155
|
+
this.logger.info(`close listener on port ${port} with ${this.transportCount} live transport(s)`)
|
|
156
|
+
// Server.close only stops accepting - it waits for existing connections to go.
|
|
157
|
+
// Release them, otherwise a shutdown blocks on whichever counterparty happens to
|
|
158
|
+
// be connected (and forever on one that is half open).
|
|
159
|
+
Object.keys(this.transports).forEach(id => {
|
|
160
|
+
const tid = Number(id)
|
|
161
|
+
this.logger.info(`close: releasing transport ${tid}`)
|
|
162
|
+
this.transports[tid]?.end()
|
|
163
|
+
})
|
|
111
164
|
this.server.close(callback)
|
|
112
165
|
}
|
|
113
166
|
|
|
114
|
-
|
|
167
|
+
/** how many connections this listener currently holds */
|
|
168
|
+
public get transportCount (): number {
|
|
169
|
+
return Object.keys(this.transports).length
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** ids of the live transports, for a periodic census in the application log */
|
|
173
|
+
public liveTransports (): string[] {
|
|
174
|
+
return Object.keys(this.transports).map(id => {
|
|
175
|
+
const meta = this.accepted[id as any]
|
|
176
|
+
const age = meta ? Math.round((Date.now() - meta.at) / 1000) : -1
|
|
177
|
+
return `${id}@${meta?.remote ?? '?'} up ${age}s`
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private readonly accepted: Record<number, { remote: string, at: number }> = {}
|
|
182
|
+
|
|
183
|
+
private saveTransport (tid: number, transport: MsgTransport, remote: string): void {
|
|
115
184
|
this.transports[tid] = transport
|
|
116
|
-
|
|
117
|
-
this.logger.info(`new transport id = ${tid} created total transports = ${
|
|
185
|
+
this.accepted[tid] = { remote, at: Date.now() }
|
|
186
|
+
this.logger.info(`new transport id = ${tid} from ${remote} created total transports = ${this.transportCount}`)
|
|
118
187
|
this.emit('transport', transport)
|
|
119
188
|
}
|
|
120
189
|
|
|
121
|
-
private harvestTransport (tid: number): void {
|
|
190
|
+
private harvestTransport (tid: number, reason: string): void {
|
|
191
|
+
if (!this.transports[tid]) return
|
|
192
|
+
const meta = this.accepted[tid]
|
|
193
|
+
const lifetime = meta ? Math.round((Date.now() - meta.at) / 1000) : -1
|
|
122
194
|
delete this.transports[tid]
|
|
123
|
-
|
|
124
|
-
this.logger.info(`transport ${tid}
|
|
195
|
+
delete this.accepted[tid]
|
|
196
|
+
this.logger.info(`transport ${tid} from ${meta?.remote ?? '?'} harvested after ${lifetime}s ` +
|
|
197
|
+
`(${reason}) total transports = ${this.transportCount}`)
|
|
198
|
+
this.emit('harvested', tid, reason)
|
|
125
199
|
}
|
|
126
200
|
}
|
|
@@ -9,6 +9,7 @@ import { connect as tlsConnect, ConnectionOptions, TLSSocket } from 'tls'
|
|
|
9
9
|
import { createConnection } from 'net'
|
|
10
10
|
import Timeout = NodeJS.Timeout
|
|
11
11
|
import { TlsOptionsFactory } from './tls-options-factory'
|
|
12
|
+
import { describeConnectionOptions, describeNegotiated, describePeerCertificate } from './tls-diagnostics'
|
|
12
13
|
import { inject, injectable } from 'tsyringe'
|
|
13
14
|
import { DITokens } from '../../runtime/di-tokens'
|
|
14
15
|
import { ITcpTransportDescription } from './tcp-transport-description'
|
|
@@ -67,8 +68,13 @@ export class TcpInitiator extends FixInitiator {
|
|
|
67
68
|
this.logger.info(`connecting with timeout ${timeoutSeconds}`)
|
|
68
69
|
this.tryConnect()
|
|
69
70
|
.then((t: MsgTransport) => { resolve(t) })
|
|
70
|
-
.catch((
|
|
71
|
-
|
|
71
|
+
.catch((first: Error) => {
|
|
72
|
+
// carry the first failure into repeatConnect. it used to be discarded, so a
|
|
73
|
+
// connect() whose timeout expires before the first retry interval fires - the
|
|
74
|
+
// default case, timeout and reconnectSeconds are both 5 - reported a generic
|
|
75
|
+
// "timeout whilst connecting" and threw away the reason. see #94.
|
|
76
|
+
this.logger.info(`first connect attempt failed: ${first.message}`)
|
|
77
|
+
this.repeatConnect(timeoutSeconds, first)
|
|
72
78
|
.then((t: MsgTransport) => { resolve(t) })
|
|
73
79
|
.catch((e: Error) => { reject(e) })
|
|
74
80
|
})
|
|
@@ -120,15 +126,26 @@ export class TcpInitiator extends FixInitiator {
|
|
|
120
126
|
const connectionOptions: ConnectionOptions | null = tcp ? TlsOptionsFactory.getTlsConnectionOptions(tcp) : null
|
|
121
127
|
if (connectionOptions) {
|
|
122
128
|
try {
|
|
129
|
+
this.logger.info(`tls connect with ${describeConnectionOptions(connectionOptions)}`)
|
|
123
130
|
tlsSocket = tlsConnect(connectionOptions, () => {
|
|
124
131
|
if (!tlsSocket) return null
|
|
125
132
|
this.logger.info(`client connected ${tlsSocket.authorized ? 'authorized' : 'unauthorized'}`)
|
|
126
|
-
|
|
133
|
+
this.logger.info(`tls negotiated ${describeNegotiated(tlsSocket)}`)
|
|
134
|
+
this.logger.info(`tls peer certificate ${describePeerCertificate(tlsSocket)}`)
|
|
135
|
+
// https://github.com/TimelordUK/jspurefix/issues/94 - honour rejectUnauthorized:
|
|
136
|
+
// false rather than always rejecting on an unauthorized cert. Node's tls.connect()
|
|
137
|
+
// never throws for an unauthorized peer by itself; it's the caller's job to decide
|
|
138
|
+
// what to do with tlsSocket.authorized, and rejectUnauthorized is how a caller opts
|
|
139
|
+
// out of that check - so only reject here when the caller didn't opt out.
|
|
140
|
+
if (!tlsSocket.authorized && connectionOptions.rejectUnauthorized !== false) {
|
|
127
141
|
const error = tlsSocket.authorizationError
|
|
128
142
|
this.logger.warning(`rejecting from state ${this.state} authorizationError ${error}`)
|
|
129
143
|
tlsSocket.end()
|
|
130
144
|
reject(error)
|
|
131
145
|
} else {
|
|
146
|
+
if (!tlsSocket.authorized) {
|
|
147
|
+
this.logger.warning(`proceeding despite unauthorized TLS peer (rejectUnauthorized: false): ${tlsSocket.authorizationError}`)
|
|
148
|
+
}
|
|
132
149
|
tlsSocket.setEncoding('utf8')
|
|
133
150
|
const tlsDuplex = new TcpDuplex(tlsSocket)
|
|
134
151
|
if (tcp?.tls?.enableTrace) {
|
|
@@ -140,6 +157,9 @@ export class TcpInitiator extends FixInitiator {
|
|
|
140
157
|
}
|
|
141
158
|
})
|
|
142
159
|
tlsSocket.on('error', (err) => {
|
|
160
|
+
// previously silent - the socket error is the real reason a tls session failed,
|
|
161
|
+
// and without it the caller only ever sees repeatConnect's generic timeout.
|
|
162
|
+
this.logger.warning(`tls socket error from state ${this.state}: ${err.message}`)
|
|
143
163
|
reject(err)
|
|
144
164
|
})
|
|
145
165
|
} catch (e) {
|
|
@@ -171,14 +191,14 @@ export class TcpInitiator extends FixInitiator {
|
|
|
171
191
|
}
|
|
172
192
|
}
|
|
173
193
|
|
|
174
|
-
private async repeatConnect (timeoutSeconds: number): Promise < MsgTransport > {
|
|
194
|
+
private async repeatConnect (timeoutSeconds: number, initialError?: Error): Promise < MsgTransport > {
|
|
175
195
|
return await new Promise<MsgTransport>(async (resolve, reject) => {
|
|
176
196
|
const application = this.application
|
|
177
197
|
const promisify = util.promisify
|
|
178
198
|
const timeoutPromise = promisify(setTimeout)
|
|
179
199
|
const reconnectSeconds = application?.reconnectSeconds ?? 5
|
|
180
200
|
let retries = 0
|
|
181
|
-
let lastError: Error
|
|
201
|
+
let lastError: Error | undefined = initialError
|
|
182
202
|
const name = application?.name ?? 'initiator'
|
|
183
203
|
this.th = setInterval(() => {
|
|
184
204
|
++retries
|
|
@@ -196,6 +216,7 @@ export class TcpInitiator extends FixInitiator {
|
|
|
196
216
|
this.clearTimer()
|
|
197
217
|
this.state = InitiatorState.Stopped
|
|
198
218
|
const e = lastError ?? new Error(`${name}: timeout of ${timeoutSeconds} whilst connecting`)
|
|
219
|
+
this.logger.warning(`${name}: giving up after ${timeoutSeconds}s and ${retries} retries, last error: ${e.message}`)
|
|
199
220
|
reject(e)
|
|
200
221
|
}).catch(e => {
|
|
201
222
|
reject(e)
|
|
@@ -4,4 +4,11 @@ export interface ITcpTransportDescription {
|
|
|
4
4
|
readonly port: number
|
|
5
5
|
readonly host: string
|
|
6
6
|
readonly tls?: ITlsOptions
|
|
7
|
+
/**
|
|
8
|
+
* TCP keep-alive probe delay in milliseconds for connections accepted by this
|
|
9
|
+
* listener. Keep-alive is what lets the OS notice a peer that has vanished
|
|
10
|
+
* without FIN or RST - the half open socket in issue #153. Set 0 to disable.
|
|
11
|
+
* Default 30000.
|
|
12
|
+
*/
|
|
13
|
+
readonly keepAliveMs?: number
|
|
7
14
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ConnectionOptions, TLSSocket, TlsOptions } from 'tls'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
tls failures are routinely reported as "it just times out" - see issues #94 and #151. the
|
|
5
|
+
session either connects or it does not, and nothing in the log says which options were
|
|
6
|
+
actually applied or what the peer presented. enableTrace exists but is node's raw handshake
|
|
7
|
+
trace: all or nothing, written to stderr rather than through IJsFixLogger.
|
|
8
|
+
|
|
9
|
+
these helpers render the small number of facts needed to diagnose a handshake, and are
|
|
10
|
+
deliberately conservative about what they include - never key material, never a full
|
|
11
|
+
certificate, only the identifying fields of one.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function describeCommonOptions (options: ConnectionOptions | TlsOptions): string[] {
|
|
15
|
+
const parts: string[] = []
|
|
16
|
+
// presence only - the values are private keys and certificate bodies.
|
|
17
|
+
parts.push(`key ${options.key ? 'supplied' : 'none'}`)
|
|
18
|
+
parts.push(`cert ${options.cert ? 'supplied' : 'none'}`)
|
|
19
|
+
const ca = options.ca
|
|
20
|
+
const caCount = Array.isArray(ca) ? ca.length : (ca ? 1 : 0)
|
|
21
|
+
parts.push(`ca ${caCount}`)
|
|
22
|
+
// the whole point of #94/#151 - make it unambiguous which value won.
|
|
23
|
+
parts.push(`rejectUnauthorized ${String(options.rejectUnauthorized ?? '(default true)')}`)
|
|
24
|
+
parts.push(`requestCert ${String(options.requestCert ?? '(default false)')}`)
|
|
25
|
+
if (options.minVersion) parts.push(`minVersion ${options.minVersion}`)
|
|
26
|
+
if (options.maxVersion) parts.push(`maxVersion ${options.maxVersion}`)
|
|
27
|
+
return parts
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function describeConnectionOptions (options: ConnectionOptions): string {
|
|
31
|
+
const parts: string[] = [`host ${options.host ?? '?'}`, `port ${options.port ?? '?'}`]
|
|
32
|
+
if (options.servername) parts.push(`servername ${options.servername}`)
|
|
33
|
+
return parts.concat(describeCommonOptions(options)).join(', ')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function describeServerOptions (options: TlsOptions): string {
|
|
37
|
+
return describeCommonOptions(options).join(', ')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
what the handshake actually negotiated. the maintainer's suggestion in #94 was to reproduce
|
|
42
|
+
the connection in powershell to see the negotiated suite - this puts the same facts in our
|
|
43
|
+
own log instead.
|
|
44
|
+
*/
|
|
45
|
+
export function describeNegotiated (socket: TLSSocket): string {
|
|
46
|
+
const parts: string[] = []
|
|
47
|
+
parts.push(`protocol ${socket.getProtocol() ?? 'none'}`)
|
|
48
|
+
const cipher = socket.getCipher()
|
|
49
|
+
parts.push(`cipher ${cipher ? `${cipher.name} (${cipher.version})` : 'none'}`)
|
|
50
|
+
parts.push(`authorized ${socket.authorized}`)
|
|
51
|
+
if (!socket.authorized && socket.authorizationError) {
|
|
52
|
+
parts.push(`authorizationError ${String(socket.authorizationError)}`)
|
|
53
|
+
}
|
|
54
|
+
return parts.join(', ')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
identifying fields of the peer certificate only. getPeerCertificate() returns the full DER
|
|
59
|
+
in .raw plus the public key - none of that belongs in a log, so pick fields explicitly.
|
|
60
|
+
*/
|
|
61
|
+
export function describePeerCertificate (socket: TLSSocket): string {
|
|
62
|
+
const cert = socket.getPeerCertificate(false)
|
|
63
|
+
if (!cert || Object.keys(cert).length === 0) {
|
|
64
|
+
return 'peer presented no certificate'
|
|
65
|
+
}
|
|
66
|
+
const subject = cert.subject ? (cert.subject.CN ?? JSON.stringify(cert.subject)) : '?'
|
|
67
|
+
const issuer = cert.issuer ? (cert.issuer.CN ?? JSON.stringify(cert.issuer)) : '?'
|
|
68
|
+
const parts: string[] = [
|
|
69
|
+
`subject ${subject}`,
|
|
70
|
+
`issuer ${issuer}`,
|
|
71
|
+
`valid ${cert.valid_from ?? '?'} to ${cert.valid_to ?? '?'}`
|
|
72
|
+
]
|
|
73
|
+
if (cert.subjectaltname) parts.push(`san ${cert.subjectaltname}`)
|
|
74
|
+
if (cert.fingerprint256) parts.push(`sha256 ${cert.fingerprint256}`)
|
|
75
|
+
return parts.join(', ')
|
|
76
|
+
}
|
|
@@ -5,6 +5,22 @@ const path = require('path')
|
|
|
5
5
|
const fs = require('fs')
|
|
6
6
|
|
|
7
7
|
export class TlsOptionsFactory {
|
|
8
|
+
/*
|
|
9
|
+
session descriptions are routinely hand written json, where a boolean flag is easily
|
|
10
|
+
quoted by mistake i.e. "rejectUnauthorized": "false". node treats that string as truthy
|
|
11
|
+
and so does exactly the opposite of what was intended, silently. accept the two string
|
|
12
|
+
spellings rather than inverting the caller's meaning - anything else is left alone so it
|
|
13
|
+
shows up as an obviously wrong value rather than being guessed at.
|
|
14
|
+
*/
|
|
15
|
+
static asBoolean (value: any): any {
|
|
16
|
+
if (typeof value === 'string') {
|
|
17
|
+
const lowered = value.toLowerCase()
|
|
18
|
+
if (lowered === 'true') return true
|
|
19
|
+
if (lowered === 'false') return false
|
|
20
|
+
}
|
|
21
|
+
return value
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
static read (filePath: string): string {
|
|
9
25
|
const root: string = path.join(__dirname, '../../../')
|
|
10
26
|
const fullPath = path.join(root, filePath)
|
|
@@ -18,8 +34,8 @@ export class TlsOptionsFactory {
|
|
|
18
34
|
let tlsOptions: TlsOptions | null = null
|
|
19
35
|
if (tls) {
|
|
20
36
|
tlsOptions = {
|
|
21
|
-
requestCert: tls.requestCert,
|
|
22
|
-
rejectUnauthorized: tls.rejectUnauthorized
|
|
37
|
+
requestCert: TlsOptionsFactory.asBoolean(tls.requestCert),
|
|
38
|
+
rejectUnauthorized: TlsOptionsFactory.asBoolean(tls.rejectUnauthorized)
|
|
23
39
|
} as TlsOptions
|
|
24
40
|
|
|
25
41
|
if (tls.key) {
|
|
@@ -62,11 +78,27 @@ export class TlsOptionsFactory {
|
|
|
62
78
|
if (tcp.tls.sessionTimeout) {
|
|
63
79
|
connectionOptions.sessionTimeout = tcp.tls.sessionTimeout
|
|
64
80
|
}
|
|
81
|
+
// https://github.com/TimelordUK/jspurefix/issues/151 - ITlsOptions declares these and
|
|
82
|
+
// getTlsOptions honours them on the server path, so map them here too rather than making
|
|
83
|
+
// nodeTlsConnectionOptions the only spelling that works for an initiator. Applied before
|
|
84
|
+
// the spread below so an explicit nodeTlsConnectionOptions value still wins.
|
|
85
|
+
if (tls.requestCert !== undefined) {
|
|
86
|
+
connectionOptions.requestCert = TlsOptionsFactory.asBoolean(tls.requestCert)
|
|
87
|
+
}
|
|
88
|
+
if (tls.rejectUnauthorized !== undefined) {
|
|
89
|
+
connectionOptions.rejectUnauthorized = TlsOptionsFactory.asBoolean(tls.rejectUnauthorized)
|
|
90
|
+
}
|
|
65
91
|
if (tls.nodeTlsConnectionOptions) {
|
|
66
92
|
connectionOptions = {
|
|
67
93
|
...connectionOptions,
|
|
68
94
|
...tls.nodeTlsConnectionOptions
|
|
69
95
|
}
|
|
96
|
+
if (connectionOptions.rejectUnauthorized !== undefined) {
|
|
97
|
+
connectionOptions.rejectUnauthorized = TlsOptionsFactory.asBoolean(connectionOptions.rejectUnauthorized)
|
|
98
|
+
}
|
|
99
|
+
if (connectionOptions.requestCert !== undefined) {
|
|
100
|
+
connectionOptions.requestCert = TlsOptionsFactory.asBoolean(connectionOptions.requestCert)
|
|
101
|
+
}
|
|
70
102
|
}
|
|
71
103
|
}
|
|
72
104
|
return connectionOptions
|