feat(canary): mysql wire protocol (handshake + auth + ERR packets)
Phase 8 first commit. Pure encoding/decoding layer for the MySQL
wire protocol — no I/O orchestration, no DB lookups, no business
logic. Three primary functions plus utility helpers, all driven by
the byte-exact spec §9.7 lines 1363-1403:
- BuildHandshakeV10(connID, authData[20]) []byte
Server's initial greeting packet (0x0a protocol version,
"5.7.40-canary" server version, random auth-plugin-data split
into 8+12 byte parts, capability flags 0xf7ff / 0x81ff,
utf8mb4_unicode_ci charset, mysql_native_password plugin name).
Wraps in 3-byte LE length + 1-byte sequence ID (0x00).
- ReadClientAuth(r io.Reader) (*ClientAuth, error)
Parses HandshakeResponse41. Returns the username (null-terminated,
extracted starting at byte offset 32 after the 4+4+1+23-byte
fixed header) plus the client's capabilities, max packet size,
and charset for forensic richness. Defensive bounds checking
against ErrInvalidPayload / ErrUsernameMissing / ErrShortPacket /
ErrPacketTooLarge sentinels. 64KB max packet size cap.
- BuildAccessDeniedErr(username, sourceHost string) []byte
Standard MySQL ERR_Packet 1045 with SQL state "28000" and the
exact "Access denied for user '%s'@'%s' (using password: YES)"
message kubectl and the mysql client both display verbatim.
Sequence ID 0x02 (after handshake=0 and client auth=1).
Plus crypto/rand-backed helpers NewRandomAuthData (20 bytes for the
challenge) and NewRandomConnectionID (uint32). Consistent with the
crypto/rand discipline from Phase 0 supplement, Phase 5 (pdf), Phase 7
(envfile).
Tests (~24 cases): byte-exact assertions on packet headers, payload
layout, sequence IDs; round-trip Build/Read for 5 username variants;
error paths (short packet, missing username terminator, empty reader,
oversize packet rejected mid-stream); randomness distinctness (50
calls → near-50 unique values).
Handler + server land in next commit; generator + registry in the
one after. Trigger interface conformance for the mysql Generator will
be a sentinel-error no-op (mysql triggers over TCP, not HTTP — Phase
9 router doesn't mount any HTTP path to mysql.Trigger).
go test -race -timeout=60s ./internal/token/generators/mysql/...
passes, golangci-lint clean.
This commit is contained in:
parent
3d9b97da21
commit
712202abc0
|
|
@ -0,0 +1,220 @@
|
|||
// ©AngelaMos | 2026
|
||||
// protocol.go
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
packetHeaderSize = 4
|
||||
protocolVersion = 0x0a
|
||||
serverVersionString = "5.7.40-canary"
|
||||
|
||||
capabilityFlagsLower = uint16(0xf7ff)
|
||||
capabilityFlagsUpper = uint16(0x81ff)
|
||||
|
||||
characterSetUtf8MB4 = 0x21
|
||||
statusFlags = uint16(0x0002)
|
||||
authPluginDataLen = byte(0x15)
|
||||
|
||||
authPluginName = "mysql_native_password"
|
||||
|
||||
errPacketHeader = 0xff
|
||||
errCodeAccessDenied = uint16(1045)
|
||||
sqlStateMarker = '#'
|
||||
sqlStateAccessDenied = "28000"
|
||||
|
||||
seqIDServerHandshake byte = 0x00
|
||||
seqIDServerErr byte = 0x02
|
||||
|
||||
maxPacketSize = 0x00ffffff
|
||||
handshakeResponseMaxBytes = 64 * 1024
|
||||
|
||||
handshakeResponseFillerLen = 23
|
||||
handshakeResponseHeaderBytes = 4 + 4 + 1 + handshakeResponseFillerLen
|
||||
|
||||
authPluginDataTotalLen = 20
|
||||
authPluginDataPart1Len = 8
|
||||
authPluginDataPart2Len = 12
|
||||
)
|
||||
|
||||
var (
|
||||
ErrShortPacket = errors.New("mysql: short packet header")
|
||||
ErrPacketTooLarge = errors.New("mysql: packet exceeds max size")
|
||||
ErrInvalidPayload = errors.New("mysql: invalid payload structure")
|
||||
ErrUsernameMissing = errors.New(
|
||||
"mysql: username missing or unterminated",
|
||||
)
|
||||
ErrHTTPTriggerNotSupported = errors.New(
|
||||
"mysql: Trigger via HTTP is not applicable (use the TCP listener)",
|
||||
)
|
||||
)
|
||||
|
||||
type ClientAuth struct {
|
||||
Capabilities uint32
|
||||
MaxPacketSize uint32
|
||||
Charset uint8
|
||||
Username string
|
||||
}
|
||||
|
||||
func BuildHandshakeV10(
|
||||
connID uint32,
|
||||
authData [authPluginDataTotalLen]byte,
|
||||
) ([]byte, error) {
|
||||
var payload bytes.Buffer
|
||||
payload.WriteByte(protocolVersion)
|
||||
payload.WriteString(serverVersionString)
|
||||
payload.WriteByte(0x00)
|
||||
|
||||
var connIDBytes [4]byte
|
||||
binary.LittleEndian.PutUint32(connIDBytes[:], connID)
|
||||
payload.Write(connIDBytes[:])
|
||||
|
||||
payload.Write(authData[:authPluginDataPart1Len])
|
||||
payload.WriteByte(0x00)
|
||||
|
||||
var capLower [2]byte
|
||||
binary.LittleEndian.PutUint16(capLower[:], capabilityFlagsLower)
|
||||
payload.Write(capLower[:])
|
||||
|
||||
payload.WriteByte(characterSetUtf8MB4)
|
||||
|
||||
var statusBytes [2]byte
|
||||
binary.LittleEndian.PutUint16(statusBytes[:], statusFlags)
|
||||
payload.Write(statusBytes[:])
|
||||
|
||||
var capUpper [2]byte
|
||||
binary.LittleEndian.PutUint16(capUpper[:], capabilityFlagsUpper)
|
||||
payload.Write(capUpper[:])
|
||||
|
||||
payload.WriteByte(authPluginDataLen)
|
||||
|
||||
var reserved [10]byte
|
||||
payload.Write(reserved[:])
|
||||
|
||||
payload.Write(authData[authPluginDataPart1Len:])
|
||||
payload.WriteByte(0x00)
|
||||
|
||||
payload.WriteString(authPluginName)
|
||||
payload.WriteByte(0x00)
|
||||
|
||||
return wrapPacket(payload.Bytes(), seqIDServerHandshake)
|
||||
}
|
||||
|
||||
func ReadClientAuth(r io.Reader) (*ClientAuth, error) {
|
||||
payload, err := readPacket(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(payload) < handshakeResponseHeaderBytes+1 {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: have %d bytes, need at least %d",
|
||||
ErrInvalidPayload,
|
||||
len(payload),
|
||||
handshakeResponseHeaderBytes+1,
|
||||
)
|
||||
}
|
||||
|
||||
auth := &ClientAuth{
|
||||
Capabilities: binary.LittleEndian.Uint32(payload[0:4]),
|
||||
MaxPacketSize: binary.LittleEndian.Uint32(payload[4:8]),
|
||||
Charset: payload[8],
|
||||
}
|
||||
|
||||
usernameStart := handshakeResponseHeaderBytes
|
||||
rel := bytes.IndexByte(payload[usernameStart:], 0x00)
|
||||
if rel < 0 {
|
||||
return nil, ErrUsernameMissing
|
||||
}
|
||||
auth.Username = string(payload[usernameStart : usernameStart+rel])
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func BuildAccessDeniedErr(username, sourceHost string) ([]byte, error) {
|
||||
msg := fmt.Sprintf(
|
||||
`Access denied for user '%s'@'%s' (using password: YES)`,
|
||||
username,
|
||||
sourceHost,
|
||||
)
|
||||
|
||||
var payload bytes.Buffer
|
||||
payload.WriteByte(errPacketHeader)
|
||||
|
||||
var code [2]byte
|
||||
binary.LittleEndian.PutUint16(code[:], errCodeAccessDenied)
|
||||
payload.Write(code[:])
|
||||
|
||||
payload.WriteByte(sqlStateMarker)
|
||||
payload.WriteString(sqlStateAccessDenied)
|
||||
payload.WriteString(msg)
|
||||
|
||||
return wrapPacket(payload.Bytes(), seqIDServerErr)
|
||||
}
|
||||
|
||||
func wrapPacket(payload []byte, seqID byte) ([]byte, error) {
|
||||
if len(payload) > maxPacketSize {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: payload %d > max %d",
|
||||
ErrPacketTooLarge,
|
||||
len(payload),
|
||||
maxPacketSize,
|
||||
)
|
||||
}
|
||||
out := make([]byte, packetHeaderSize+len(payload))
|
||||
n := len(payload)
|
||||
out[0] = byte(n & 0xff)
|
||||
out[1] = byte((n >> 8) & 0xff)
|
||||
out[2] = byte((n >> 16) & 0xff)
|
||||
out[3] = seqID
|
||||
copy(out[packetHeaderSize:], payload)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func readPacket(r io.Reader) ([]byte, error) {
|
||||
var hdr [packetHeaderSize]byte
|
||||
if _, err := io.ReadFull(r, hdr[:]); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrShortPacket, err)
|
||||
}
|
||||
payloadLen := uint32(hdr[0]) |
|
||||
uint32(hdr[1])<<8 |
|
||||
uint32(hdr[2])<<16
|
||||
if payloadLen > handshakeResponseMaxBytes {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: payload %d > limit %d",
|
||||
ErrPacketTooLarge,
|
||||
payloadLen,
|
||||
handshakeResponseMaxBytes,
|
||||
)
|
||||
}
|
||||
if payloadLen == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
payload := make([]byte, payloadLen)
|
||||
if _, err := io.ReadFull(r, payload); err != nil {
|
||||
return nil, fmt.Errorf("mysql: read payload: %w", err)
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func NewRandomAuthData() ([authPluginDataTotalLen]byte, error) {
|
||||
var data [authPluginDataTotalLen]byte
|
||||
if _, err := rand.Read(data[:]); err != nil {
|
||||
return data, fmt.Errorf("mysql: random auth data: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func NewRandomConnectionID() (uint32, error) {
|
||||
var b [4]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
return 0, fmt.Errorf("mysql: random connection id: %w", err)
|
||||
}
|
||||
return binary.LittleEndian.Uint32(b[:]), nil
|
||||
}
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
// ©AngelaMos | 2026
|
||||
// protocol_test.go
|
||||
|
||||
package mysql_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators/mysql"
|
||||
)
|
||||
|
||||
const (
|
||||
packetHeaderSize = 4
|
||||
expectedProtoVersion = 0x0a
|
||||
expectedCharset = 0x21
|
||||
)
|
||||
|
||||
func buildHandshakeResponse41(t *testing.T, username string) []byte {
|
||||
t.Helper()
|
||||
var payload bytes.Buffer
|
||||
|
||||
var caps [4]byte
|
||||
binary.LittleEndian.PutUint32(caps[:], 0x0001a285)
|
||||
payload.Write(caps[:])
|
||||
|
||||
var maxPkt [4]byte
|
||||
binary.LittleEndian.PutUint32(maxPkt[:], 0x01000000)
|
||||
payload.Write(maxPkt[:])
|
||||
|
||||
payload.WriteByte(expectedCharset)
|
||||
|
||||
var filler [23]byte
|
||||
payload.Write(filler[:])
|
||||
|
||||
payload.WriteString(username)
|
||||
payload.WriteByte(0x00)
|
||||
|
||||
body := payload.Bytes()
|
||||
out := make([]byte, packetHeaderSize+len(body))
|
||||
n := len(body)
|
||||
out[0] = byte(n & 0xff)
|
||||
out[1] = byte((n >> 8) & 0xff)
|
||||
out[2] = byte((n >> 16) & 0xff)
|
||||
out[3] = 0x01
|
||||
copy(out[packetHeaderSize:], body)
|
||||
return out
|
||||
}
|
||||
|
||||
func TestBuildHandshakeV10_HasCorrectPacketHeader(t *testing.T) {
|
||||
auth := [20]byte{
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
||||
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
||||
}
|
||||
pkt, err := mysql.BuildHandshakeV10(0xdeadbeef, auth)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(pkt), packetHeaderSize)
|
||||
|
||||
payloadLen := int(pkt[0]) |
|
||||
int(pkt[1])<<8 |
|
||||
int(pkt[2])<<16
|
||||
require.Equal(t, len(pkt)-packetHeaderSize, payloadLen)
|
||||
require.Equal(t, byte(0x00), pkt[3], "server handshake sequence ID is 0")
|
||||
}
|
||||
|
||||
func TestBuildHandshakeV10_PayloadStartsWithProtocolVersion(t *testing.T) {
|
||||
var auth [20]byte
|
||||
pkt, err := mysql.BuildHandshakeV10(0x1234, auth)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, byte(expectedProtoVersion), pkt[packetHeaderSize])
|
||||
}
|
||||
|
||||
func TestBuildHandshakeV10_ContainsServerVersionString(t *testing.T) {
|
||||
var auth [20]byte
|
||||
pkt, err := mysql.BuildHandshakeV10(0x1234, auth)
|
||||
require.NoError(t, err)
|
||||
require.Contains(
|
||||
t,
|
||||
string(pkt),
|
||||
"5.7.40-canary",
|
||||
"handshake must advertise the canary-server version string for verisimilitude",
|
||||
)
|
||||
}
|
||||
|
||||
func TestBuildHandshakeV10_AdvertisesMySQLNativePassword(t *testing.T) {
|
||||
var auth [20]byte
|
||||
pkt, err := mysql.BuildHandshakeV10(0x1234, auth)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(pkt), "mysql_native_password")
|
||||
}
|
||||
|
||||
func TestBuildHandshakeV10_EmbedsConnectionIDLittleEndian(t *testing.T) {
|
||||
var auth [20]byte
|
||||
connID := uint32(0xdeadbeef)
|
||||
pkt, err := mysql.BuildHandshakeV10(connID, auth)
|
||||
require.NoError(t, err)
|
||||
|
||||
versionEnd := bytes.IndexByte(
|
||||
pkt[packetHeaderSize+1:],
|
||||
0x00,
|
||||
) + packetHeaderSize + 1
|
||||
require.Greater(t, versionEnd, packetHeaderSize+1)
|
||||
connIDStart := versionEnd + 1
|
||||
got := binary.LittleEndian.Uint32(pkt[connIDStart : connIDStart+4])
|
||||
require.Equal(t, connID, got)
|
||||
}
|
||||
|
||||
func TestBuildHandshakeV10_EmbedsAuthDataInTwoParts(t *testing.T) {
|
||||
auth := [20]byte{
|
||||
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
|
||||
20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
|
||||
}
|
||||
pkt, err := mysql.BuildHandshakeV10(0xcafef00d, auth)
|
||||
require.NoError(t, err)
|
||||
require.Contains(
|
||||
t,
|
||||
string(pkt),
|
||||
string(auth[:8]),
|
||||
"first 8 bytes of auth data must appear in handshake",
|
||||
)
|
||||
require.Contains(
|
||||
t,
|
||||
string(pkt),
|
||||
string(auth[8:]),
|
||||
"last 12 bytes of auth data must appear in handshake (after part-1 + filler + caps + ...)",
|
||||
)
|
||||
}
|
||||
|
||||
func TestReadClientAuth_ExtractsUsername(t *testing.T) {
|
||||
pkt := buildHandshakeResponse41(t, "canary_abc123def456")
|
||||
auth, err := mysql.ReadClientAuth(bytes.NewReader(pkt))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, auth)
|
||||
require.Equal(t, "canary_abc123def456", auth.Username)
|
||||
}
|
||||
|
||||
func TestReadClientAuth_CapturesCapabilities(t *testing.T) {
|
||||
pkt := buildHandshakeResponse41(t, "anyone")
|
||||
auth, err := mysql.ReadClientAuth(bytes.NewReader(pkt))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(0x0001a285), auth.Capabilities)
|
||||
require.Equal(t, uint32(0x01000000), auth.MaxPacketSize)
|
||||
require.Equal(t, uint8(expectedCharset), auth.Charset)
|
||||
}
|
||||
|
||||
func TestReadClientAuth_EmptyUsernameStillParses(t *testing.T) {
|
||||
pkt := buildHandshakeResponse41(t, "")
|
||||
auth, err := mysql.ReadClientAuth(bytes.NewReader(pkt))
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, auth.Username)
|
||||
}
|
||||
|
||||
func TestReadClientAuth_ShortPayloadReturnsError(t *testing.T) {
|
||||
short := make([]byte, packetHeaderSize+5)
|
||||
short[0] = 0x05
|
||||
_, err := mysql.ReadClientAuth(bytes.NewReader(short))
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, mysql.ErrInvalidPayload)
|
||||
}
|
||||
|
||||
func TestReadClientAuth_NoUsernameTerminatorReturnsError(t *testing.T) {
|
||||
var payload bytes.Buffer
|
||||
var caps [4]byte
|
||||
payload.Write(caps[:])
|
||||
var maxPkt [4]byte
|
||||
payload.Write(maxPkt[:])
|
||||
payload.WriteByte(expectedCharset)
|
||||
var filler [23]byte
|
||||
payload.Write(filler[:])
|
||||
payload.WriteString("nocan_terminator_here_no_null")
|
||||
body := payload.Bytes()
|
||||
|
||||
out := make([]byte, packetHeaderSize+len(body))
|
||||
n := len(body)
|
||||
out[0] = byte(n & 0xff)
|
||||
out[1] = byte((n >> 8) & 0xff)
|
||||
out[2] = byte((n >> 16) & 0xff)
|
||||
out[3] = 0x01
|
||||
copy(out[packetHeaderSize:], body)
|
||||
|
||||
_, err := mysql.ReadClientAuth(bytes.NewReader(out))
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, mysql.ErrUsernameMissing)
|
||||
}
|
||||
|
||||
func TestReadClientAuth_EmptyReaderReturnsShortPacket(t *testing.T) {
|
||||
_, err := mysql.ReadClientAuth(bytes.NewReader(nil))
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, mysql.ErrShortPacket)
|
||||
}
|
||||
|
||||
func TestReadClientAuth_OversizePacketRejected(t *testing.T) {
|
||||
var hdr [packetHeaderSize]byte
|
||||
hdr[0] = 0x01
|
||||
hdr[1] = 0x00
|
||||
hdr[2] = 0x10
|
||||
hdr[3] = 0x01
|
||||
|
||||
r := io.MultiReader(
|
||||
bytes.NewReader(hdr[:]),
|
||||
strings.NewReader(strings.Repeat("X", 70000)),
|
||||
)
|
||||
_, err := mysql.ReadClientAuth(r)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, mysql.ErrPacketTooLarge)
|
||||
}
|
||||
|
||||
func TestBuildAccessDeniedErr_HasCorrectMarkerAndCode(t *testing.T) {
|
||||
pkt, err := mysql.BuildAccessDeniedErr("canary_xyz", "203.0.113.50")
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(pkt), packetHeaderSize)
|
||||
|
||||
require.Equal(
|
||||
t,
|
||||
byte(0xff),
|
||||
pkt[packetHeaderSize],
|
||||
"ERR packet header byte",
|
||||
)
|
||||
code := binary.LittleEndian.Uint16(
|
||||
pkt[packetHeaderSize+1 : packetHeaderSize+3],
|
||||
)
|
||||
require.Equal(
|
||||
t,
|
||||
uint16(1045),
|
||||
code,
|
||||
"MySQL error code 1045 (access denied)",
|
||||
)
|
||||
}
|
||||
|
||||
func TestBuildAccessDeniedErr_HasSQLStateMarkerAnd28000(t *testing.T) {
|
||||
pkt, err := mysql.BuildAccessDeniedErr("canary_xyz", "203.0.113.50")
|
||||
require.NoError(t, err)
|
||||
require.Equal(
|
||||
t,
|
||||
byte('#'),
|
||||
pkt[packetHeaderSize+3],
|
||||
"SQL state marker must be '#'",
|
||||
)
|
||||
require.Equal(
|
||||
t,
|
||||
"28000",
|
||||
string(pkt[packetHeaderSize+4:packetHeaderSize+9]),
|
||||
"SQL state 28000 (invalid authorization)",
|
||||
)
|
||||
}
|
||||
|
||||
func TestBuildAccessDeniedErr_MessageContainsUserAndHost(t *testing.T) {
|
||||
pkt, err := mysql.BuildAccessDeniedErr("canary_abc", "203.0.113.7")
|
||||
require.NoError(t, err)
|
||||
require.Contains(
|
||||
t,
|
||||
string(pkt),
|
||||
`Access denied for user 'canary_abc'@'203.0.113.7' (using password: YES)`,
|
||||
)
|
||||
}
|
||||
|
||||
func TestBuildAccessDeniedErr_SequenceIDIs2(t *testing.T) {
|
||||
pkt, err := mysql.BuildAccessDeniedErr("u", "h")
|
||||
require.NoError(t, err)
|
||||
require.Equal(
|
||||
t,
|
||||
byte(0x02),
|
||||
pkt[3],
|
||||
"ERR packet sequence ID is 2 (after handshake=0 and client auth=1)",
|
||||
)
|
||||
}
|
||||
|
||||
func TestRoundTrip_BuildAndParseUsername(t *testing.T) {
|
||||
cases := []string{
|
||||
"canary_abcdef0123",
|
||||
"canary_xyz",
|
||||
"root",
|
||||
"",
|
||||
"app_user",
|
||||
}
|
||||
for _, name := range cases {
|
||||
name := name
|
||||
t.Run("username="+name, func(t *testing.T) {
|
||||
pkt := buildHandshakeResponse41(t, name)
|
||||
auth, err := mysql.ReadClientAuth(bytes.NewReader(pkt))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, name, auth.Username)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRandomAuthData_ReturnsTwentyBytes(t *testing.T) {
|
||||
d, err := mysql.NewRandomAuthData()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, d, 20)
|
||||
}
|
||||
|
||||
func TestNewRandomAuthData_DistinctCallsProduceDistinctOutputs(t *testing.T) {
|
||||
seen := make(map[[20]byte]struct{})
|
||||
for range 50 {
|
||||
d, err := mysql.NewRandomAuthData()
|
||||
require.NoError(t, err)
|
||||
seen[d] = struct{}{}
|
||||
}
|
||||
require.Greater(
|
||||
t,
|
||||
len(seen),
|
||||
45,
|
||||
"50 calls to NewRandomAuthData should produce near-50 unique values",
|
||||
)
|
||||
}
|
||||
|
||||
func TestNewRandomConnectionID_DistinctCallsProduceDistinctOutputs(
|
||||
t *testing.T,
|
||||
) {
|
||||
seen := make(map[uint32]struct{})
|
||||
for range 50 {
|
||||
id, err := mysql.NewRandomConnectionID()
|
||||
require.NoError(t, err)
|
||||
seen[id] = struct{}{}
|
||||
}
|
||||
require.Greater(t, len(seen), 45)
|
||||
}
|
||||
|
||||
func TestErrors_AreDistinctSentinels(t *testing.T) {
|
||||
require.NotErrorIs(t, mysql.ErrShortPacket, mysql.ErrPacketTooLarge)
|
||||
require.NotErrorIs(t, mysql.ErrPacketTooLarge, mysql.ErrInvalidPayload)
|
||||
require.NotErrorIs(t, mysql.ErrInvalidPayload, mysql.ErrUsernameMissing)
|
||||
}
|
||||
Loading…
Reference in New Issue