| // Go MySQL Driver - A MySQL-Driver for Go's database/sql package | |
| // | |
| // Copyright 2023 The Go-MySQL-Driver Authors. All rights reserved. | |
| // | |
| // This Source Code Form is subject to the terms of the Mozilla Public | |
| // License, v. 2.0. If a copy of the MPL was not distributed with this file, | |
| // You can obtain one at http://mozilla.org/MPL/2.0/. | |
| package mysql | |
| import ( | |
| "context" | |
| "crypto/rsa" | |
| "crypto/tls" | |
| "errors" | |
| "math/big" | |
| "net" | |
| "sync" | |
| "sync/atomic" | |
| "testing" | |
| "time" | |
| ) | |
| func TestSimpleAuthRejectsContinuation(t *testing.T) { | |
| nextPacket, err := (simpleAuth{}).ContinuationAuth(context.Background(), nil, nil, &AuthContext{}) | |
| if err != ErrMalformPkt { | |
| t.Fatalf("expected ErrMalformPkt, got %v", err) | |
| } | |
| if nextPacket != nil { | |
| t.Errorf("expected no response packet, got %v", nextPacket) | |
| } | |
| } | |
| func TestRegisterAuthPluginFactoryLifetime(t *testing.T) { | |
| var calls atomic.Int32 | |
| const name = "test_auth_factory_lifetime" | |
| registerTestAuthPlugin(t, name, func() AuthPlugin { | |
| calls.Add(1) | |
| return &authTestPlugin{} | |
| }) | |
| if calls.Load() != 0 { | |
| t.Fatal("registration called the factory") | |
| } | |
| one, ok := globalPluginRegistry.GetPlugin(name) | |
| if !ok { | |
| t.Fatal("plugin was not registered") | |
| } | |
| two, ok := globalPluginRegistry.GetPlugin(name) | |
| if !ok || one == two || calls.Load() != 2 { | |
| t.Fatal("lookups did not create independent plugin instances") | |
| } | |
| RegisterAuthPlugin(name, func() AuthPlugin { return &nativePasswordPlugin{} }) | |
| if plugin, ok := globalPluginRegistry.GetPlugin(name); !ok { | |
| t.Fatal("replacement was not registered") | |
| } else if _, ok := plugin.(*nativePasswordPlugin); !ok { | |
| t.Fatalf("replacement returned %T", plugin) | |
| } | |
| } | |
| func TestRegisterAuthPluginOverridesBuiltins(t *testing.T) { | |
| for _, name := range []string{ | |
| "mysql_native_password", "mysql_old_password", "mysql_clear_password", | |
| "sha256_password", "caching_sha2_password", "client_ed25519", | |
| } { | |
| t.Run(name, func(t *testing.T) { | |
| if _, ok := globalPluginRegistry.GetPlugin(name); !ok { | |
| t.Fatal("built-in plugin is not registered") | |
| } | |
| for _, stage := range []string{"greeting", "switch"} { | |
| t.Run(stage, func(t *testing.T) { | |
| want := errors.New("replacement plugin called") | |
| calls := 0 | |
| registerTestAuthPlugin(t, name, func() AuthPlugin { | |
| return &authTestPlugin{init: func(context.Context, []byte, *AuthContext) ([]byte, error) { | |
| calls++ | |
| return nil, want | |
| }} | |
| }) | |
| greeting := name | |
| wantWrites := 0 | |
| if stage == "switch" { | |
| greeting = "mysql_native_password" | |
| if name == greeting { | |
| greeting = "caching_sha2_password" | |
| } | |
| wantWrites = 1 | |
| } | |
| caps := clientMySQL | clientProtocol41 | clientSecureConn | clientPluginAuth | clientPluginAuthLenEncClientData | |
| mock := &mockConn{ | |
| data: authTestHandshake(greeting, caps), | |
| queuedReplies: [][]byte{authFallbackSwitch(name)}, | |
| maxReads: 2, | |
| } | |
| cfg := NewConfig() | |
| cfg.Passwd = "secret" | |
| cfg.AllowOldPasswords = true | |
| cfg.AllowCleartextPasswords = true | |
| cfg.DialFunc = func(context.Context, string, string) (net.Conn, error) { return mock, nil } | |
| c, err := NewConnector(cfg) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| conn, err := c.Connect(t.Context()) | |
| if conn != nil { | |
| conn.Close() | |
| } | |
| if !errors.Is(err, want) || calls != 1 || mock.writes != wantWrites { | |
| t.Fatalf("Connect = %v, calls = %d, writes = %d; want %v, 1, %d", err, calls, mock.writes, want, wantWrites) | |
| } | |
| }) | |
| } | |
| }) | |
| } | |
| } | |
| func TestAuthPluginFactoryOutsideLock(t *testing.T) { | |
| r := newPluginRegistry() | |
| var calls atomic.Int32 | |
| r.Register("outer", func() AuthPlugin { | |
| // Factories may consult or update the registry, even when multiple | |
| // connections start authentication concurrently. | |
| r.Register("inner", func() AuthPlugin { return &nativePasswordPlugin{} }) | |
| plugin, _ := r.GetPlugin("inner") | |
| calls.Add(1) | |
| return plugin | |
| }) | |
| var wg sync.WaitGroup | |
| for range 20 { | |
| wg.Add(1) | |
| go func() { | |
| defer wg.Done() | |
| r.GetPlugin("outer") | |
| }() | |
| } | |
| done := make(chan struct{}) | |
| go func() { wg.Wait(); close(done) }() | |
| select { | |
| case <-done: | |
| if calls.Load() != 20 { | |
| t.Fatalf("factory calls = %d, want 20", calls.Load()) | |
| } | |
| case <-time.After(5 * time.Second): | |
| t.Fatal("factory was called with the registry locked") | |
| } | |
| } | |
| func TestAuthContextSnapshot(t *testing.T) { | |
| cfg := NewConfig() | |
| cfg.User = "user" | |
| cfg.Passwd = "first-factor" | |
| cfg.Net = "unix" | |
| cfg.TLS = &tls.Config{} | |
| cfg.pubKey = &rsa.PublicKey{N: big.NewInt(12345), E: 65537} | |
| auth := newAuthContext(cfg, "selected-factor", false) | |
| // Later configuration changes must not change an active exchange. | |
| cfg.User = "changed" | |
| cfg.Passwd = "changed" | |
| cfg.Net = "tcp" | |
| cfg.pubKey.N.SetInt64(99) | |
| cfg.pubKey.E = 3 | |
| if auth.User() != "user" || auth.Password() != "selected-factor" || !auth.UnixSocket() { | |
| t.Fatal("authentication snapshot changed with the configuration") | |
| } | |
| if auth.TLS() { | |
| t.Fatal("configured TLS must not be reported as an established TLS connection") | |
| } | |
| key := auth.ServerPublicKey() | |
| if key.N.Int64() != 12345 || key.E != 65537 { | |
| t.Fatal("authentication snapshot shares the configured RSA key") | |
| } | |
| key.N.SetInt64(42) | |
| key.E = 17 | |
| key = auth.ServerPublicKey() | |
| if key.N.Int64() != 12345 || key.E != 65537 { | |
| t.Fatal("ServerPublicKey returned a mutable reference into the snapshot") | |
| } | |
| if (&AuthContext{}).ServerPublicKey() != nil { | |
| t.Fatal("expected nil for an unconfigured RSA key") | |
| } | |
| } | |
| func TestInitAuthPolicy(t *testing.T) { | |
| for _, tc := range []struct { | |
| plugin string | |
| want error | |
| }{ | |
| {"mysql_native_password", ErrNativePassword}, | |
| {"mysql_old_password", ErrOldPassword}, | |
| {"mysql_clear_password", ErrCleartextPassword}, | |
| } { | |
| t.Run(tc.plugin, func(t *testing.T) { | |
| _, mc := newRWMockConn(1) | |
| mc.cfg.AllowNativePasswords = false | |
| mc.cfg.Passwd = "password" | |
| auth := newAuthContext(mc.cfg, mc.cfg.Passwd, false) | |
| plugin, response, err := mc.initAuth(context.Background(), tc.plugin, nil, auth) | |
| if err != tc.want || plugin != nil || response != nil { | |
| t.Fatalf("initAuth() = (%T, %v, %v), want (nil, nil, %v)", plugin, response, err, tc.want) | |
| } | |
| }) | |
| } | |
| } | |
| func TestAuthPluginCancellation(t *testing.T) { | |
| for _, stage := range []string{"initial", "continuation"} { | |
| t.Run(stage, func(t *testing.T) { | |
| ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) | |
| defer cancel() | |
| entered := make(chan struct{}) | |
| waitForCancel := func(gotCtx context.Context, _ []byte, _ *AuthContext) ([]byte, error) { | |
| if gotCtx != ctx { | |
| return nil, errors.New("plugin did not receive the connection context") | |
| } | |
| close(entered) | |
| <-gotCtx.Done() | |
| return nil, gotCtx.Err() | |
| } | |
| const name = "test_auth_cancel" | |
| factory := func() AuthPlugin { | |
| return &authTestPlugin{ | |
| init: waitForCancel, | |
| next: func(ctx context.Context, packet, _ []byte, auth *AuthContext) ([]byte, error) { | |
| return waitForCancel(ctx, packet, auth) | |
| }, | |
| } | |
| } | |
| registerTestAuthPlugin(t, name, factory) | |
| conn, mc := newRWMockConn(2) | |
| auth := newAuthContext(mc.cfg, mc.cfg.Passwd, false) | |
| conn.data = makePacket(2, []byte{iAuthMoreData, 7}) | |
| conn.maxReads = 1 | |
| result := make(chan error, 1) | |
| go func() { | |
| if stage == "initial" { | |
| _, _, err := mc.initAuth(ctx, name, nil, auth) | |
| result <- err | |
| } else { | |
| result <- mc.handleAuthResult(ctx, nil, factory(), auth) | |
| } | |
| }() | |
| select { | |
| case <-entered: | |
| cancel() | |
| case err := <-result: | |
| t.Fatalf("plugin returned without waiting for cancellation: %v", err) | |
| case <-ctx.Done(): | |
| t.Fatal("plugin was not called") | |
| } | |
| if err := <-result; err != context.Canceled { | |
| t.Fatalf("got %v, want context.Canceled", err) | |
| } | |
| if len(conn.written) != 0 { | |
| t.Error("sent authentication data after cancellation") | |
| } | |
| }) | |
| } | |
| } | |
| func TestAuthSwitchContextSnapshot(t *testing.T) { | |
| conn, mc := newRWMockConn(2) | |
| mc.cfg.User = "user" | |
| mc.cfg.Passwd = "configured-password" | |
| auth := newAuthContext(mc.cfg, "selected-password", true) | |
| // A switch must keep the selected credentials and established transport, | |
| // even if the original configuration changes. | |
| mc.cfg.User = "changed" | |
| mc.cfg.Passwd = "changed" | |
| ctx := t.Context() | |
| var switchedAuth *AuthContext | |
| continued := false | |
| const name = "test_auth_switch_context" | |
| registerTestAuthPlugin(t, name, func() AuthPlugin { | |
| return &authTestPlugin{ | |
| init: func(gotCtx context.Context, seed []byte, gotAuth *AuthContext) ([]byte, error) { | |
| if gotCtx != ctx || gotAuth == auth || gotAuth.User() != "user" || gotAuth.Password() != "selected-password" || !gotAuth.TLS() || string(seed) != "challenge" { | |
| return nil, errors.New("auth switch did not preserve the authentication snapshot") | |
| } | |
| switchedAuth = gotAuth | |
| return []byte{42}, nil | |
| }, | |
| next: func(gotCtx context.Context, packet, seed []byte, gotAuth *AuthContext) ([]byte, error) { | |
| if gotCtx != ctx || gotAuth != switchedAuth || string(seed) != "challenge" { | |
| return nil, errors.New("continuation did not retain the switched context") | |
| } | |
| continued = true | |
| return nil, nil // Read the queued OK without sending a response. | |
| }, | |
| } | |
| }) | |
| payload := append([]byte{iEOF}, name...) | |
| payload = append(payload, 0) | |
| payload = append(payload, "challenge\x00"...) | |
| conn.data = makePacket(2, payload) | |
| conn.queuedReplies = [][]byte{append( | |
| makePacket(4, []byte{iAuthMoreData, 7}), | |
| makePacket(5, []byte{0, 0, 0, 2, 0, 0, 0})..., | |
| )} | |
| conn.maxReads = 2 | |
| if err := mc.handleAuthResult(ctx, nil, &nativePasswordPlugin{}, auth); err != nil { | |
| t.Fatal(err) | |
| } | |
| if !continued || conn.writes != 1 { | |
| t.Fatalf("continued = %v, writes = %d; want true, 1", continued, conn.writes) | |
| } | |
| } |