diff --git a/initiator.go b/initiator.go index 9af90cb2b..61decf689 100644 --- a/initiator.go +++ b/initiator.go @@ -19,11 +19,8 @@ import ( "bufio" "context" "crypto/tls" - "strings" "sync" "time" - - "golang.org/x/net/proxy" ) // Initiator initiates connections and processes messages for all sessions. @@ -44,21 +41,10 @@ type Initiator struct { func (i *Initiator) Start() (err error) { i.stopChan = make(chan interface{}) - for sessionID, settings := range i.sessionSettings { - // TODO: move into session factory. - var tlsConfig *tls.Config - if tlsConfig, err = loadTLSConfig(settings); err != nil { - return - } - - var dialer proxy.ContextDialer - if dialer, err = loadDialerConfig(settings); err != nil { - return - } - + for sessionID := range i.sessionSettings { i.wg.Add(1) go func(sessID SessionID) { - i.handleConnection(i.sessions[sessID], tlsConfig, dialer) + i.handleConnection(i.sessions[sessID]) i.wg.Done() }(sessionID) } @@ -143,7 +129,7 @@ func (i *Initiator) waitForReconnectInterval(reconnectInterval time.Duration) bo return true } -func (i *Initiator) handleConnection(session *session, tlsConfig *tls.Config, dialer proxy.ContextDialer) { +func (i *Initiator) handleConnection(session *session) { var wg sync.WaitGroup wg.Add(1) go func() { @@ -183,20 +169,14 @@ func (i *Initiator) handleConnection(session *session, tlsConfig *tls.Config, di address := session.SocketConnectAddress[connectionAttempt%len(session.SocketConnectAddress)] session.log.OnEventf("Connecting to: %v", address) - netConn, err := dialer.DialContext(ctx, "tcp", address) + netConn, err := session.dialer.DialContext(ctx, "tcp", address) if err != nil { session.log.OnEventf("Failed to connect: %v", err) goto reconnect - } else if tlsConfig != nil { - // Unless InsecureSkipVerify is true, server name config is required for TLS - // to verify the received certificate - if !tlsConfig.InsecureSkipVerify && len(tlsConfig.ServerName) == 0 { - serverName := address - if c := strings.LastIndex(serverName, ":"); c > 0 { - serverName = serverName[:c] - } - tlsConfig.ServerName = serverName - } + } else if session.tlsConfig != nil { + // Derive ServerName from the current address without mutating the shared + // session TLS config, so multi-address reconnects keep the correct name. + tlsConfig := tlsConfigForAddress(session.tlsConfig, address) tlsConn := tls.Client(netConn, tlsConfig) if err = tlsConn.Handshake(); err != nil { session.log.OnEventf("Failed handshake: %v", err) diff --git a/session.go b/session.go index 342988f64..88c6362b0 100644 --- a/session.go +++ b/session.go @@ -17,11 +17,14 @@ package quickfix import ( "bytes" + "crypto/tls" "errors" "fmt" "sync" "time" + "golang.org/x/net/proxy" + "github.com/quickfixgo/quickfix/datadictionary" "github.com/quickfixgo/quickfix/internal" ) @@ -64,6 +67,10 @@ type session struct { timestampPrecision TimestampPrecision lastCheckedResetSeqTime time.Time + + // Specific to initiators; configured by sessionFactory. + tlsConfig *tls.Config + dialer proxy.ContextDialer } func (s *session) logError(err error) { diff --git a/session_factory.go b/session_factory.go index 1b79f5ee1..3f87ebe3b 100644 --- a/session_factory.go +++ b/session_factory.go @@ -537,7 +537,20 @@ func (f sessionFactory) buildInitiatorSettings(session *session, settings *Sessi } } - return f.configureSocketConnectAddress(session, settings) + if err := f.configureSocketConnectAddress(session, settings); err != nil { + return err + } + + var err error + if session.tlsConfig, err = loadTLSConfig(settings); err != nil { + return err + } + + if session.dialer, err = loadDialerConfig(settings); err != nil { + return err + } + + return nil } func (f sessionFactory) configureSocketConnectAddress(session *session, settings *SessionSettings) (err error) { diff --git a/session_factory_test.go b/session_factory_test.go index 349e7580c..1105f5f80 100644 --- a/session_factory_test.go +++ b/session_factory_test.go @@ -16,6 +16,7 @@ package quickfix import ( + "net" "testing" "time" @@ -470,6 +471,39 @@ func (s *SessionFactorySuite) TestNewSessionBuildInitiators() { s.Equal(10*time.Second, session.LogonTimeout) s.Equal(2*time.Second, session.LogoutTimeout) s.Equal("127.0.0.1:5000", session.SocketConnectAddress[0]) + s.NotNil(session.dialer) + s.Nil(session.tlsConfig) +} + +func (s *SessionFactorySuite) TestNewSessionBuildInitiatorsTLSAndDialer() { + s.sessionFactory.BuildInitiators = true + s.SessionSettings.Set(config.HeartBtInt, "34") + s.SessionSettings.Set(config.SocketConnectHost, "127.0.0.1") + s.SessionSettings.Set(config.SocketConnectPort, "5000") + s.SessionSettings.Set(config.SocketUseSSL, "Y") + s.SessionSettings.Set(config.SocketServerName, "fix.example.com") + s.SessionSettings.Set(config.SocketTimeout, "5s") + + session, err := s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + s.Require().Nil(err) + s.Require().NotNil(session.tlsConfig) + s.Equal("fix.example.com", session.tlsConfig.ServerName) + s.Require().NotNil(session.dialer) + + stdDialer, ok := session.dialer.(*net.Dialer) + s.Require().True(ok) + s.Equal(5*time.Second, stdDialer.Timeout) +} + +func (s *SessionFactorySuite) TestNewSessionBuildInitiatorsInvalidDialer() { + s.sessionFactory.BuildInitiators = true + s.SessionSettings.Set(config.HeartBtInt, "34") + s.SessionSettings.Set(config.SocketConnectHost, "127.0.0.1") + s.SessionSettings.Set(config.SocketConnectPort, "5000") + s.SessionSettings.Set(config.ProxyType, "totallyinvalidproxytype") + + _, err := s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + s.NotNil(err) } func (s *SessionFactorySuite) TestDuplicateSession() { @@ -498,6 +532,8 @@ func (s *SessionFactorySuite) TestNewSessionBuildAcceptors() { s.False(session.InitiateLogon) s.Zero(session.HeartBtInt) s.False(session.HeartBtIntOverride) + s.Nil(session.tlsConfig) + s.Nil(session.dialer) s.SessionSettings.Set(config.HeartBtIntOverride, "Y") session, err = s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) diff --git a/tls.go b/tls.go index 335d8e5d7..1daf879ec 100644 --- a/tls.go +++ b/tls.go @@ -20,6 +20,7 @@ import ( "crypto/x509" "errors" "fmt" + "net" "os" "github.com/quickfixgo/quickfix/config" @@ -183,3 +184,23 @@ func setMinVersionExplicit(settings *SessionSettings, tlsConfig *tls.Config) { } } } + +// tlsConfigForAddress returns a TLS config suitable for dialing address. +// When ServerName is unset and verification is enabled, a clone is returned with +// ServerName derived from address so the shared session config is not mutated. +func tlsConfigForAddress(base *tls.Config, address string) *tls.Config { + if base == nil { + return nil + } + if base.InsecureSkipVerify || base.ServerName != "" { + return base + } + + cfg := base.Clone() + host, _, err := net.SplitHostPort(address) + if err != nil { + host = address + } + cfg.ServerName = host + return cfg +} diff --git a/tls_test.go b/tls_test.go index ea9ea93a6..d0d9089e2 100644 --- a/tls_test.go +++ b/tls_test.go @@ -331,3 +331,22 @@ func (s *TLSTestSuite) TestServerNameWithCertsFromBytes() { s.NotNil(tlsConfig) s.Equal("DummyServerNameWithCerts", tlsConfig.ServerName) } + +func (s *TLSTestSuite) TestTLSConfigForAddress() { + s.Nil(tlsConfigForAddress(nil, "example.com:443")) + + configured := &tls.Config{ServerName: "configured.example.com"} + s.Equal(configured, tlsConfigForAddress(configured, "other.example.com:443")) + + insecure := &tls.Config{InsecureSkipVerify: true} + s.Equal(insecure, tlsConfigForAddress(insecure, "example.com:443")) + + base := &tls.Config{} + derived := tlsConfigForAddress(base, "example.com:443") + s.NotEqual(base, derived) + s.Equal("example.com", derived.ServerName) + s.Empty(base.ServerName) + + ipv6 := tlsConfigForAddress(&tls.Config{}, "[2001:db8:a0b:12f0::1]:3001") + s.Equal("2001:db8:a0b:12f0::1", ipv6.ServerName) +}