From 71151ad03d8e07ad11950f7fa2136cbc0d8b30a3 Mon Sep 17 00:00:00 2001 From: Vasily Fedoseyev Date: Thu, 18 Jan 2018 20:18:52 +0300 Subject: [PATCH 01/16] Support for sending connection attributes --- AUTHORS | 1 + README.md | 10 ++++++++++ driver_test.go | 39 +++++++++++++++++++++++++++++++++++++++ dsn.go | 43 +++++++++++++++++++++++++++++++++++++++++++ dsn_test.go | 17 +++++++++++++++++ packets.go | 30 +++++++++++++++++++++++++++++- utils.go | 6 ++++++ 7 files changed, 145 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 73ff68fbc..7124f8bd4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -72,6 +72,7 @@ Shuode Li Soroush Pour Stan Putrya Stanley Gunawan +Vasily Fedoseyev Xiangyu Hu Xiaobing Jiang Xiuming Chen diff --git a/README.md b/README.md index 2e9b07eeb..59b87ba07 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,16 @@ SELECT u.id FROM users as u will return `u.id` instead of just `id` if `columnsWithAlias=true`. +##### `connectionAttributes` + +``` +Type: map +Valid Values: comma-separated list of attribute:value pairs +Default: empty +``` + +Allows setting of connection attributes, for example `connectionAttributes=program_name:YourProgramName` will show `YourProgramName` in `Program` field of connections list of Mysql Workbench. + ##### `interpolateParams` ``` diff --git a/driver_test.go b/driver_test.go index f2bf344e5..c3156c47b 100644 --- a/driver_test.go +++ b/driver_test.go @@ -2074,6 +2074,45 @@ func TestEmptyPassword(t *testing.T) { if !strings.HasPrefix(err.Error(), "Error 1045") { t.Fatal(err.Error()) } + } +} + +func TestConnectionAttributes(t *testing.T) { + if !available { + t.Skipf("MySQL server not running on %s", netAddr) + } + + db, err := sql.Open("mysql", dsn+"&connectionAttributes=program_name:GoTest,foo:bar") + if err != nil { + t.Fatalf("error connecting: %s", err.Error()) + } + defer db.Close() + dbt := &DBTest{t, db} + + rows, err := dbt.db.Query("SELECT program_name FROM sys.processlist WHERE db=?", dbname) + if err != nil { + dbt.Skip("server probably does not support program_name in sys.processlist") + } + + if rows.Next() { + var str string + rows.Scan(&str) + if "GoTest" != str { + dbt.Errorf("GoTest != %s", str) + } + } else { + dbt.Error("no data") + } + + rows = dbt.mustQuery("select attr_value from performance_schema.session_account_connect_attrs where processlist_id=CONNECTION_ID() and attr_name='foo'") + if rows.Next() { + var str string + rows.Scan(&str) + if "bar" != str { + dbt.Errorf("bar != %s", str) + } + } else { + dbt.Error("no data") } } diff --git a/dsn.go b/dsn.go index be014babe..4a196fd3c 100644 --- a/dsn.go +++ b/dsn.go @@ -39,6 +39,7 @@ type Config struct { Addr string // Network address (requires Net) DBName string // Database name Params map[string]string // Connection parameters + Attributes map[string]string // Connection attributes Collation string // Connection collation Loc *time.Location // Location for time.Time values MaxAllowedPacket int // Max packet size allowed @@ -308,6 +309,30 @@ func (cfg *Config) FormatDSN() string { } + if len(cfg.Attributes) > 0 { + // connectionAttributes=program_name:Login Server,other_name:other + if hasParam { + buf.WriteString("&connectionAttributes=") + } else { + hasParam = true + buf.WriteString("?connectionAttributes=") + } + + var attr_names []string + for attr_name := range cfg.Attributes { + attr_names = append(attr_names, attr_name) + } + sort.Strings(attr_names) + for index, attr_name := range attr_names { + if index > 0 { + buf.WriteByte(',') + } + buf.WriteString(attr_name) + buf.WriteByte(':') + buf.WriteString(url.QueryEscape(cfg.Attributes[attr_name])) + } + } + // other params if cfg.Params != nil { var params []string @@ -588,6 +613,24 @@ func parseDSNParams(cfg *Config, params string) (err error) { if err != nil { return } + case "connectionAttributes": + if cfg.Attributes == nil { + cfg.Attributes = make(map[string]string) + } + + var attributes string + if attributes, err = url.QueryUnescape(value); err != nil { + return + } + + // program_name:Name,foo:bar + for _, attr_str := range strings.Split(attributes, ",") { + attr := strings.SplitN(attr_str, ":", 2) + if len(attr) != 2 { + continue + } + cfg.Attributes[attr[0]] = attr[1] + } default: // lazy init if cfg.Params == nil { diff --git a/dsn_test.go b/dsn_test.go index 1cd095496..cca33f25d 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -71,6 +71,9 @@ var testDSNs = []struct { }, { "tcp(de:ad:be:ef::ca:fe)/dbname", &Config{Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:3306", DBName: "dbname", Collation: "utf8_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true}, +}, { + "tcp(127.0.0.1)/dbname?connectionAttributes=program_name:SomeService", + &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Attributes: map[string]string{"program_name": "SomeService"}, Collation: "utf8_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true}, }, } @@ -318,6 +321,20 @@ func TestParamsAreSorted(t *testing.T) { } } +func TestAttributesAreSorted(t *testing.T) { + expected := "/dbname?connectionAttributes=p1:v1,p2:v2" + cfg := NewConfig() + cfg.DBName = "dbname" + cfg.Attributes = map[string]string{ + "p2": "v2", + "p1": "v1", + } + actual := cfg.FormatDSN() + if actual != expected { + t.Errorf("generic Config.Attributes were not sorted: want %#v, got %#v", expected, actual) + } +} + func BenchmarkParseDSN(b *testing.B) { b.ReportAllocs() diff --git a/packets.go b/packets.go index f99934e73..359a187b5 100644 --- a/packets.go +++ b/packets.go @@ -202,10 +202,15 @@ func (mc *mysqlConn) readHandshakePacket() ([]byte, string, error) { if len(data) > pos { // character set [1 byte] // status flags [2 bytes] + pos += 1 + 2 + // capability flags (upper 2 bytes) [2 bytes] + mc.flags |= clientFlag(uint32(binary.LittleEndian.Uint16(data[pos:pos+2])) << 16) + pos += 2 + // length of auth-plugin-data [1 byte] // reserved (all [00]) [10 bytes] - pos += 1 + 2 + 2 + 1 + 10 + pos += 1 + 10 // second part of the password cipher [mininum 13 bytes], // where len=MAX(13, length of auth-plugin-data - 8) @@ -284,6 +289,24 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, addNUL bool, pktLen++ } + connectAttrsBuf := make([]byte, 0, 100) + if mc.flags&clientConnectAttrs != 0 { + clientFlags |= clientConnectAttrs + connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte("_client_name")) + connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte("go-mysql-driver")) + + for k, v := range mc.cfg.Attributes { + if k == "_client_name" { + // do not allow overwriting reserved values + continue + } + connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte(k)) + connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte(v)) + } + connectAttrsBuf = appendLengthEncodedString(make([]byte, 0, 100), connectAttrsBuf) + pktLen += len(connectAttrsBuf) + } + // To specify a db name if n := len(mc.cfg.DBName); n > 0 { clientFlags |= clientConnectWithDB @@ -367,6 +390,11 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, addNUL bool, pos += copy(data[pos:], plugin) data[pos] = 0x00 + pos++ + + if clientFlags&clientConnectAttrs != 0 { + pos += copy(data[pos:], connectAttrsBuf) + } // Send Auth packet return mc.writePacket(data) diff --git a/utils.go b/utils.go index cb3650bb9..bb1be6f1a 100644 --- a/utils.go +++ b/utils.go @@ -466,6 +466,12 @@ func skipLengthEncodedString(b []byte) (int, error) { return n, io.EOF } +// encodes a bytes slice with prepended length-encoded size and appends it to the given bytes slice +func appendLengthEncodedString(b []byte, str []byte) []byte { + b = appendLengthEncodedInteger(b, uint64(len(str))) + return append(b, str...) +} + // returns the number read, whether the value is NULL and the number of bytes read func readLengthEncodedInteger(b []byte) (uint64, bool, int) { // See issue #349 From 9057689b94d0c4bbcf8f27d0d7353325ee610d6d Mon Sep 17 00:00:00 2001 From: Vasily Fedoseyev Date: Fri, 15 Jun 2018 20:49:12 +0300 Subject: [PATCH 02/16] Rename connectionAttributes to connectAttrs and mention performance_schema in readme --- .travis.yml | 1 + README.md | 5 ++--- driver_test.go | 20 ++++++++++++++------ dsn.go | 28 ++++++++++++++-------------- dsn_test.go | 10 +++++----- packets.go | 2 +- 6 files changed, 37 insertions(+), 29 deletions(-) diff --git a/.travis.yml b/.travis.yml index 47dd289a0..7a4113dbc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ before_script: - sudo service mysql restart - .travis/wait_mysql.sh - mysql -e 'create database gotest;' + - mysql -e 'show databases;' matrix: include: diff --git a/README.md b/README.md index 59b87ba07..9c712f4fe 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ SELECT u.id FROM users as u will return `u.id` instead of just `id` if `columnsWithAlias=true`. -##### `connectionAttributes` +##### `connectAttrs` ``` Type: map @@ -212,7 +212,7 @@ Valid Values: comma-separated list of attribute:value pairs Default: empty ``` -Allows setting of connection attributes, for example `connectionAttributes=program_name:YourProgramName` will show `YourProgramName` in `Program` field of connections list of Mysql Workbench. +Allows setting of connection attributes, for example `connectAttrs=program_name:YourProgramName` will show `YourProgramName` in `Program` field of connections list of Mysql Workbench, if your server supports it (requires `performance_schema` to be supported and enabled). ##### `interpolateParams` @@ -497,4 +497,3 @@ Please read the [MPL 2.0 FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/) if you You can read the full terms here: [LICENSE](https://raw.github.com/go-sql-driver/mysql/master/LICENSE). ![Go Gopher and MySQL Dolphin](https://raw.github.com/wiki/go-sql-driver/mysql/go-mysql-driver_m.jpg "Golang Gopher transporting the MySQL Dolphin in a wheelbarrow") - diff --git a/driver_test.go b/driver_test.go index c3156c47b..e76bc9dae 100644 --- a/driver_test.go +++ b/driver_test.go @@ -2077,21 +2077,29 @@ func TestEmptyPassword(t *testing.T) { } } -func TestConnectionAttributes(t *testing.T) { +func TestConnectAttrs(t *testing.T) { if !available { t.Skipf("MySQL server not running on %s", netAddr) } - db, err := sql.Open("mysql", dsn+"&connectionAttributes=program_name:GoTest,foo:bar") + db, err := sql.Open("mysql", dsn+"&connectAttrs=program_name:GoTest,foo:bar") if err != nil { t.Fatalf("error connecting: %s", err.Error()) } defer db.Close() dbt := &DBTest{t, db} - rows, err := dbt.db.Query("SELECT program_name FROM sys.processlist WHERE db=?", dbname) + // performance_schema seems to be updated with a delay in some conditions, so first see if we are in list: + rows := dbt.mustQuery("SELECT * FROM INFORMATION_SCHEMA.PROCESSLIST where ID=CONNECTION_ID()") + if rows.Next() { + } else { + dbt.Error("no data in processlist") + } + + rows, err = dbt.db.Query("select attr_value from performance_schema.session_account_connect_attrs where processlist_id=CONNECTION_ID() and attr_name='program_name'") if err != nil { - dbt.Skip("server probably does not support program_name in sys.processlist") + fmt.Println(err) + dbt.Skip("server probably does not support performance_schema.session_account_connect_attrs") } if rows.Next() { @@ -2101,7 +2109,7 @@ func TestConnectionAttributes(t *testing.T) { dbt.Errorf("GoTest != %s", str) } } else { - dbt.Error("no data") + dbt.Error("no data for program_name") } rows = dbt.mustQuery("select attr_value from performance_schema.session_account_connect_attrs where processlist_id=CONNECTION_ID() and attr_name='foo'") @@ -2112,7 +2120,7 @@ func TestConnectionAttributes(t *testing.T) { dbt.Errorf("bar != %s", str) } } else { - dbt.Error("no data") + dbt.Error("no data for custom attribute") } } diff --git a/dsn.go b/dsn.go index 4a196fd3c..d0098ff71 100644 --- a/dsn.go +++ b/dsn.go @@ -39,7 +39,7 @@ type Config struct { Addr string // Network address (requires Net) DBName string // Database name Params map[string]string // Connection parameters - Attributes map[string]string // Connection attributes + ConnectAttrs map[string]string // Connection attributes Collation string // Connection collation Loc *time.Location // Location for time.Time values MaxAllowedPacket int // Max packet size allowed @@ -309,17 +309,17 @@ func (cfg *Config) FormatDSN() string { } - if len(cfg.Attributes) > 0 { - // connectionAttributes=program_name:Login Server,other_name:other + if len(cfg.ConnectAttrs) > 0 { + // connectAttrs=program_name:Login Server,other_name:other if hasParam { - buf.WriteString("&connectionAttributes=") + buf.WriteString("&connectAttrs=") } else { hasParam = true - buf.WriteString("?connectionAttributes=") + buf.WriteString("?connectAttrs=") } var attr_names []string - for attr_name := range cfg.Attributes { + for attr_name := range cfg.ConnectAttrs { attr_names = append(attr_names, attr_name) } sort.Strings(attr_names) @@ -329,7 +329,7 @@ func (cfg *Config) FormatDSN() string { } buf.WriteString(attr_name) buf.WriteByte(':') - buf.WriteString(url.QueryEscape(cfg.Attributes[attr_name])) + buf.WriteString(url.QueryEscape(cfg.ConnectAttrs[attr_name])) } } @@ -613,23 +613,23 @@ func parseDSNParams(cfg *Config, params string) (err error) { if err != nil { return } - case "connectionAttributes": - if cfg.Attributes == nil { - cfg.Attributes = make(map[string]string) + case "connectAttrs": + if cfg.ConnectAttrs == nil { + cfg.ConnectAttrs = make(map[string]string) } - var attributes string - if attributes, err = url.QueryUnescape(value); err != nil { + var ConnectAttrs string + if ConnectAttrs, err = url.QueryUnescape(value); err != nil { return } // program_name:Name,foo:bar - for _, attr_str := range strings.Split(attributes, ",") { + for _, attr_str := range strings.Split(ConnectAttrs, ",") { attr := strings.SplitN(attr_str, ":", 2) if len(attr) != 2 { continue } - cfg.Attributes[attr[0]] = attr[1] + cfg.ConnectAttrs[attr[0]] = attr[1] } default: // lazy init diff --git a/dsn_test.go b/dsn_test.go index cca33f25d..dae4c24e0 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -72,8 +72,8 @@ var testDSNs = []struct { "tcp(de:ad:be:ef::ca:fe)/dbname", &Config{Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:3306", DBName: "dbname", Collation: "utf8_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true}, }, { - "tcp(127.0.0.1)/dbname?connectionAttributes=program_name:SomeService", - &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Attributes: map[string]string{"program_name": "SomeService"}, Collation: "utf8_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true}, + "tcp(127.0.0.1)/dbname?connectAttrs=program_name:SomeService", + &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", ConnectAttrs: map[string]string{"program_name": "SomeService"}, Collation: "utf8_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true}, }, } @@ -322,16 +322,16 @@ func TestParamsAreSorted(t *testing.T) { } func TestAttributesAreSorted(t *testing.T) { - expected := "/dbname?connectionAttributes=p1:v1,p2:v2" + expected := "/dbname?connectAttrs=p1:v1,p2:v2" cfg := NewConfig() cfg.DBName = "dbname" - cfg.Attributes = map[string]string{ + cfg.ConnectAttrs = map[string]string{ "p2": "v2", "p1": "v1", } actual := cfg.FormatDSN() if actual != expected { - t.Errorf("generic Config.Attributes were not sorted: want %#v, got %#v", expected, actual) + t.Errorf("generic Config.ConnectAttrs were not sorted: want %#v, got %#v", expected, actual) } } diff --git a/packets.go b/packets.go index 359a187b5..06c87528e 100644 --- a/packets.go +++ b/packets.go @@ -295,7 +295,7 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, addNUL bool, connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte("_client_name")) connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte("go-mysql-driver")) - for k, v := range mc.cfg.Attributes { + for k, v := range mc.cfg.ConnectAttrs { if k == "_client_name" { // do not allow overwriting reserved values continue From 71a1948d660dd0885c37a1ec7e4f161e34d15d41 Mon Sep 17 00:00:00 2001 From: Vasily Fedoseyev Date: Fri, 15 Jun 2018 20:58:56 +0300 Subject: [PATCH 03/16] Fix formatting --- driver_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/driver_test.go b/driver_test.go index e76bc9dae..6bdd74a6c 100644 --- a/driver_test.go +++ b/driver_test.go @@ -2074,7 +2074,7 @@ func TestEmptyPassword(t *testing.T) { if !strings.HasPrefix(err.Error(), "Error 1045") { t.Fatal(err.Error()) } - } + } } func TestConnectAttrs(t *testing.T) { @@ -2089,16 +2089,16 @@ func TestConnectAttrs(t *testing.T) { defer db.Close() dbt := &DBTest{t, db} - // performance_schema seems to be updated with a delay in some conditions, so first see if we are in list: - rows := dbt.mustQuery("SELECT * FROM INFORMATION_SCHEMA.PROCESSLIST where ID=CONNECTION_ID()") - if rows.Next() { - } else { - dbt.Error("no data in processlist") - } + // performance_schema seems to be updated with a delay in some conditions, so first see if we are in list: + rows := dbt.mustQuery("SELECT * FROM INFORMATION_SCHEMA.PROCESSLIST where ID=CONNECTION_ID()") + if rows.Next() { + } else { + dbt.Error("no data in processlist") + } rows, err = dbt.db.Query("select attr_value from performance_schema.session_account_connect_attrs where processlist_id=CONNECTION_ID() and attr_name='program_name'") if err != nil { - fmt.Println(err) + fmt.Println(err) dbt.Skip("server probably does not support performance_schema.session_account_connect_attrs") } From 7038d92bd2f294d64a0ebb7db30ae48cfa414f6c Mon Sep 17 00:00:00 2001 From: Vasily Fedoseyev Date: Sat, 16 Jun 2018 04:28:50 +0300 Subject: [PATCH 04/16] Fix excessive null-termination for auth data in handshake --- auth_test.go | 30 +++++++++++++++++++----------- const.go | 4 +++- packets.go | 38 ++++++++++++++++++++++++++------------ 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/auth_test.go b/auth_test.go index 407363be4..040a016f7 100644 --- a/auth_test.go +++ b/auth_test.go @@ -13,6 +13,7 @@ import ( "crypto/rsa" "crypto/tls" "crypto/x509" + "encoding/hex" "encoding/pem" "fmt" "testing" @@ -363,13 +364,16 @@ func TestAuthFastCleartextPassword(t *testing.T) { } // check written auth response - authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1 + authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) authRespEnd := authRespStart + 1 + len(authResp) - writtenAuthRespLen := conn.written[authRespStart] writtenAuthResp := conn.written[authRespStart+1 : authRespEnd] - expectedAuthResp := []byte{115, 101, 99, 114, 101, 116} - if writtenAuthRespLen != 6 || !bytes.Equal(writtenAuthResp, expectedAuthResp) { - t.Fatalf("unexpected written auth response (%d bytes): %v", writtenAuthRespLen, writtenAuthResp) + expectedAuthResp := []byte("secret") + if !bytes.Equal(writtenAuthResp, expectedAuthResp) { + t.Fatalf("unexpected written auth response:\n%s\nExpected:\n%s\n", + hex.Dump(writtenAuthResp), hex.Dump(expectedAuthResp)) + } + if conn.written[authRespEnd] != 0 { + t.Fatalf("Expected null-terminated") } conn.written = nil @@ -683,14 +687,18 @@ func TestAuthFastSHA256PasswordSecure(t *testing.T) { } // check written auth response - authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1 - authRespEnd := authRespStart + 1 + len(authResp) + 1 - writtenAuthRespLen := conn.written[authRespStart] + authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + authRespEnd := authRespStart + 1 + len(authResp) writtenAuthResp := conn.written[authRespStart+1 : authRespEnd] - expectedAuthResp := []byte{115, 101, 99, 114, 101, 116, 0} - if writtenAuthRespLen != 6 || !bytes.Equal(writtenAuthResp, expectedAuthResp) { - t.Fatalf("unexpected written auth response (%d bytes): %v", writtenAuthRespLen, writtenAuthResp) + expectedAuthResp := []byte("secret") + if !bytes.Equal(writtenAuthResp, expectedAuthResp) { + t.Fatalf("unexpected written auth response:\n%s\nExpected:\n%s\n", + hex.Dump(writtenAuthResp), hex.Dump(expectedAuthResp)) } + if conn.written[authRespEnd] != 0 { + t.Fatalf("Expected null-terminated") + } + conn.written = nil // auth response (OK) diff --git a/const.go b/const.go index b1e6b85ef..6118a5e07 100644 --- a/const.go +++ b/const.go @@ -46,7 +46,7 @@ const ( clientIgnoreSIGPIPE clientTransactions clientReserved - clientSecureConn + clientSecureConn // reserved2 in 8.0 clientMultiStatements clientMultiResults clientPSMultiResults @@ -56,6 +56,8 @@ const ( clientCanHandleExpiredPasswords clientSessionTrack clientDeprecateEOF + clientSslVerifyServerCert clientFlag = 1 << 30 + clientRememberOptions clientFlag = 1 << 31 ) const ( diff --git a/packets.go b/packets.go index 06c87528e..13f85fe89 100644 --- a/packets.go +++ b/packets.go @@ -251,10 +251,9 @@ func (mc *mysqlConn) readHandshakePacket() ([]byte, string, error) { // Client Authentication Packet // http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeResponse -func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, addNUL bool, plugin string) error { +func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, insecureAuth bool, plugin string) error { // Adjust client flags based on server support clientFlags := clientProtocol41 | - clientSecureConn | clientLongPassword | clientTransactions | clientLocalFiles | @@ -275,17 +274,21 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, addNUL bool, clientFlags |= clientMultiStatements } + if !insecureAuth { + clientFlags |= clientSecureConn + } + // encode length of the auth plugin data var authRespLEIBuf [9]byte authRespLEI := appendLengthEncodedInteger(authRespLEIBuf[:0], uint64(len(authResp))) - if len(authRespLEI) > 1 { + if len(authRespLEI) > 1 && clientFlags&clientSecureConn != 0 { // if the length can not be written in 1 byte, it must be written as a // length encoded integer clientFlags |= clientPluginAuthLenEncClientData } pktLen := 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1 + len(authRespLEI) + len(authResp) + 21 + 1 - if addNUL { + if clientFlags&clientSecureConn == 0 || clientFlags&clientPluginAuthLenEncClientData == 0 { pktLen++ } @@ -308,7 +311,7 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, addNUL bool, } // To specify a db name - if n := len(mc.cfg.DBName); n > 0 { + if n := len(mc.cfg.DBName); mc.flags&clientConnectWithDB != 0 && n > 0 { clientFlags |= clientConnectWithDB pktLen += n + 1 } @@ -373,25 +376,36 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, addNUL bool, data[pos] = 0x00 pos++ - // Auth Data [length encoded integer] - pos += copy(data[pos:], authRespLEI) + // Auth Data [length encoded integer + data] if clientPluginAuthLenEncClientData + // clientSecureConn => 1 byte len + data + // else null-terminated + if clientFlags&clientPluginAuthLenEncClientData != 0 { + pos += copy(data[pos:], authRespLEI) + } else if clientFlags&clientSecureConn != 0 { + data[pos] = uint8(len(authResp)) + pos++ + } pos += copy(data[pos:], authResp) - if addNUL { + if clientFlags&clientSecureConn == 0 && clientFlags&clientPluginAuthLenEncClientData == 0 { data[pos] = 0x00 pos++ } // Databasename [null terminated string] - if len(mc.cfg.DBName) > 0 { + if clientFlags&clientConnectWithDB != 0 { pos += copy(data[pos:], mc.cfg.DBName) data[pos] = 0x00 pos++ } - pos += copy(data[pos:], plugin) - data[pos] = 0x00 - pos++ + // auth plugin name [null terminated string] + if clientFlags&clientPluginAuth != 0 { + pos += copy(data[pos:], plugin) + data[pos] = 0x00 + pos++ + } + // connection attributes [lenenc-int total + lenenc-str key-value pairs] if clientFlags&clientConnectAttrs != 0 { pos += copy(data[pos:], connectAttrsBuf) } From 151e4dc1053998c2e4eb76398c264f098321d574 Mon Sep 17 00:00:00 2001 From: Vasily Fedoseyev Date: Sat, 16 Jun 2018 05:00:21 +0300 Subject: [PATCH 05/16] Use performance_schema.session_connect_attrs in test --- driver_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/driver_test.go b/driver_test.go index 6bdd74a6c..bcf02e841 100644 --- a/driver_test.go +++ b/driver_test.go @@ -2096,10 +2096,10 @@ func TestConnectAttrs(t *testing.T) { dbt.Error("no data in processlist") } - rows, err = dbt.db.Query("select attr_value from performance_schema.session_account_connect_attrs where processlist_id=CONNECTION_ID() and attr_name='program_name'") + rows, err = dbt.db.Query("select attr_value from performance_schema.session_connect_attrs where processlist_id=CONNECTION_ID() and attr_name='program_name'") if err != nil { fmt.Println(err) - dbt.Skip("server probably does not support performance_schema.session_account_connect_attrs") + dbt.Skip("server probably does not support performance_schema.session_connect_attrs") } if rows.Next() { @@ -2112,7 +2112,7 @@ func TestConnectAttrs(t *testing.T) { dbt.Error("no data for program_name") } - rows = dbt.mustQuery("select attr_value from performance_schema.session_account_connect_attrs where processlist_id=CONNECTION_ID() and attr_name='foo'") + rows = dbt.mustQuery("select attr_value from performance_schema.session_connect_attrs where processlist_id=CONNECTION_ID() and attr_name='foo'") if rows.Next() { var str string rows.Scan(&str) From 26b97bad9d78a6028276247622bcfcf64f491b14 Mon Sep 17 00:00:00 2001 From: Vasily Fedoseyev Date: Sat, 16 Jun 2018 12:22:53 +0300 Subject: [PATCH 06/16] Skip connectAttrs test if performance_schema is disabled --- driver_test.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/driver_test.go b/driver_test.go index bcf02e841..e45441efa 100644 --- a/driver_test.go +++ b/driver_test.go @@ -2089,17 +2089,20 @@ func TestConnectAttrs(t *testing.T) { defer db.Close() dbt := &DBTest{t, db} - // performance_schema seems to be updated with a delay in some conditions, so first see if we are in list: - rows := dbt.mustQuery("SELECT * FROM INFORMATION_SCHEMA.PROCESSLIST where ID=CONNECTION_ID()") + rows := dbt.mustQuery("SHOW VARIABLES LIKE 'performance_schema'") if rows.Next() { + var var_name, value string + rows.Scan(&var_name, &value) + if value != "ON" { + t.Skip("performance_schema is disabled") + } } else { - dbt.Error("no data in processlist") + t.Skip("no performance_schema variable in mysql") } rows, err = dbt.db.Query("select attr_value from performance_schema.session_connect_attrs where processlist_id=CONNECTION_ID() and attr_name='program_name'") if err != nil { - fmt.Println(err) - dbt.Skip("server probably does not support performance_schema.session_connect_attrs") + dbt.Skipf("server probably does not support performance_schema.session_connect_attrs: %s", err) } if rows.Next() { From e8f4d0dad692c3e86c8d389b21be8a6ab306d166 Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Sat, 31 Jul 2021 18:59:03 +0200 Subject: [PATCH 07/16] Fix ./packets.go:308: undefined: insecureAuth --- packets.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packets.go b/packets.go index 7d1134aa6..545bd7b05 100644 --- a/packets.go +++ b/packets.go @@ -285,6 +285,7 @@ func (mc *mysqlConn) readHandshakePacket() (data []byte, plugin string, err erro func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string) error { // Adjust client flags based on server support clientFlags := clientProtocol41 | + clientSecureConn | clientLongPassword | clientTransactions | clientLocalFiles | @@ -305,10 +306,6 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string clientFlags |= clientMultiStatements } - if !insecureAuth { - clientFlags |= clientSecureConn - } - // encode length of the auth plugin data var authRespLEIBuf [9]byte authRespLen := len(authResp) From aa0e7a0b03f618459bb8255f154035a022612c2e Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Sat, 31 Jul 2021 19:04:11 +0200 Subject: [PATCH 08/16] Fix unit test TestDSNParser for connection attributes --- dsn_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsn_test.go b/dsn_test.go index 728241e32..c34512c48 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -73,7 +73,7 @@ var testDSNs = []struct { &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}, }, { "tcp(127.0.0.1)/dbname?connectAttrs=program_name:SomeService", - &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", ConnectAttrs: map[string]string{"program_name": "SomeService"}, Collation: "utf8_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true}, + &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", ConnectAttrs: map[string]string{"program_name": "SomeService"}, Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, }, } From 2f9b253584eaa8dd34fee067168cb4fd1d65017c Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Sat, 31 Jul 2021 21:18:35 +0200 Subject: [PATCH 09/16] Make SQL keywords uppercase --- driver_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/driver_test.go b/driver_test.go index 949d7f332..062ff617f 100644 --- a/driver_test.go +++ b/driver_test.go @@ -2212,7 +2212,7 @@ func TestConnectAttrs(t *testing.T) { t.Skip("no performance_schema variable in mysql") } - rows, err = dbt.db.Query("select attr_value from performance_schema.session_connect_attrs where processlist_id=CONNECTION_ID() and attr_name='program_name'") + rows, err = dbt.db.Query("SELECT attr_value FROM performance_schema.session_connect_attrs WHERE processlist_id=CONNECTION_ID() AND attr_name='program_name'") if err != nil { dbt.Skipf("server probably does not support performance_schema.session_connect_attrs: %s", err) } @@ -2227,7 +2227,7 @@ func TestConnectAttrs(t *testing.T) { dbt.Error("no data for program_name") } - rows = dbt.mustQuery("select attr_value from performance_schema.session_connect_attrs where processlist_id=CONNECTION_ID() and attr_name='foo'") + rows = dbt.mustQuery("SELECT attr_value FROM performance_schema.session_connect_attrs WHERE processlist_id=CONNECTION_ID() AND attr_name='foo'") if rows.Next() { var str string rows.Scan(&str) From 3ab2571733bb58fda4512de540e1be412fde5996 Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Fri, 17 Dec 2021 18:29:25 +0100 Subject: [PATCH 10/16] Changed "_client_name" attribute from "go-mysql-driver" to "github.com/go-sql-driver/mysql" --- packets.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packets.go b/packets.go index 545bd7b05..8e50a8e4b 100644 --- a/packets.go +++ b/packets.go @@ -325,7 +325,7 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string if mc.flags&clientConnectAttrs != 0 { clientFlags |= clientConnectAttrs connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte("_client_name")) - connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte("go-mysql-driver")) + connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte("github.com/go-sql-driver/mysql")) for k, v := range mc.cfg.ConnectAttrs { if k == "_client_name" { From b5117fc25c8379ff83f22144646ff94aee0cfc16 Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Fri, 17 Dec 2021 18:30:29 +0100 Subject: [PATCH 11/16] Allow overrwriting connection attribute "_client_name" --- packets.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packets.go b/packets.go index 8e50a8e4b..a0dc60d96 100644 --- a/packets.go +++ b/packets.go @@ -328,10 +328,6 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte("github.com/go-sql-driver/mysql")) for k, v := range mc.cfg.ConnectAttrs { - if k == "_client_name" { - // do not allow overwriting reserved values - continue - } connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte(k)) connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte(v)) } From 9042bbd91638bd0fca51012611b75dbb7b104d9b Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Tue, 21 Dec 2021 12:10:23 +0100 Subject: [PATCH 12/16] Error when connection attributes that start with "_" are used. This behaviour is very similar to the native C# MySQL driver. See https://github.com/mysql/mysql-connector-net/blob/502d718bed8ca9cf81a3a0397574f24ec41b25ba/MySQL.Data/src/X/Sessions/XInternalSession.cs#L450-L469 --- packets.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packets.go b/packets.go index a0dc60d96..eca50dab7 100644 --- a/packets.go +++ b/packets.go @@ -18,6 +18,7 @@ import ( "fmt" "io" "math" + "strings" "time" ) @@ -328,6 +329,10 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte("github.com/go-sql-driver/mysql")) for k, v := range mc.cfg.ConnectAttrs { + if strings.HasPrefix(k, "_") { + return errors.New("connection attributes cannot start with '_'. They are reserved for internal usage") + } + connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte(k)) connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte(v)) } From 7c87a56e647e229c63bc4a095d2fcfaebdd85591 Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Tue, 21 Dec 2021 12:18:06 +0100 Subject: [PATCH 13/16] Set additional (internal) connection attributes: _os, _platform, _pid --- packets.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packets.go b/packets.go index eca50dab7..1a1d87964 100644 --- a/packets.go +++ b/packets.go @@ -18,6 +18,9 @@ import ( "fmt" "io" "math" + "os" + "runtime" + "strconv" "strings" "time" ) @@ -325,9 +328,21 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string connectAttrsBuf := make([]byte, 0, 100) if mc.flags&clientConnectAttrs != 0 { clientFlags |= clientConnectAttrs + + // Set default connection attributes + // See https://dev.mysql.com/doc/refman/8.0/en/performance-schema-connection-attribute-tables.html#performance-schema-connection-attributes-available connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte("_client_name")) connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte("github.com/go-sql-driver/mysql")) + connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte("_os")) + connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte(runtime.GOOS)) + + connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte("_platform")) + connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte(runtime.GOARCH)) + + connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte("_pid")) + connectAttrsBuf = appendLengthEncodedString(connectAttrsBuf, []byte(strconv.Itoa(os.Getpid()))) + for k, v := range mc.cfg.ConnectAttrs { if strings.HasPrefix(k, "_") { return errors.New("connection attributes cannot start with '_'. They are reserved for internal usage") From 1058830325cb4ecdcea958eb5410661cab187a4f Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Fri, 31 Dec 2021 10:29:02 +0100 Subject: [PATCH 14/16] Removed check if `clientSecureConn` is set inside clientFlags, because it is always set --- packets.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packets.go b/packets.go index 1a1d87964..9ac62fae2 100644 --- a/packets.go +++ b/packets.go @@ -314,7 +314,7 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string var authRespLEIBuf [9]byte authRespLen := len(authResp) authRespLEI := appendLengthEncodedInteger(authRespLEIBuf[:0], uint64(authRespLen)) - if len(authRespLEI) > 1 && clientFlags&clientSecureConn != 0 { + if len(authRespLEI) > 1 { // if the length can not be written in 1 byte, it must be written as a // length encoded integer clientFlags |= clientPluginAuthLenEncClientData From 00a09d858b6dcad3c455f6ad5105e30b5deca880 Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Fri, 31 Dec 2021 10:36:07 +0100 Subject: [PATCH 15/16] Add Andy Grunwald to AUTHORS file Andy Co-Authored the PR for MySQL Connection Attributes, based on the work from Vasily Fedoseyev --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index df847251d..4ed9f1916 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,6 +16,7 @@ Achille Roussel Alex Snast Alexey Palazhchenko Andrew Reid +Andy Grunwald Animesh Ray Arne Hormann Ariel Mashraki From 406e2a1c5330545ce9c2a7df6c89cd31dde85c2d Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Mon, 10 Jan 2022 23:28:03 +0100 Subject: [PATCH 16/16] Close rows after execution --- driver_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/driver_test.go b/driver_test.go index d026ee8a3..6a0ee9bd3 100644 --- a/driver_test.go +++ b/driver_test.go @@ -2211,6 +2211,7 @@ func TestConnectAttrs(t *testing.T) { } else { t.Skip("no performance_schema variable in mysql") } + rows.Close() rows, err = dbt.db.Query("SELECT attr_value FROM performance_schema.session_connect_attrs WHERE processlist_id=CONNECTION_ID() AND attr_name='program_name'") if err != nil { @@ -2226,6 +2227,7 @@ func TestConnectAttrs(t *testing.T) { } else { dbt.Error("no data for program_name") } + rows.Close() rows = dbt.mustQuery("SELECT attr_value FROM performance_schema.session_connect_attrs WHERE processlist_id=CONNECTION_ID() AND attr_name='foo'") if rows.Next() { @@ -2237,6 +2239,7 @@ func TestConnectAttrs(t *testing.T) { } else { dbt.Error("no data for custom attribute") } + rows.Close() } // static interface implementation checks of mysqlConn