diff --git a/connection.go b/connection.go index daff945ec..99423d0d8 100644 --- a/connection.go +++ b/connection.go @@ -30,7 +30,6 @@ type mysqlConn struct { result mysqlResult // managed by clearResult() and handleOkPacket(). compIO *compIO cfg *Config - connector *connector maxAllowedPacket int maxWriteSize int capabilities capabilityFlag diff --git a/connector.go b/connector.go index 3d3760477..0a90f0367 100644 --- a/connector.go +++ b/connector.go @@ -19,8 +19,7 @@ import ( ) type connector struct { - cfg *Config // immutable private copy. - encodedAttributes string // Encoded connection attributes. + cfg *Config // immutable private copy. } func encodeConnectionAttributes(cfg *Config) string { @@ -55,10 +54,8 @@ func encodeConnectionAttributes(cfg *Config) string { } func newConnector(cfg *Config) *connector { - encodedAttributes := encodeConnectionAttributes(cfg) return &connector{ - cfg: cfg, - encodedAttributes: encodedAttributes, + cfg: cfg, } } @@ -75,6 +72,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { if err != nil { return nil, err } + cfg.encodedAttributes = encodeConnectionAttributes(cfg) } // New mysqlConn @@ -83,7 +81,6 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { maxWriteSize: maxPacketSize - 1, closech: make(chan struct{}), cfg: cfg, - connector: c, } mc.parseTime = mc.cfg.ParseTime @@ -91,12 +88,12 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { dctx := ctx if mc.cfg.Timeout > 0 { var cancel context.CancelFunc - dctx, cancel = context.WithTimeout(ctx, c.cfg.Timeout) + dctx, cancel = context.WithTimeout(ctx, mc.cfg.Timeout) defer cancel() } - if c.cfg.DialFunc != nil { - mc.netConn, err = c.cfg.DialFunc(dctx, mc.cfg.Net, mc.cfg.Addr) + if mc.cfg.DialFunc != nil { + mc.netConn, err = mc.cfg.DialFunc(dctx, mc.cfg.Net, mc.cfg.Addr) } else { dialsLock.RLock() dial, ok := dials[mc.cfg.Net] @@ -116,7 +113,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { // Enable TCP Keepalives on TCP connections if tc, ok := mc.netConn.(*net.TCPConn); ok { if err := tc.SetKeepAlive(true); err != nil { - c.cfg.Logger.Print(err) + mc.cfg.Logger.Print(err) } } @@ -145,7 +142,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { authResp, err := mc.auth(authData, plugin) if err != nil { // try the default auth plugin, if using the requested plugin failed - c.cfg.Logger.Print("could not use requested auth plugin '"+plugin+"': ", err.Error()) + mc.cfg.Logger.Print("could not use requested auth plugin '"+plugin+"': ", err.Error()) plugin = defaultAuthPlugin authResp, err = mc.auth(authData, plugin) if err != nil { @@ -153,7 +150,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { return nil, err } } - mc.initCapabilities(serverCapabilities, serverExtCapabilities, mc.cfg) + mc.initCapabilities(serverCapabilities, serverExtCapabilities) if err = mc.writeHandshakeResponsePacket(authResp, plugin); err != nil { mc.cleanup() return nil, err diff --git a/connector_test.go b/connector_test.go index 82d8c5989..0725877e4 100644 --- a/connector_test.go +++ b/connector_test.go @@ -1,7 +1,9 @@ package mysql import ( + "bytes" "context" + "errors" "net" "testing" "time" @@ -28,3 +30,117 @@ func TestConnectorReturnsTimeout(t *testing.T) { t.Fatalf("expected %T, got %T", nerr, err) } } + +func TestBeforeConnectUsesEffectiveTimeout(t *testing.T) { + dialErr := errors.New("stop after observing dial context") + var remaining time.Duration + + cfg := NewConfig() + cfg.Timeout = 2 * time.Hour + cfg.DialFunc = func(ctx context.Context, _, _ string) (net.Conn, error) { + deadline, ok := ctx.Deadline() + if !ok { + t.Fatal("dial context has no deadline") + } + remaining = time.Until(deadline) + return nil, dialErr + } + if err := cfg.Apply(BeforeConnect(func(_ context.Context, cfg *Config) error { + cfg.Timeout = time.Hour + return nil + })); err != nil { + t.Fatal(err) + } + + connector, err := NewConnector(cfg) + if err != nil { + t.Fatal(err) + } + if _, err := connector.Connect(context.Background()); !errors.Is(err, dialErr) { + t.Fatalf("Connect() error = %v, want %v", err, dialErr) + } + if remaining < 59*time.Minute || remaining > 61*time.Minute { + t.Fatalf("dial timeout = %v, want about 1h", remaining) + } +} + +func TestBeforeConnectUsesEffectiveDialerAndAttributes(t *testing.T) { + serverHandshake := []byte( + "\x48\x00\x00\x00" + // Packet header: 72-byte payload, sequence 0. + "\x0a" + // Protocol version 10. + "5.5.8\x00" + // NUL-terminated server version. + "\xa5\x00\x00\x00" + // Connection ID 165. + " query + beforeConnect func(context.Context, *Config) error // Invoked before a connection is established + encodedAttributes string // Encoded connection attributes + paramOrder []string // Order of connection parameters parsed from the DSN + pubKey *rsa.PublicKey // Server public key + timeTruncate time.Duration // Truncate time.Time values to the specified duration + charsets []string // Connection charset. When set, this will be set in SET NAMES query } // Functional Options Pattern @@ -146,6 +147,9 @@ func TimeTruncate(d time.Duration) Option { } // BeforeConnect sets the function to be invoked before a connection is established. +// If the function changes [Config.Addr] while [Config.TLS] is non-nil and its +// InsecureSkipVerify field is false, it must also update ServerName to match +// the hostname in the new address. func BeforeConnect(fn func(context.Context, *Config) error) Option { return func(cfg *Config) error { cfg.beforeConnect = fn @@ -263,6 +267,7 @@ func (cfg *Config) normalize() error { if cfg.Logger == nil { cfg.Logger = defaultLogger } + cfg.encodedAttributes = encodeConnectionAttributes(cfg) return nil } diff --git a/dsn_test.go b/dsn_test.go index 2c1f3f889..120550cf4 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -33,6 +33,7 @@ func newTestConfig(update func(*Config)) *Config { if update != nil { update(cfg) } + cfg.encodedAttributes = encodeConnectionAttributes(cfg) return cfg } diff --git a/packets.go b/packets.go index d0b21b06c..d08969d9b 100644 --- a/packets.go +++ b/packets.go @@ -277,7 +277,7 @@ func (mc *mysqlConn) readHandshakePacket() (data []byte, capabilities capability } // initCapabilities initializes the capabilities based on server support and configuration -func (mc *mysqlConn) initCapabilities(serverCapabilities capabilityFlag, serverExtCapabilities extendedCapabilityFlag, cfg *Config) { +func (mc *mysqlConn) initCapabilities(serverCapabilities capabilityFlag, serverExtCapabilities extendedCapabilityFlag) { clientCapabilities := clientMySQL | clientLongFlag | @@ -291,10 +291,10 @@ func (mc *mysqlConn) initCapabilities(serverCapabilities capabilityFlag, serverE clientConnectAttrs | clientDeprecateEOF - if cfg.ClientFoundRows { + if mc.cfg.ClientFoundRows { clientCapabilities |= clientFoundRows } - if cfg.compress { + if mc.cfg.compress { clientCapabilities |= clientCompress } // To enable TLS / SSL @@ -305,7 +305,7 @@ func (mc *mysqlConn) initCapabilities(serverCapabilities capabilityFlag, serverE if mc.cfg.MultiStatements { clientCapabilities |= clientMultiStatements } - if n := len(cfg.DBName); n > 0 { + if n := len(mc.cfg.DBName); n > 0 { clientCapabilities |= clientConnectWithDB } @@ -405,9 +405,9 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string // Connection Attributes if mc.capabilities&clientConnectAttrs != 0 { - connAttrsLen := len(mc.connector.encodedAttributes) + connAttrsLen := len(mc.cfg.encodedAttributes) data = appendLengthEncodedInteger(data, uint64(connAttrsLen)) - data = append(data, mc.connector.encodedAttributes...) + data = append(data, mc.cfg.encodedAttributes...) } // Send Auth packet diff --git a/packets_test.go b/packets_test.go index b487051e2..17bfe15c8 100644 --- a/packets_test.go +++ b/packets_test.go @@ -96,11 +96,13 @@ var _ net.Conn = new(mockConn) func newRWMockConn(sequence uint8) (*mockConn, *mysqlConn) { conn := new(mockConn) - connector := newConnector(NewConfig()) + cfg := NewConfig() + if err := cfg.normalize(); err != nil { + panic(err) + } mc := &mysqlConn{ buf: newBuffer(), - cfg: connector.cfg, - connector: connector, + cfg: cfg, netConn: conn, closech: make(chan struct{}), maxAllowedPacket: defaultMaxAllowedPacket,