UNPKG

jspurefix

Version:
206 lines (191 loc) 7.02 kB
import { FixInitiator } from '../fix-initiator' import { MsgTransport } from '../factory' import { IJsFixConfig, IJsFixLogger } from '../../config' import { TcpDuplex, FixDuplex } from '../duplex' import * as util from 'util' import { connect as tlsConnect, ConnectionOptions, TLSSocket } from 'tls' import { createConnection } from 'net' import Timeout = NodeJS.Timeout import { TlsOptionsFactory } from './tls-options-factory' import { inject, injectable } from 'tsyringe' import { DITokens } from '../../runtime/di-tokens' import { ITcpTransportDescription } from './tcp-transport-description' export enum InitiatorState { Idle = 1, Connecting = 2, Connected = 3, Stopped = 4 } @injectable() export class TcpInitiator extends FixInitiator { public tcp: ITcpTransportDescription | null public state: InitiatorState = InitiatorState.Idle private readonly logger: IJsFixLogger private duplex: FixDuplex private th: Timeout | null = null constructor (@inject(DITokens.IJsFixConfig) public readonly jsFixConfig: IJsFixConfig) { super(jsFixConfig.description.application ?? null) const name = this.application?.name ?? 'initiator' this.logger = jsFixConfig.logFactory.logger(`${name}:TcpInitiator`) if (!this.application) { throw new Error('no application in session description.') } this.tcp = this.application.tcp ?? null if (!this.tcp) { throw new Error('no tcp in session description need tcp { host: hostname, port: port }') } } public end (): void { this.clearTimer() switch (this.state) { case InitiatorState.Connected: { this.logger.info('end') this.duplex.end() this.state = InitiatorState.Stopped break } default: { this.logger.info(`end :state ${this.state}`) this.state = InitiatorState.Stopped break } } } public async connect (timeoutSeconds: number): Promise<MsgTransport> { return await new Promise<MsgTransport>(async (resolve, reject) => { switch (this.state) { case InitiatorState.Idle: { this.state = InitiatorState.Connecting this.logger.info(`connecting with timeout ${timeoutSeconds}`) this.tryConnect() .then((t: MsgTransport) => { resolve(t) }) .catch((_: Error) => { this.repeatConnect(timeoutSeconds) .then((t: MsgTransport) => { resolve(t) }) .catch((e: Error) => { reject(e) }) }) break } default: { const e: Error = new Error('connect not valid from non idle state') this.logger.warning(`rejecting from state ${this.state}`) reject(e) } } }) } private async unsecureDuplex (): Promise<TcpDuplex> { const tcp = this.tcp return await new Promise<TcpDuplex>((resolve, reject) => { try { this.logger.info('unsecureDuplex try to connect to endPoint') const socket = tcp ? createConnection(tcp, () => { try { this.logger.info('net.createConnection cb, resolving') if (socket) { const tcpDuplex = new TcpDuplex(socket) resolve(tcpDuplex) } else { reject(new Error('no socket in tcp initiator')) } } catch (e) { reject(e) } }) : null socket?.on('error', (err) => { reject(err) }) } catch (e) { reject(e) } }) } private async tlsDuplex (): Promise<TcpDuplex > { return await new Promise<TcpDuplex>((resolve, reject) => { let tlsSocket: TLSSocket | null = null const tcp = this.tcp const connectionOptions: ConnectionOptions | null = tcp ? TlsOptionsFactory.getTlsConnectionOptions(tcp) : null if (connectionOptions) { try { tlsSocket = tlsConnect(connectionOptions, () => { if (!tlsSocket) return null this.logger.info(`client connected ${tlsSocket.authorized ? 'authorized' : 'unauthorized'}`) if (!tlsSocket.authorized) { const error = tlsSocket.authorizationError this.logger.warning(`rejecting from state ${this.state} authorizationError ${error}`) tlsSocket.end() reject(error) } else { tlsSocket.setEncoding('utf8') const tlsDuplex = new TcpDuplex(tlsSocket) if (tcp?.tls?.enableTrace) { this.logger.info('enabling tls session trace') tlsSocket.enableTrace() } this.logger.info('tlsDuplex resolving') resolve(tlsDuplex) } }) tlsSocket.on('error', (err) => { reject(err) }) } catch (e) { reject(e) } } }) } private async tryConnect (): Promise < MsgTransport > { return await new Promise<MsgTransport>((resolve, reject) => { const tcp = this.tcp const connectionOptions: ConnectionOptions | null = tcp ? TlsOptionsFactory.getTlsConnectionOptions(tcp) : null const connector = connectionOptions ? this.tlsDuplex() : this.unsecureDuplex() this.logger.info(`tryConnect ${tcp?.host}:${tcp?.port}`) connector.then(duplex => { this.duplex = duplex resolve(new MsgTransport(0, this.jsFixConfig, duplex)) }).catch(e => { reject(e) }) }) } public clearTimer (): void { if (this.th) { clearInterval(this.th) this.th = null } } private async repeatConnect (timeoutSeconds: number): Promise < MsgTransport > { return await new Promise<MsgTransport>(async (resolve, reject) => { const application = this.application const promisify = util.promisify const timeoutPromise = promisify(setTimeout) const reconnectSeconds = application?.reconnectSeconds ?? 5 let retries = 0 let lastError: Error const name = application?.name ?? 'initiator' this.th = setInterval(() => { ++retries this.tryConnect() .then((t: MsgTransport) => { this.state = InitiatorState.Connected this.clearTimer() resolve(t) }).catch((e: Error) => { lastError = e this.logger.info(`${name}: retries ${retries} ${e.message}`) }) }, reconnectSeconds * 1000) timeoutPromise(timeoutSeconds * 1000).then(() => { this.clearTimer() this.state = InitiatorState.Stopped const e = lastError ?? new Error(`${name}: timeout of ${timeoutSeconds} whilst connecting`) reject(e) }).catch(e => { reject(e) }) }) } }