1// Go MySQL Driver - A MySQL-Driver for Go's database/sql package 2// 3// Copyright 2016 The Go-MySQL-Driver Authors. All rights reserved. 4// 5// This Source Code Form is subject to the terms of the Mozilla Public 6// License, v. 2.0. If a copy of the MPL was not distributed with this file, 7// You can obtain one at http://mozilla.org/MPL/2.0/. 8 9package mysql 10 11import ( 12 "crypto/tls" 13 "fmt" 14 "net/url" 15 "reflect" 16 "testing" 17 "time" 18) 19 20var testDSNs = []struct { 21 in string 22 out *Config 23}{{ 24 "username:password@protocol(address)/dbname?param=value", 25 &Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, 26}, { 27 "username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", 28 &Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true, ColumnsWithAlias: true}, 29}, { 30 "username:password@protocol(address)/dbname?param=value&columnsWithAlias=true&multiStatements=true", 31 &Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true, ColumnsWithAlias: true, MultiStatements: true}, 32}, { 33 "user@unix(/path/to/socket)/dbname?charset=utf8", 34 &Config{User: "user", Net: "unix", Addr: "/path/to/socket", DBName: "dbname", Params: map[string]string{"charset": "utf8"}, Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, 35}, { 36 "user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", 37 &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", Params: map[string]string{"charset": "utf8"}, Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true, TLSConfig: "true"}, 38}, { 39 "user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", 40 &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", Params: map[string]string{"charset": "utf8mb4,utf8"}, Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true, TLSConfig: "skip-verify"}, 41}, { 42 "user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci&maxAllowedPacket=16777216&tls=false&allowCleartextPasswords=true&parseTime=true&rejectReadOnly=true", 43 &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_unicode_ci", Loc: time.UTC, TLSConfig: "false", AllowCleartextPasswords: true, AllowNativePasswords: true, Timeout: 30 * time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, AllowAllFiles: true, AllowOldPasswords: true, CheckConnLiveness: true, ClientFoundRows: true, MaxAllowedPacket: 16777216, ParseTime: true, RejectReadOnly: true}, 44}, { 45 "user:password@/dbname?allowNativePasswords=false&checkConnLiveness=false&maxAllowedPacket=0", 46 &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: 0, AllowNativePasswords: false, CheckConnLiveness: false}, 47}, { 48 "user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", 49 &Config{User: "user", Passwd: "p@ss(word)", Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:80", DBName: "dbname", Collation: "utf8mb4_general_ci", Loc: time.Local, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, 50}, { 51 "/dbname", 52 &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, 53}, { 54 "@/", 55 &Config{Net: "tcp", Addr: "127.0.0.1:3306", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, 56}, { 57 "/", 58 &Config{Net: "tcp", Addr: "127.0.0.1:3306", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, 59}, { 60 "", 61 &Config{Net: "tcp", Addr: "127.0.0.1:3306", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, 62}, { 63 "user:p@/ssword@/", 64 &Config{User: "user", Passwd: "p@/ssword", Net: "tcp", Addr: "127.0.0.1:3306", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, 65}, { 66 "unix/?arg=%2Fsome%2Fpath.ext", 67 &Config{Net: "unix", Addr: "/tmp/mysql.sock", Params: map[string]string{"arg": "/some/path.ext"}, Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, 68}, { 69 "tcp(127.0.0.1)/dbname", 70 &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, 71}, { 72 "tcp(de:ad:be:ef::ca:fe)/dbname", 73 &Config{Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:3306", DBName: "dbname", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, 74}, 75} 76 77func TestDSNParser(t *testing.T) { 78 for i, tst := range testDSNs { 79 cfg, err := ParseDSN(tst.in) 80 if err != nil { 81 t.Error(err.Error()) 82 } 83 84 // pointer not static 85 cfg.tls = nil 86 87 if !reflect.DeepEqual(cfg, tst.out) { 88 t.Errorf("%d. ParseDSN(%q) mismatch:\ngot %+v\nwant %+v", i, tst.in, cfg, tst.out) 89 } 90 } 91} 92 93func TestDSNParserInvalid(t *testing.T) { 94 var invalidDSNs = []string{ 95 "@net(addr/", // no closing brace 96 "@tcp(/", // no closing brace 97 "tcp(/", // no closing brace 98 "(/", // no closing brace 99 "net(addr)//", // unescaped 100 "User:pass@tcp(1.2.3.4:3306)", // no trailing slash 101 "net()/", // unknown default addr 102 //"/dbname?arg=/some/unescaped/path", 103 } 104 105 for i, tst := range invalidDSNs { 106 if _, err := ParseDSN(tst); err == nil { 107 t.Errorf("invalid DSN #%d. (%s) didn't error!", i, tst) 108 } 109 } 110} 111 112func TestDSNReformat(t *testing.T) { 113 for i, tst := range testDSNs { 114 dsn1 := tst.in 115 cfg1, err := ParseDSN(dsn1) 116 if err != nil { 117 t.Error(err.Error()) 118 continue 119 } 120 cfg1.tls = nil // pointer not static 121 res1 := fmt.Sprintf("%+v", cfg1) 122 123 dsn2 := cfg1.FormatDSN() 124 cfg2, err := ParseDSN(dsn2) 125 if err != nil { 126 t.Error(err.Error()) 127 continue 128 } 129 cfg2.tls = nil // pointer not static 130 res2 := fmt.Sprintf("%+v", cfg2) 131 132 if res1 != res2 { 133 t.Errorf("%d. %q does not match %q", i, res2, res1) 134 } 135 } 136} 137 138func TestDSNServerPubKey(t *testing.T) { 139 baseDSN := "User:password@tcp(localhost:5555)/dbname?serverPubKey=" 140 141 RegisterServerPubKey("testKey", testPubKeyRSA) 142 defer DeregisterServerPubKey("testKey") 143 144 tst := baseDSN + "testKey" 145 cfg, err := ParseDSN(tst) 146 if err != nil { 147 t.Error(err.Error()) 148 } 149 150 if cfg.ServerPubKey != "testKey" { 151 t.Errorf("unexpected cfg.ServerPubKey value: %v", cfg.ServerPubKey) 152 } 153 if cfg.pubKey != testPubKeyRSA { 154 t.Error("pub key pointer doesn't match") 155 } 156 157 // Key is missing 158 tst = baseDSN + "invalid_name" 159 cfg, err = ParseDSN(tst) 160 if err == nil { 161 t.Errorf("invalid name in DSN (%s) but did not error. Got config: %#v", tst, cfg) 162 } 163} 164 165func TestDSNServerPubKeyQueryEscape(t *testing.T) { 166 const name = "&%!:" 167 dsn := "User:password@tcp(localhost:5555)/dbname?serverPubKey=" + url.QueryEscape(name) 168 169 RegisterServerPubKey(name, testPubKeyRSA) 170 defer DeregisterServerPubKey(name) 171 172 cfg, err := ParseDSN(dsn) 173 if err != nil { 174 t.Error(err.Error()) 175 } 176 177 if cfg.pubKey != testPubKeyRSA { 178 t.Error("pub key pointer doesn't match") 179 } 180} 181 182func TestDSNWithCustomTLS(t *testing.T) { 183 baseDSN := "User:password@tcp(localhost:5555)/dbname?tls=" 184 tlsCfg := tls.Config{} 185 186 RegisterTLSConfig("utils_test", &tlsCfg) 187 defer DeregisterTLSConfig("utils_test") 188 189 // Custom TLS is missing 190 tst := baseDSN + "invalid_tls" 191 cfg, err := ParseDSN(tst) 192 if err == nil { 193 t.Errorf("invalid custom TLS in DSN (%s) but did not error. Got config: %#v", tst, cfg) 194 } 195 196 tst = baseDSN + "utils_test" 197 198 // Custom TLS with a server name 199 name := "foohost" 200 tlsCfg.ServerName = name 201 cfg, err = ParseDSN(tst) 202 203 if err != nil { 204 t.Error(err.Error()) 205 } else if cfg.tls.ServerName != name { 206 t.Errorf("did not get the correct TLS ServerName (%s) parsing DSN (%s).", name, tst) 207 } 208 209 // Custom TLS without a server name 210 name = "localhost" 211 tlsCfg.ServerName = "" 212 cfg, err = ParseDSN(tst) 213 214 if err != nil { 215 t.Error(err.Error()) 216 } else if cfg.tls.ServerName != name { 217 t.Errorf("did not get the correct ServerName (%s) parsing DSN (%s).", name, tst) 218 } else if tlsCfg.ServerName != "" { 219 t.Errorf("tlsCfg was mutated ServerName (%s) should be empty parsing DSN (%s).", name, tst) 220 } 221} 222 223func TestDSNTLSConfig(t *testing.T) { 224 expectedServerName := "example.com" 225 dsn := "tcp(example.com:1234)/?tls=true" 226 227 cfg, err := ParseDSN(dsn) 228 if err != nil { 229 t.Error(err.Error()) 230 } 231 if cfg.tls == nil { 232 t.Error("cfg.tls should not be nil") 233 } 234 if cfg.tls.ServerName != expectedServerName { 235 t.Errorf("cfg.tls.ServerName should be %q, got %q (host with port)", expectedServerName, cfg.tls.ServerName) 236 } 237 238 dsn = "tcp(example.com)/?tls=true" 239 cfg, err = ParseDSN(dsn) 240 if err != nil { 241 t.Error(err.Error()) 242 } 243 if cfg.tls == nil { 244 t.Error("cfg.tls should not be nil") 245 } 246 if cfg.tls.ServerName != expectedServerName { 247 t.Errorf("cfg.tls.ServerName should be %q, got %q (host without port)", expectedServerName, cfg.tls.ServerName) 248 } 249} 250 251func TestDSNWithCustomTLSQueryEscape(t *testing.T) { 252 const configKey = "&%!:" 253 dsn := "User:password@tcp(localhost:5555)/dbname?tls=" + url.QueryEscape(configKey) 254 name := "foohost" 255 tlsCfg := tls.Config{ServerName: name} 256 257 RegisterTLSConfig(configKey, &tlsCfg) 258 defer DeregisterTLSConfig(configKey) 259 260 cfg, err := ParseDSN(dsn) 261 262 if err != nil { 263 t.Error(err.Error()) 264 } else if cfg.tls.ServerName != name { 265 t.Errorf("did not get the correct TLS ServerName (%s) parsing DSN (%s).", name, dsn) 266 } 267} 268 269func TestDSNUnsafeCollation(t *testing.T) { 270 _, err := ParseDSN("/dbname?collation=gbk_chinese_ci&interpolateParams=true") 271 if err != errInvalidDSNUnsafeCollation { 272 t.Errorf("expected %v, got %v", errInvalidDSNUnsafeCollation, err) 273 } 274 275 _, err = ParseDSN("/dbname?collation=gbk_chinese_ci&interpolateParams=false") 276 if err != nil { 277 t.Errorf("expected %v, got %v", nil, err) 278 } 279 280 _, err = ParseDSN("/dbname?collation=gbk_chinese_ci") 281 if err != nil { 282 t.Errorf("expected %v, got %v", nil, err) 283 } 284 285 _, err = ParseDSN("/dbname?collation=ascii_bin&interpolateParams=true") 286 if err != nil { 287 t.Errorf("expected %v, got %v", nil, err) 288 } 289 290 _, err = ParseDSN("/dbname?collation=latin1_german1_ci&interpolateParams=true") 291 if err != nil { 292 t.Errorf("expected %v, got %v", nil, err) 293 } 294 295 _, err = ParseDSN("/dbname?collation=utf8_general_ci&interpolateParams=true") 296 if err != nil { 297 t.Errorf("expected %v, got %v", nil, err) 298 } 299 300 _, err = ParseDSN("/dbname?collation=utf8mb4_general_ci&interpolateParams=true") 301 if err != nil { 302 t.Errorf("expected %v, got %v", nil, err) 303 } 304} 305 306func TestParamsAreSorted(t *testing.T) { 307 expected := "/dbname?interpolateParams=true&foobar=baz&quux=loo" 308 cfg := NewConfig() 309 cfg.DBName = "dbname" 310 cfg.InterpolateParams = true 311 cfg.Params = map[string]string{ 312 "quux": "loo", 313 "foobar": "baz", 314 } 315 actual := cfg.FormatDSN() 316 if actual != expected { 317 t.Errorf("generic Config.Params were not sorted: want %#v, got %#v", expected, actual) 318 } 319} 320 321func TestCloneConfig(t *testing.T) { 322 RegisterServerPubKey("testKey", testPubKeyRSA) 323 defer DeregisterServerPubKey("testKey") 324 325 expectedServerName := "example.com" 326 dsn := "tcp(example.com:1234)/?tls=true&foobar=baz&serverPubKey=testKey" 327 cfg, err := ParseDSN(dsn) 328 if err != nil { 329 t.Fatal(err.Error()) 330 } 331 332 cfg2 := cfg.Clone() 333 if cfg == cfg2 { 334 t.Errorf("Config.Clone did not create a separate config struct") 335 } 336 337 if cfg2.tls.ServerName != expectedServerName { 338 t.Errorf("cfg.tls.ServerName should be %q, got %q (host with port)", expectedServerName, cfg.tls.ServerName) 339 } 340 341 cfg2.tls.ServerName = "example2.com" 342 if cfg.tls.ServerName == cfg2.tls.ServerName { 343 t.Errorf("changed cfg.tls.Server name should not propagate to original Config") 344 } 345 346 if _, ok := cfg2.Params["foobar"]; !ok { 347 t.Errorf("cloned Config is missing custom params") 348 } 349 350 delete(cfg2.Params, "foobar") 351 352 if _, ok := cfg.Params["foobar"]; !ok { 353 t.Errorf("custom params in cloned Config should not propagate to original Config") 354 } 355 356 if !reflect.DeepEqual(cfg.pubKey, cfg2.pubKey) { 357 t.Errorf("public key in Config should be identical") 358 } 359} 360 361func TestNormalizeTLSConfig(t *testing.T) { 362 tt := []struct { 363 tlsConfig string 364 want *tls.Config 365 }{ 366 {"", nil}, 367 {"false", nil}, 368 {"true", &tls.Config{ServerName: "myserver"}}, 369 {"skip-verify", &tls.Config{InsecureSkipVerify: true}}, 370 {"preferred", &tls.Config{InsecureSkipVerify: true}}, 371 {"test_tls_config", &tls.Config{ServerName: "myServerName"}}, 372 } 373 374 RegisterTLSConfig("test_tls_config", &tls.Config{ServerName: "myServerName"}) 375 defer func() { DeregisterTLSConfig("test_tls_config") }() 376 377 for _, tc := range tt { 378 t.Run(tc.tlsConfig, func(t *testing.T) { 379 cfg := &Config{ 380 Addr: "myserver:3306", 381 TLSConfig: tc.tlsConfig, 382 } 383 384 cfg.normalize() 385 386 if cfg.tls == nil { 387 if tc.want != nil { 388 t.Fatal("wanted a tls config but got nil instead") 389 } 390 return 391 } 392 393 if cfg.tls.ServerName != tc.want.ServerName { 394 t.Errorf("tls.ServerName doesn't match (want: '%s', got: '%s')", 395 tc.want.ServerName, cfg.tls.ServerName) 396 } 397 if cfg.tls.InsecureSkipVerify != tc.want.InsecureSkipVerify { 398 t.Errorf("tls.InsecureSkipVerify doesn't match (want: %T, got :%T)", 399 tc.want.InsecureSkipVerify, cfg.tls.InsecureSkipVerify) 400 } 401 }) 402 } 403} 404 405func BenchmarkParseDSN(b *testing.B) { 406 b.ReportAllocs() 407 408 for i := 0; i < b.N; i++ { 409 for _, tst := range testDSNs { 410 if _, err := ParseDSN(tst.in); err != nil { 411 b.Error(err.Error()) 412 } 413 } 414 } 415} 416