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