blob: 0013bef94da62dda98bba88664e1e00707f9b2bb [file] [edit]
// 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)
}
}