From 7ec062f2361c0484d4ef97d44c723e709bfc696f Mon Sep 17 00:00:00 2001 From: Luiz Paulo Date: Sun, 8 Nov 2015 01:12:32 -0200 Subject: [PATCH 01/65] Fix issue with NO_AUTO_CREATE_USER option in sql_mode --- driver_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/driver_test.go b/driver_test.go index f9da416ec..91e192755 100644 --- a/driver_test.go +++ b/driver_test.go @@ -1018,7 +1018,7 @@ func TestFoundRows(t *testing.T) { func TestStrict(t *testing.T) { // ALLOW_INVALID_DATES to get rid of stricter modes - we want to test for warnings, not errors - relaxedDsn := dsn + "&sql_mode=ALLOW_INVALID_DATES" + relaxedDsn := dsn + "&sql_mode='ALLOW_INVALID_DATES,NO_AUTO_CREATE_USER'" // make sure the MySQL version is recent enough with a separate connection // before running the test conn, err := MySQLDriver{}.Open(relaxedDsn) @@ -1643,7 +1643,7 @@ func TestSqlInjection(t *testing.T) { dsns := []string{ dsn, - dsn + "&sql_mode=NO_BACKSLASH_ESCAPES", + dsn + "&sql_mode='NO_BACKSLASH_ESCAPES,NO_AUTO_CREATE_USER'", } for _, testdsn := range dsns { runTests(t, testdsn, createTest("1 OR 1=1")) @@ -1673,7 +1673,7 @@ func TestInsertRetrieveEscapedData(t *testing.T) { dsns := []string{ dsn, - dsn + "&sql_mode=NO_BACKSLASH_ESCAPES", + dsn + "&sql_mode='NO_BACKSLASH_ESCAPES,NO_AUTO_CREATE_USER'", } for _, testdsn := range dsns { runTests(t, testdsn, testData) From 0da0f4e972308e3c984399230530f5cc0d719dbb Mon Sep 17 00:00:00 2001 From: Daniel Nichter Date: Sun, 6 Dec 2015 12:23:40 -0800 Subject: [PATCH 02/65] Fixes #389 by not sending COM_QUIT until authenticated. Also refactors auth response handling code into func to reduce some duplication of logic. Test requires a MySQL socket and user having privs to use it. --- AUTHORS | 3 ++- connection.go | 23 ++++++++++++----- driver.go | 70 +++++++++++++++++++++++++++++--------------------- driver_test.go | 49 +++++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 37 deletions(-) diff --git a/AUTHORS b/AUTHORS index 6dd0167f3..0985a70ca 100644 --- a/AUTHORS +++ b/AUTHORS @@ -15,6 +15,7 @@ Aaron Hopkins Arne Hormann Carlos Nieto Chris Moos +Daniel Nichter DisposaBoy Frederick Mayle Gustavo Kristic @@ -25,6 +26,7 @@ INADA Naoki James Harr Jian Zhen Joshua Prunier +Julien Lefevre Julien Schmidt Kamil Dziedzic Leonardo YongUk Kim @@ -37,7 +39,6 @@ Soroush Pour Stan Putrya Xiaobing Jiang Xiuming Chen -Julien Lefevre # Organizations diff --git a/connection.go b/connection.go index 72ed09d69..455fcc1d9 100644 --- a/connection.go +++ b/connection.go @@ -120,18 +120,27 @@ func (mc *mysqlConn) Close() (err error) { // Makes Close idempotent if mc.netConn != nil { err = mc.writeCommandPacket(comQuit) - if err == nil { - err = mc.netConn.Close() - } else { - mc.netConn.Close() + } + + mc.cleanup() + + return +} + +// Closes the network connection and unsets internal variables. Do not call this +// function after successfully authentication, call Close instead. This function +// is called before auth or on auth failure because MySQL will have already +// closed the network connection. +func (mc *mysqlConn) cleanup() { + // Makes cleanup idempotent + if mc.netConn != nil { + if err := mc.netConn.Close(); err != nil { + errLog.Print(err) } mc.netConn = nil } - mc.cfg = nil mc.buf.rd = nil - - return } func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) { diff --git a/driver.go b/driver.go index d310624ad..639b3f271 100644 --- a/driver.go +++ b/driver.go @@ -84,43 +84,23 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) { // Reading Handshake Initialization Packet cipher, err := mc.readInitPacket() if err != nil { - mc.Close() + mc.cleanup() return nil, err } // Send Client Authentication Packet if err = mc.writeAuthPacket(cipher); err != nil { - mc.Close() + mc.cleanup() return nil, err } - // Read Result Packet - err = mc.readResultOK() - if err != nil { - // Retry with old authentication method, if allowed - if mc.cfg != nil && mc.cfg.allowOldPasswords && err == ErrOldPassword { - if err = mc.writeOldAuthPacket(cipher); err != nil { - mc.Close() - return nil, err - } - if err = mc.readResultOK(); err != nil { - mc.Close() - return nil, err - } - } else if mc.cfg != nil && mc.cfg.allowCleartextPasswords && err == ErrCleartextPassword { - if err = mc.writeClearAuthPacket(); err != nil { - mc.Close() - return nil, err - } - if err = mc.readResultOK(); err != nil { - mc.Close() - return nil, err - } - } else { - mc.Close() - return nil, err - } - + // Handle response to auth packet, switch methods if possible + if err = handleAuthResult(mc, cipher); err != nil { + // Authentication failed and MySQL has already closed the connection + // (https://dev.mysql.com/doc/internals/en/authentication-fails.html). + // Do not send COM_QUIT, just cleanup and return the error. + mc.cleanup() + return nil, err } // Get max allowed packet size @@ -144,6 +124,38 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) { return mc, nil } +func handleAuthResult(mc *mysqlConn, cipher []byte) (err error) { + // Read Result Packet + err = mc.readResultOK() + if err == nil { + return // auth successful + } + + if mc.cfg == nil { + return // auth failed and retry not possible + } + + // Retry auth if configured to do so. + if mc.cfg.allowOldPasswords && err == ErrOldPassword { + // Retry with old authentication method. Note: there are edge cases + // where this should work but doesn't; this is currently "wontfix": + // https://github.com/go-sql-driver/mysql/issues/184 + if err = mc.writeOldAuthPacket(cipher); err != nil { + return + } + err = mc.readResultOK() + } else if mc.cfg.allowCleartextPasswords && err == ErrCleartextPassword { + // Retry with clear text password for + // http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html + // http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html + if err = mc.writeClearAuthPacket(); err != nil { + return + } + err = mc.readResultOK() + } + return +} + func init() { sql.Register("mysql", &MySQLDriver{}) } diff --git a/driver_test.go b/driver_test.go index f9da416ec..aae9f9fa8 100644 --- a/driver_test.go +++ b/driver_test.go @@ -9,12 +9,14 @@ package mysql import ( + "bytes" "crypto/tls" "database/sql" "database/sql/driver" "fmt" "io" "io/ioutil" + "log" "net" "net/url" "os" @@ -1679,3 +1681,50 @@ func TestInsertRetrieveEscapedData(t *testing.T) { runTests(t, testdsn, testData) } } + +func TestUnixSocketAuthFail(t *testing.T) { + runTests(t, dsn, func(dbt *DBTest) { + // Save the current logger so we can restore it. + oldLogger := errLog + + // Set a new logger so we can capture its output. + buffer := bytes.NewBuffer(make([]byte, 0, 64)) + newLogger := log.New(buffer, "prefix: ", 0) + SetLogger(newLogger) + + // Restore the logger. + defer SetLogger(oldLogger) + + // Make a new DSN that uses the MySQL socket file and a bad password, which + // we can make by simply appending any character to the real password. + badPass := pass + "x" + socket := "" + if prot == "unix" { + socket = addr + } else { + // Get socket file from MySQL. + err := dbt.db.QueryRow("SELECT @@socket").Scan(&socket) + if err != nil { + t.Fatalf("Error on SELECT @@socket: %s", err.Error()) + } + } + t.Logf("socket: %s", socket) + badDSN := fmt.Sprintf("%s:%s@unix(%s)/%s?timeout=30s&strict=true", user, badPass, socket, dbname) + db, err := sql.Open("mysql", badDSN) + if err != nil { + t.Fatalf("Error connecting: %s", err.Error()) + } + defer db.Close() + + // Connect to MySQL for real. This will cause an auth failure. + err = db.Ping() + if err == nil { + t.Error("expected Ping() to return an error") + } + + // The driver should not log anything. + if actual := buffer.String(); actual != "" { + t.Errorf("expected no output, got %q", actual) + } + }) +} From 0874761875dd74e1c1c06340aa28e68eb48fa715 Mon Sep 17 00:00:00 2001 From: Kevin Malachowski Date: Wed, 23 Dec 2015 16:32:48 -0800 Subject: [PATCH 03/65] QueryUnescape the tlsConfig name during DSN parsing. Also add unit test. --- utils.go | 2 ++ utils_test.go | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/utils.go b/utils.go index 6a26ad129..4bfc331e3 100644 --- a/utils.go +++ b/utils.go @@ -267,6 +267,8 @@ func parseDSNParams(cfg *config, params string) (err error) { if boolValue { cfg.tls = &tls.Config{} } + } else if value, err := url.QueryUnescape(value); err != nil { + return fmt.Errorf("Invalid value for tls config name: %v", err) } else { if strings.ToLower(value) == "skip-verify" { cfg.tls = &tls.Config{InsecureSkipVerify: true} diff --git a/utils_test.go b/utils_test.go index 79fbdd1eb..15e4d4b7a 100644 --- a/utils_test.go +++ b/utils_test.go @@ -13,6 +13,7 @@ import ( "crypto/tls" "encoding/binary" "fmt" + "net/url" "testing" "time" ) @@ -116,6 +117,23 @@ func TestDSNWithCustomTLS(t *testing.T) { DeregisterTLSConfig("utils_test") } +func TestDSNWithCustomTLS_queryEscape(t *testing.T) { + const configKey = "&%!:" + dsn := "user:password@tcp(localhost:5555)/dbname?tls=" + url.QueryEscape(configKey) + name := "foohost" + tlsCfg := tls.Config{ServerName: name} + + RegisterTLSConfig(configKey, &tlsCfg) + + cfg, err := parseDSN(dsn) + + if err != nil { + t.Error(err.Error()) + } else if cfg.tls.ServerName != name { + t.Errorf("Did not get the correct TLS ServerName (%s) parsing DSN (%s).", name, dsn) + } +} + func TestDSNUnsafeCollation(t *testing.T) { _, err := parseDSN("/dbname?collation=gbk_chinese_ci&interpolateParams=true") if err != errInvalidDSNUnsafeCollation { From 5e93316731528fdcbf2b4c836eeefe37b7e9f5e4 Mon Sep 17 00:00:00 2001 From: Kevin Malachowski Date: Wed, 6 Jan 2016 11:23:08 -0800 Subject: [PATCH 04/65] Update AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 6fc4c6f7b..f3364b99b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -26,6 +26,7 @@ Jian Zhen Joshua Prunier Julien Schmidt Kamil Dziedzic +Kevin Malachowski Leonardo YongUk Kim Lucas Liu Luke Scott From 0f5d83a2cb5a037c0805ab1a93090c7bb8d9d345 Mon Sep 17 00:00:00 2001 From: Daniel Nichter Date: Wed, 6 Jan 2016 13:53:17 -0800 Subject: [PATCH 05/65] Use explicit return in handleAuthResult. --- driver.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/driver.go b/driver.go index 639b3f271..7502c57b4 100644 --- a/driver.go +++ b/driver.go @@ -124,15 +124,15 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) { return mc, nil } -func handleAuthResult(mc *mysqlConn, cipher []byte) (err error) { +func handleAuthResult(mc *mysqlConn, cipher []byte) error { // Read Result Packet - err = mc.readResultOK() + err := mc.readResultOK() if err == nil { - return // auth successful + return nil // auth successful } if mc.cfg == nil { - return // auth failed and retry not possible + return err // auth failed and retry not possible } // Retry auth if configured to do so. @@ -141,7 +141,7 @@ func handleAuthResult(mc *mysqlConn, cipher []byte) (err error) { // where this should work but doesn't; this is currently "wontfix": // https://github.com/go-sql-driver/mysql/issues/184 if err = mc.writeOldAuthPacket(cipher); err != nil { - return + return err } err = mc.readResultOK() } else if mc.cfg.allowCleartextPasswords && err == ErrCleartextPassword { @@ -149,11 +149,11 @@ func handleAuthResult(mc *mysqlConn, cipher []byte) (err error) { // http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html // http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html if err = mc.writeClearAuthPacket(); err != nil { - return + return err } err = mc.readResultOK() } - return + return err } func init() { From a4041c6a4933fc536dd8fea400505339a57e451a Mon Sep 17 00:00:00 2001 From: Kevin Malachowski Date: Wed, 6 Jan 2016 17:19:47 -0800 Subject: [PATCH 06/65] Use Errorf instead of Error in the correct places --- utils_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/utils_test.go b/utils_test.go index 15e4d4b7a..8ff905069 100644 --- a/utils_test.go +++ b/utils_test.go @@ -137,37 +137,37 @@ func TestDSNWithCustomTLS_queryEscape(t *testing.T) { func TestDSNUnsafeCollation(t *testing.T) { _, err := parseDSN("/dbname?collation=gbk_chinese_ci&interpolateParams=true") if err != errInvalidDSNUnsafeCollation { - t.Error("Expected %v, Got %v", errInvalidDSNUnsafeCollation, err) + t.Errorf("Expected %v, Got %v", errInvalidDSNUnsafeCollation, err) } _, err = parseDSN("/dbname?collation=gbk_chinese_ci&interpolateParams=false") if err != nil { - t.Error("Expected %v, Got %v", nil, err) + t.Errorf("Expected %v, Got %v", nil, err) } _, err = parseDSN("/dbname?collation=gbk_chinese_ci") if err != nil { - t.Error("Expected %v, Got %v", nil, err) + t.Errorf("Expected %v, Got %v", nil, err) } _, err = parseDSN("/dbname?collation=ascii_bin&interpolateParams=true") if err != nil { - t.Error("Expected %v, Got %v", nil, err) + t.Errorf("Expected %v, Got %v", nil, err) } _, err = parseDSN("/dbname?collation=latin1_german1_ci&interpolateParams=true") if err != nil { - t.Error("Expected %v, Got %v", nil, err) + t.Errorf("Expected %v, Got %v", nil, err) } _, err = parseDSN("/dbname?collation=utf8_general_ci&interpolateParams=true") if err != nil { - t.Error("Expected %v, Got %v", nil, err) + t.Errorf("Expected %v, Got %v", nil, err) } _, err = parseDSN("/dbname?collation=utf8mb4_general_ci&interpolateParams=true") if err != nil { - t.Error("Expected %v, Got %v", nil, err) + t.Errorf("Expected %v, Got %v", nil, err) } } From d164b602b9254200a31ba9ca462b063fac2c1fdc Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Wed, 13 Jan 2016 00:25:22 +0100 Subject: [PATCH 07/65] Update CHANGELOG --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 161ad0fcc..381d91825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,21 @@ Bugfixes: - Enable microsecond resolution on TIME, DATETIME and TIMESTAMP (#249) - Fixed handling of queries without columns and rows (#255) - Fixed a panic when SetKeepAlive() failed (#298) + - Support receiving ERR packet while reading rows (#321) + - Fixed reading NULL length-encoded integers in MySQL 5.6+ (#349) + - Fixed absolute paths support in LOAD LOCAL DATA INFILE (#356) + - Actually zero out bytes in handshake response (#378) + - Fixed race condition in registering LOAD DATA INFILE handler (#383) + - Fixed tests with MySQL 5.7.9+ (#380) + - QueryUnescape TLS config names (#397) + - Fixed "broken pipe" error by writing to closed socket (#390) New Features: - - Support for returning table alias on Columns() (#289) + - Support for returning table alias on Columns() (#289, #359, #382) - Placeholder interpolation, can be actived with the DSN parameter `interpolateParams=true` (#309, #318) + - Support for uint64 parameters with high bit set (#332, #345) + - Cleartext authentication plugin support (#327) + ## Version 1.2 (2014-06-03) From 481fa108c8707092b26fd0dcf33eb05628b483c9 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Wed, 13 Jan 2016 11:34:46 +0100 Subject: [PATCH 08/65] Travis: Test with Go 1.5 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 2f4e3c2f0..c48cedd87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ go: - 1.2 - 1.3 - 1.4 + - 1.5 - tip before_script: From c2d7e965ef48d2e334f103668e16e069d41153a1 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Wed, 13 Jan 2016 13:56:13 +0100 Subject: [PATCH 09/65] Export ParseDSN and Config Fixes #224 --- benchmark_test.go | 6 +- connection.go | 52 +------- driver.go | 16 +-- driver_test.go | 2 +- dsn.go | 298 ++++++++++++++++++++++++++++++++++++++++++++++ dsn_test.go | 180 ++++++++++++++++++++++++++++ infile.go | 2 +- packets.go | 40 +++---- rows.go | 2 +- utils.go | 237 ------------------------------------ utils_test.go | 167 -------------------------- 11 files changed, 518 insertions(+), 484 deletions(-) create mode 100644 dsn.go create mode 100644 dsn_test.go diff --git a/benchmark_test.go b/benchmark_test.go index fb8a2f5f3..7ccb46fcc 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -216,9 +216,9 @@ func BenchmarkRoundtripBin(b *testing.B) { func BenchmarkInterpolation(b *testing.B) { mc := &mysqlConn{ - cfg: &config{ - interpolateParams: true, - loc: time.UTC, + cfg: &Config{ + InterpolateParams: true, + Loc: time.UTC, }, maxPacketAllowed: maxPacketSize, maxWriteSize: maxPacketSize - 1, diff --git a/connection.go b/connection.go index 455fcc1d9..82fee76a0 100644 --- a/connection.go +++ b/connection.go @@ -9,9 +9,7 @@ package mysql import ( - "crypto/tls" "database/sql/driver" - "errors" "net" "strconv" "strings" @@ -23,7 +21,7 @@ type mysqlConn struct { netConn net.Conn affectedRows uint64 insertId uint64 - cfg *config + cfg *Config maxPacketAllowed int maxWriteSize int flags clientFlag @@ -33,28 +31,9 @@ type mysqlConn struct { strict bool } -type config struct { - user string - passwd string - net string - addr string - dbname string - params map[string]string - loc *time.Location - tls *tls.Config - timeout time.Duration - collation uint8 - allowAllFiles bool - allowOldPasswords bool - allowCleartextPasswords bool - clientFoundRows bool - columnsWithAlias bool - interpolateParams bool -} - // Handles parameters set in DSN after the connection is established func (mc *mysqlConn) handleParams() (err error) { - for param, val := range mc.cfg.params { + for param, val := range mc.cfg.Params { switch param { // Charset case "charset": @@ -70,27 +49,6 @@ func (mc *mysqlConn) handleParams() (err error) { return } - // time.Time parsing - case "parseTime": - var isBool bool - mc.parseTime, isBool = readBool(val) - if !isBool { - return errors.New("Invalid Bool value: " + val) - } - - // Strict mode - case "strict": - var isBool bool - mc.strict, isBool = readBool(val) - if !isBool { - return errors.New("Invalid Bool value: " + val) - } - - // Compression - case "compress": - err = errors.New("Compression not implemented yet") - return - // System Vars default: err = mc.exec("SET " + param + "=" + val + "") @@ -217,7 +175,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin if v.IsZero() { buf = append(buf, "'0000-00-00'"...) } else { - v := v.In(mc.cfg.loc) + v := v.In(mc.cfg.Loc) v = v.Add(time.Nanosecond * 500) // To round under microsecond year := v.Year() year100 := year / 100 @@ -298,7 +256,7 @@ func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, err return nil, driver.ErrBadConn } if len(args) != 0 { - if !mc.cfg.interpolateParams { + if !mc.cfg.InterpolateParams { return nil, driver.ErrSkip } // try to interpolate the parameters to save extra roundtrips for preparing and closing a statement @@ -349,7 +307,7 @@ func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, erro return nil, driver.ErrBadConn } if len(args) != 0 { - if !mc.cfg.interpolateParams { + if !mc.cfg.InterpolateParams { return nil, driver.ErrSkip } // try client-side prepare to reduce roundtrip diff --git a/driver.go b/driver.go index 7502c57b4..1d7723b82 100644 --- a/driver.go +++ b/driver.go @@ -53,17 +53,19 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) { maxPacketAllowed: maxPacketSize, maxWriteSize: maxPacketSize - 1, } - mc.cfg, err = parseDSN(dsn) + mc.cfg, err = ParseDSN(dsn) if err != nil { return nil, err } + mc.parseTime = mc.cfg.ParseTime + mc.strict = mc.cfg.Strict // Connect to Server - if dial, ok := dials[mc.cfg.net]; ok { - mc.netConn, err = dial(mc.cfg.addr) + if dial, ok := dials[mc.cfg.Net]; ok { + mc.netConn, err = dial(mc.cfg.Addr) } else { - nd := net.Dialer{Timeout: mc.cfg.timeout} - mc.netConn, err = nd.Dial(mc.cfg.net, mc.cfg.addr) + nd := net.Dialer{Timeout: mc.cfg.Timeout} + mc.netConn, err = nd.Dial(mc.cfg.Net, mc.cfg.Addr) } if err != nil { return nil, err @@ -136,7 +138,7 @@ func handleAuthResult(mc *mysqlConn, cipher []byte) error { } // Retry auth if configured to do so. - if mc.cfg.allowOldPasswords && err == ErrOldPassword { + if mc.cfg.AllowOldPasswords && err == ErrOldPassword { // Retry with old authentication method. Note: there are edge cases // where this should work but doesn't; this is currently "wontfix": // https://github.com/go-sql-driver/mysql/issues/184 @@ -144,7 +146,7 @@ func handleAuthResult(mc *mysqlConn, cipher []byte) error { return err } err = mc.readResultOK() - } else if mc.cfg.allowCleartextPasswords && err == ErrCleartextPassword { + } else if mc.cfg.AllowCleartextPasswords && err == ErrCleartextPassword { // Retry with clear text password for // http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html // http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html diff --git a/driver_test.go b/driver_test.go index fbbe6d5ec..0e9571a59 100644 --- a/driver_test.go +++ b/driver_test.go @@ -91,7 +91,7 @@ func runTests(t *testing.T, dsn string, tests ...func(dbt *DBTest)) { dsn2 := dsn + "&interpolateParams=true" var db2 *sql.DB - if _, err := parseDSN(dsn2); err != errInvalidDSNUnsafeCollation { + if _, err := ParseDSN(dsn2); err != errInvalidDSNUnsafeCollation { db2, err = sql.Open("mysql", dsn2) if err != nil { t.Fatalf("Error connecting: %s", err.Error()) diff --git a/dsn.go b/dsn.go new file mode 100644 index 000000000..45b4899d9 --- /dev/null +++ b/dsn.go @@ -0,0 +1,298 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// +// Copyright 2016 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 ( + "crypto/tls" + "errors" + "fmt" + "net" + "net/url" + "strings" + "time" +) + +var ( + errInvalidDSNUnescaped = errors.New("Invalid DSN: Did you forget to escape a param value?") + errInvalidDSNAddr = errors.New("Invalid DSN: Network Address not terminated (missing closing brace)") + errInvalidDSNNoSlash = errors.New("Invalid DSN: Missing the slash separating the database name") + errInvalidDSNUnsafeCollation = errors.New("Invalid DSN: interpolateParams can be used with ascii, latin1, utf8 and utf8mb4 charset") +) + +// Config is a configuration parsed from a DSN string +type Config struct { + User string // Username + Passwd string // Password + Net string // Network type + Addr string // Network address + DBName string // Database name + Params map[string]string // Connection parameters + Loc *time.Location // Location for time.Time values + TLS *tls.Config // TLS configuration + Timeout time.Duration // Dial timeout + Collation uint8 // Connection collation + + AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE + AllowCleartextPasswords bool // Allows the cleartext client side plugin + AllowOldPasswords bool // Allows the old insecure password method + ClientFoundRows bool // Return number of matching rows instead of rows changed + ColumnsWithAlias bool // Prepend table alias to column names + InterpolateParams bool // Interpolate placeholders into query string + ParseTime bool // Parse time values to time.Time + Strict bool // Return warnings as errors +} + +// ParseDSN parses the DSN string to a Config +func ParseDSN(dsn string) (cfg *Config, err error) { + // New config with some default values + cfg = &Config{ + Loc: time.UTC, + Collation: defaultCollation, + } + + // [user[:password]@][net[(addr)]]/dbname[?param1=value1¶mN=valueN] + // Find the last '/' (since the password or the net addr might contain a '/') + foundSlash := false + for i := len(dsn) - 1; i >= 0; i-- { + if dsn[i] == '/' { + foundSlash = true + var j, k int + + // left part is empty if i <= 0 + if i > 0 { + // [username[:password]@][protocol[(address)]] + // Find the last '@' in dsn[:i] + for j = i; j >= 0; j-- { + if dsn[j] == '@' { + // username[:password] + // Find the first ':' in dsn[:j] + for k = 0; k < j; k++ { + if dsn[k] == ':' { + cfg.Passwd = dsn[k+1 : j] + break + } + } + cfg.User = dsn[:k] + + break + } + } + + // [protocol[(address)]] + // Find the first '(' in dsn[j+1:i] + for k = j + 1; k < i; k++ { + if dsn[k] == '(' { + // dsn[i-1] must be == ')' if an address is specified + if dsn[i-1] != ')' { + if strings.ContainsRune(dsn[k+1:i], ')') { + return nil, errInvalidDSNUnescaped + } + return nil, errInvalidDSNAddr + } + cfg.Addr = dsn[k+1 : i-1] + break + } + } + cfg.Net = dsn[j+1 : k] + } + + // dbname[?param1=value1&...¶mN=valueN] + // Find the first '?' in dsn[i+1:] + for j = i + 1; j < len(dsn); j++ { + if dsn[j] == '?' { + if err = parseDSNParams(cfg, dsn[j+1:]); err != nil { + return + } + break + } + } + cfg.DBName = dsn[i+1 : j] + + break + } + } + + if !foundSlash && len(dsn) > 0 { + return nil, errInvalidDSNNoSlash + } + + if cfg.InterpolateParams && unsafeCollations[cfg.Collation] { + return nil, errInvalidDSNUnsafeCollation + } + + // Set default network if empty + if cfg.Net == "" { + cfg.Net = "tcp" + } + + // Set default address if empty + if cfg.Addr == "" { + switch cfg.Net { + case "tcp": + cfg.Addr = "127.0.0.1:3306" + case "unix": + cfg.Addr = "/tmp/mysql.sock" + default: + return nil, errors.New("Default addr for network '" + cfg.Net + "' unknown") + } + + } + + return +} + +// parseDSNParams parses the DSN "query string" +// Values must be url.QueryEscape'ed +func parseDSNParams(cfg *Config, params string) (err error) { + for _, v := range strings.Split(params, "&") { + param := strings.SplitN(v, "=", 2) + if len(param) != 2 { + continue + } + + // cfg params + switch value := param[1]; param[0] { + + // Disable INFILE whitelist / enable all files + case "allowAllFiles": + var isBool bool + cfg.AllowAllFiles, isBool = readBool(value) + if !isBool { + return fmt.Errorf("Invalid Bool value: %s", value) + } + + // Use cleartext authentication mode (MySQL 5.5.10+) + case "allowCleartextPasswords": + var isBool bool + cfg.AllowCleartextPasswords, isBool = readBool(value) + if !isBool { + return fmt.Errorf("Invalid Bool value: %s", value) + } + + // Use old authentication mode (pre MySQL 4.1) + case "allowOldPasswords": + var isBool bool + cfg.AllowOldPasswords, isBool = readBool(value) + if !isBool { + return fmt.Errorf("Invalid Bool value: %s", value) + } + + // Switch "rowsAffected" mode + case "clientFoundRows": + var isBool bool + cfg.ClientFoundRows, isBool = readBool(value) + if !isBool { + return fmt.Errorf("Invalid Bool value: %s", value) + } + + // Collation + case "collation": + collation, ok := collations[value] + if !ok { + // Note possibility for false negatives: + // could be triggered although the collation is valid if the + // collations map does not contain entries the server supports. + err = errors.New("unknown collation") + return + } + cfg.Collation = collation + break + + case "columnsWithAlias": + var isBool bool + cfg.ColumnsWithAlias, isBool = readBool(value) + if !isBool { + return fmt.Errorf("Invalid Bool value: %s", value) + } + + // Compression + case "compress": + return errors.New("Compression not implemented yet") + + // Enable client side placeholder substitution + case "interpolateParams": + var isBool bool + cfg.InterpolateParams, isBool = readBool(value) + if !isBool { + return fmt.Errorf("Invalid Bool value: %s", value) + } + + // Time Location + case "loc": + if value, err = url.QueryUnescape(value); err != nil { + return + } + cfg.Loc, err = time.LoadLocation(value) + if err != nil { + return + } + + // time.Time parsing + case "parseTime": + var isBool bool + cfg.ParseTime, isBool = readBool(value) + if !isBool { + return errors.New("Invalid Bool value: " + value) + } + + // Strict mode + case "strict": + var isBool bool + cfg.Strict, isBool = readBool(value) + if !isBool { + return errors.New("Invalid Bool value: " + value) + } + + // Dial Timeout + case "timeout": + cfg.Timeout, err = time.ParseDuration(value) + if err != nil { + return + } + + // TLS-Encryption + case "tls": + boolValue, isBool := readBool(value) + if isBool { + if boolValue { + cfg.TLS = &tls.Config{} + } + } else if value, err := url.QueryUnescape(value); err != nil { + return fmt.Errorf("Invalid value for tls config name: %v", err) + } else { + if strings.ToLower(value) == "skip-verify" { + cfg.TLS = &tls.Config{InsecureSkipVerify: true} + } else if tlsConfig, ok := tlsConfigRegister[value]; ok { + if len(tlsConfig.ServerName) == 0 && !tlsConfig.InsecureSkipVerify { + host, _, err := net.SplitHostPort(cfg.Addr) + if err == nil { + tlsConfig.ServerName = host + } + } + + cfg.TLS = tlsConfig + } else { + return fmt.Errorf("Invalid value / unknown config name: %s", value) + } + } + + default: + // lazy init + if cfg.Params == nil { + cfg.Params = make(map[string]string) + } + + if cfg.Params[param[0]], err = url.QueryUnescape(value); err != nil { + return + } + } + } + + return +} diff --git a/dsn_test.go b/dsn_test.go new file mode 100644 index 000000000..4ac1f562c --- /dev/null +++ b/dsn_test.go @@ -0,0 +1,180 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// +// Copyright 2016 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 ( + "crypto/tls" + "fmt" + "net/url" + "testing" +) + +var testDSNs = []struct { + in string + out string +}{ + {"username:password@protocol(address)/dbname?param=value", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false ParseTime:false Strict:false}"}, + {"user@unix(/path/to/socket)/dbname?charset=utf8", "&{User:user Passwd: Net:unix Addr:/path/to/socket DBName:dbname Params:map[charset:utf8] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8mb4,utf8] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"user:password@/dbname?loc=UTC&timeout=30s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", "&{User:user Passwd:password Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Loc:UTC TLS: Timeout:30s Collation:224 AllowAllFiles:true AllowCleartextPasswords:false AllowOldPasswords:true ClientFoundRows:true ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{User:user Passwd:p@ss(word) Net:tcp Addr:[de:ad:be:ef::ca:fe]:80 DBName:dbname Params:map[] Loc:Local TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"/dbname", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"@/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"user:p@/ssword@/", "&{User:user Passwd:p@/ssword Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"unix/?arg=%2Fsome%2Fpath.ext", "&{User: Passwd: Net:unix Addr:/tmp/mysql.sock DBName: Params:map[arg:/some/path.ext] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, +} + +func TestDSNParser(t *testing.T) { + var cfg *Config + var err error + var res string + + for i, tst := range testDSNs { + cfg, err = ParseDSN(tst.in) + if err != nil { + t.Error(err.Error()) + } + + // pointer not static + cfg.TLS = nil + + res = fmt.Sprintf("%+v", cfg) + if res != tst.out { + t.Errorf("%d. ParseDSN(%q) => %q, want %q", i, tst.in, res, tst.out) + } + } +} + +func TestDSNParserInvalid(t *testing.T) { + var invalidDSNs = []string{ + "@net(addr/", // no closing brace + "@tcp(/", // no closing brace + "tcp(/", // no closing brace + "(/", // no closing brace + "net(addr)//", // unescaped + "User:pass@tcp(1.2.3.4:3306)", // no trailing slash + //"/dbname?arg=/some/unescaped/path", + } + + for i, tst := range invalidDSNs { + if _, err := ParseDSN(tst); err == nil { + t.Errorf("invalid DSN #%d. (%s) didn't error!", i, tst) + } + } +} + +func TestDSNWithCustomTLS(t *testing.T) { + baseDSN := "User:password@tcp(localhost:5555)/dbname?tls=" + tlsCfg := tls.Config{} + + RegisterTLSConfig("utils_test", &tlsCfg) + + // Custom TLS is missing + tst := baseDSN + "invalid_tls" + cfg, err := ParseDSN(tst) + if err == nil { + t.Errorf("invalid custom TLS in DSN (%s) but did not error. Got config: %#v", tst, cfg) + } + + tst = baseDSN + "utils_test" + + // Custom TLS with a server name + name := "foohost" + tlsCfg.ServerName = name + cfg, err = ParseDSN(tst) + + if err != nil { + t.Error(err.Error()) + } else if cfg.TLS.ServerName != name { + t.Errorf("did not get the correct TLS ServerName (%s) parsing DSN (%s).", name, tst) + } + + // Custom TLS without a server name + name = "localhost" + tlsCfg.ServerName = "" + cfg, err = ParseDSN(tst) + + if err != nil { + t.Error(err.Error()) + } else if cfg.TLS.ServerName != name { + t.Errorf("did not get the correct ServerName (%s) parsing DSN (%s).", name, tst) + } + + DeregisterTLSConfig("utils_test") +} + +func TestDSNWithCustomTLS_queryEscape(t *testing.T) { + const configKey = "&%!:" + dsn := "User:password@tcp(localhost:5555)/dbname?tls=" + url.QueryEscape(configKey) + name := "foohost" + tlsCfg := tls.Config{ServerName: name} + + RegisterTLSConfig(configKey, &tlsCfg) + + cfg, err := ParseDSN(dsn) + + if err != nil { + t.Error(err.Error()) + } else if cfg.TLS.ServerName != name { + t.Errorf("did not get the correct TLS ServerName (%s) parsing DSN (%s).", name, dsn) + } +} + +func TestDSNUnsafeCollation(t *testing.T) { + _, err := ParseDSN("/dbname?collation=gbk_chinese_ci&interpolateParams=true") + if err != errInvalidDSNUnsafeCollation { + t.Errorf("expected %v, got %v", errInvalidDSNUnsafeCollation, err) + } + + _, err = ParseDSN("/dbname?collation=gbk_chinese_ci&interpolateParams=false") + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } + + _, err = ParseDSN("/dbname?collation=gbk_chinese_ci") + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } + + _, err = ParseDSN("/dbname?collation=ascii_bin&interpolateParams=true") + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } + + _, err = ParseDSN("/dbname?collation=latin1_german1_ci&interpolateParams=true") + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } + + _, err = ParseDSN("/dbname?collation=utf8_general_ci&interpolateParams=true") + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } + + _, err = ParseDSN("/dbname?collation=utf8mb4_general_ci&interpolateParams=true") + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } +} + +func BenchmarkParseDSN(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + for _, tst := range testDSNs { + if _, err := ParseDSN(tst.in); err != nil { + b.Error(err.Error()) + } + } + } +} diff --git a/infile.go b/infile.go index 84c53a99c..9c898b705 100644 --- a/infile.go +++ b/infile.go @@ -124,7 +124,7 @@ func (mc *mysqlConn) handleInFileRequest(name string) (err error) { fileRegisterLock.RLock() fr := fileRegister[name] fileRegisterLock.RUnlock() - if mc.cfg.allowAllFiles || fr { + if mc.cfg.AllowAllFiles || fr { var file *os.File var fi os.FileInfo diff --git a/packets.go b/packets.go index 76cb7c84e..532c56c96 100644 --- a/packets.go +++ b/packets.go @@ -161,7 +161,7 @@ func (mc *mysqlConn) readInitPacket() ([]byte, error) { if mc.flags&clientProtocol41 == 0 { return nil, ErrOldProtocol } - if mc.flags&clientSSL == 0 && mc.cfg.tls != nil { + if mc.flags&clientSSL == 0 && mc.cfg.TLS != nil { return nil, ErrNoTLS } pos += 2 @@ -221,22 +221,22 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte) error { clientPluginAuth | mc.flags&clientLongFlag - if mc.cfg.clientFoundRows { + if mc.cfg.ClientFoundRows { clientFlags |= clientFoundRows } // To enable TLS / SSL - if mc.cfg.tls != nil { + if mc.cfg.TLS != nil { clientFlags |= clientSSL } // User Password - scrambleBuff := scramblePassword(cipher, []byte(mc.cfg.passwd)) + scrambleBuff := scramblePassword(cipher, []byte(mc.cfg.Passwd)) - pktLen := 4 + 4 + 1 + 23 + len(mc.cfg.user) + 1 + 1 + len(scrambleBuff) + 21 + 1 + pktLen := 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1 + 1 + len(scrambleBuff) + 21 + 1 // To specify a db name - if n := len(mc.cfg.dbname); n > 0 { + if n := len(mc.cfg.DBName); n > 0 { clientFlags |= clientConnectWithDB pktLen += n + 1 } @@ -262,18 +262,18 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte) error { data[11] = 0x00 // Charset [1 byte] - data[12] = mc.cfg.collation + data[12] = mc.cfg.Collation // SSL Connection Request Packet // http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::SSLRequest - if mc.cfg.tls != nil { + if mc.cfg.TLS != nil { // Send TLS / SSL request packet if err := mc.writePacket(data[:(4+4+1+23)+4]); err != nil { return err } // Switch to TLS - tlsConn := tls.Client(mc.netConn, mc.cfg.tls) + tlsConn := tls.Client(mc.netConn, mc.cfg.TLS) if err := tlsConn.Handshake(); err != nil { return err } @@ -288,8 +288,8 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte) error { } // User [null terminated string] - if len(mc.cfg.user) > 0 { - pos += copy(data[pos:], mc.cfg.user) + if len(mc.cfg.User) > 0 { + pos += copy(data[pos:], mc.cfg.User) } data[pos] = 0x00 pos++ @@ -299,8 +299,8 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte) error { pos += 1 + copy(data[pos+1:], scrambleBuff) // Databasename [null terminated string] - if len(mc.cfg.dbname) > 0 { - pos += copy(data[pos:], mc.cfg.dbname) + if len(mc.cfg.DBName) > 0 { + pos += copy(data[pos:], mc.cfg.DBName) data[pos] = 0x00 pos++ } @@ -317,7 +317,7 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte) error { // http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchResponse func (mc *mysqlConn) writeOldAuthPacket(cipher []byte) error { // User password - scrambleBuff := scrambleOldPassword(cipher, []byte(mc.cfg.passwd)) + scrambleBuff := scrambleOldPassword(cipher, []byte(mc.cfg.Passwd)) // Calculate the packet length and add a tailing 0 pktLen := len(scrambleBuff) + 1 @@ -339,7 +339,7 @@ func (mc *mysqlConn) writeOldAuthPacket(cipher []byte) error { // http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchResponse func (mc *mysqlConn) writeClearAuthPacket() error { // Calculate the packet length and add a tailing 0 - pktLen := len(mc.cfg.passwd) + 1 + pktLen := len(mc.cfg.Passwd) + 1 data := mc.buf.takeSmallBuffer(4 + pktLen) if data == nil { // can not take the buffer. Something must be wrong with the connection @@ -348,7 +348,7 @@ func (mc *mysqlConn) writeClearAuthPacket() error { } // Add the clear password [null terminated string] - copy(data[4:], mc.cfg.passwd) + copy(data[4:], mc.cfg.Passwd) data[4+pktLen-1] = 0x00 return mc.writePacket(data) @@ -575,7 +575,7 @@ func (mc *mysqlConn) readColumns(count int) ([]mysqlField, error) { pos += n // Table [len coded string] - if mc.cfg.columnsWithAlias { + if mc.cfg.ColumnsWithAlias { tableName, _, n, err := readLengthEncodedString(data[pos:]) if err != nil { return nil, err @@ -674,7 +674,7 @@ func (rows *textRows) readRow(dest []driver.Value) error { fieldTypeDate, fieldTypeNewDate: dest[i], err = parseDateTime( string(dest[i].([]byte)), - mc.cfg.loc, + mc.cfg.Loc, ) if err == nil { continue @@ -981,7 +981,7 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error { if v.IsZero() { val = []byte("0000-00-00") } else { - val = []byte(v.In(mc.cfg.loc).Format(timeFormat)) + val = []byte(v.In(mc.cfg.Loc).Format(timeFormat)) } paramValues = appendLengthEncodedInteger(paramValues, @@ -1144,7 +1144,7 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { } dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], dstlen, true) case rows.mc.parseTime: - dest[i], err = parseBinaryDateTime(num, data[pos:], rows.mc.cfg.loc) + dest[i], err = parseBinaryDateTime(num, data[pos:], rows.mc.cfg.Loc) default: var dstlen uint8 if rows.columns[i].fieldType == fieldTypeDate { diff --git a/rows.go b/rows.go index ba606e146..5d21948ad 100644 --- a/rows.go +++ b/rows.go @@ -38,7 +38,7 @@ type emptyRows struct{} func (rows *mysqlRows) Columns() []string { columns := make([]string, len(rows.columns)) - if rows.mc.cfg.columnsWithAlias { + if rows.mc.cfg.ColumnsWithAlias { for i := range columns { if tableName := rows.columns[i].tableName; len(tableName) > 0 { columns[i] = tableName + "." + rows.columns[i].name diff --git a/utils.go b/utils.go index 4bfc331e3..e267cce4e 100644 --- a/utils.go +++ b/utils.go @@ -13,22 +13,14 @@ import ( "crypto/tls" "database/sql/driver" "encoding/binary" - "errors" "fmt" "io" - "net" - "net/url" "strings" "time" ) var ( tlsConfigRegister map[string]*tls.Config // Register for custom tls.Configs - - errInvalidDSNUnescaped = errors.New("Invalid DSN: Did you forget to escape a param value?") - errInvalidDSNAddr = errors.New("Invalid DSN: Network Address not terminated (missing closing brace)") - errInvalidDSNNoSlash = errors.New("Invalid DSN: Missing the slash separating the database name") - errInvalidDSNUnsafeCollation = errors.New("Invalid DSN: interpolateParams can be used with ascii, latin1, utf8 and utf8mb4 charset") ) func init() { @@ -72,235 +64,6 @@ func DeregisterTLSConfig(key string) { delete(tlsConfigRegister, key) } -// parseDSN parses the DSN string to a config -func parseDSN(dsn string) (cfg *config, err error) { - // New config with some default values - cfg = &config{ - loc: time.UTC, - collation: defaultCollation, - } - - // [user[:password]@][net[(addr)]]/dbname[?param1=value1¶mN=valueN] - // Find the last '/' (since the password or the net addr might contain a '/') - foundSlash := false - for i := len(dsn) - 1; i >= 0; i-- { - if dsn[i] == '/' { - foundSlash = true - var j, k int - - // left part is empty if i <= 0 - if i > 0 { - // [username[:password]@][protocol[(address)]] - // Find the last '@' in dsn[:i] - for j = i; j >= 0; j-- { - if dsn[j] == '@' { - // username[:password] - // Find the first ':' in dsn[:j] - for k = 0; k < j; k++ { - if dsn[k] == ':' { - cfg.passwd = dsn[k+1 : j] - break - } - } - cfg.user = dsn[:k] - - break - } - } - - // [protocol[(address)]] - // Find the first '(' in dsn[j+1:i] - for k = j + 1; k < i; k++ { - if dsn[k] == '(' { - // dsn[i-1] must be == ')' if an address is specified - if dsn[i-1] != ')' { - if strings.ContainsRune(dsn[k+1:i], ')') { - return nil, errInvalidDSNUnescaped - } - return nil, errInvalidDSNAddr - } - cfg.addr = dsn[k+1 : i-1] - break - } - } - cfg.net = dsn[j+1 : k] - } - - // dbname[?param1=value1&...¶mN=valueN] - // Find the first '?' in dsn[i+1:] - for j = i + 1; j < len(dsn); j++ { - if dsn[j] == '?' { - if err = parseDSNParams(cfg, dsn[j+1:]); err != nil { - return - } - break - } - } - cfg.dbname = dsn[i+1 : j] - - break - } - } - - if !foundSlash && len(dsn) > 0 { - return nil, errInvalidDSNNoSlash - } - - if cfg.interpolateParams && unsafeCollations[cfg.collation] { - return nil, errInvalidDSNUnsafeCollation - } - - // Set default network if empty - if cfg.net == "" { - cfg.net = "tcp" - } - - // Set default address if empty - if cfg.addr == "" { - switch cfg.net { - case "tcp": - cfg.addr = "127.0.0.1:3306" - case "unix": - cfg.addr = "/tmp/mysql.sock" - default: - return nil, errors.New("Default addr for network '" + cfg.net + "' unknown") - } - - } - - return -} - -// parseDSNParams parses the DSN "query string" -// Values must be url.QueryEscape'ed -func parseDSNParams(cfg *config, params string) (err error) { - for _, v := range strings.Split(params, "&") { - param := strings.SplitN(v, "=", 2) - if len(param) != 2 { - continue - } - - // cfg params - switch value := param[1]; param[0] { - - // Enable client side placeholder substitution - case "interpolateParams": - var isBool bool - cfg.interpolateParams, isBool = readBool(value) - if !isBool { - return fmt.Errorf("Invalid Bool value: %s", value) - } - - // Disable INFILE whitelist / enable all files - case "allowAllFiles": - var isBool bool - cfg.allowAllFiles, isBool = readBool(value) - if !isBool { - return fmt.Errorf("Invalid Bool value: %s", value) - } - - // Use cleartext authentication mode (MySQL 5.5.10+) - case "allowCleartextPasswords": - var isBool bool - cfg.allowCleartextPasswords, isBool = readBool(value) - if !isBool { - return fmt.Errorf("Invalid Bool value: %s", value) - } - - // Use old authentication mode (pre MySQL 4.1) - case "allowOldPasswords": - var isBool bool - cfg.allowOldPasswords, isBool = readBool(value) - if !isBool { - return fmt.Errorf("Invalid Bool value: %s", value) - } - - // Switch "rowsAffected" mode - case "clientFoundRows": - var isBool bool - cfg.clientFoundRows, isBool = readBool(value) - if !isBool { - return fmt.Errorf("Invalid Bool value: %s", value) - } - - // Collation - case "collation": - collation, ok := collations[value] - if !ok { - // Note possibility for false negatives: - // could be triggered although the collation is valid if the - // collations map does not contain entries the server supports. - err = errors.New("unknown collation") - return - } - cfg.collation = collation - break - - case "columnsWithAlias": - var isBool bool - cfg.columnsWithAlias, isBool = readBool(value) - if !isBool { - return fmt.Errorf("Invalid Bool value: %s", value) - } - - // Time Location - case "loc": - if value, err = url.QueryUnescape(value); err != nil { - return - } - cfg.loc, err = time.LoadLocation(value) - if err != nil { - return - } - - // Dial Timeout - case "timeout": - cfg.timeout, err = time.ParseDuration(value) - if err != nil { - return - } - - // TLS-Encryption - case "tls": - boolValue, isBool := readBool(value) - if isBool { - if boolValue { - cfg.tls = &tls.Config{} - } - } else if value, err := url.QueryUnescape(value); err != nil { - return fmt.Errorf("Invalid value for tls config name: %v", err) - } else { - if strings.ToLower(value) == "skip-verify" { - cfg.tls = &tls.Config{InsecureSkipVerify: true} - } else if tlsConfig, ok := tlsConfigRegister[value]; ok { - if len(tlsConfig.ServerName) == 0 && !tlsConfig.InsecureSkipVerify { - host, _, err := net.SplitHostPort(cfg.addr) - if err == nil { - tlsConfig.ServerName = host - } - } - - cfg.tls = tlsConfig - } else { - return fmt.Errorf("Invalid value / unknown config name: %s", value) - } - } - - default: - // lazy init - if cfg.params == nil { - cfg.params = make(map[string]string) - } - - if cfg.params[param[0]], err = url.QueryUnescape(value); err != nil { - return - } - } - } - - return -} - // Returns the bool value of the input. // The 2nd return value indicates if the input was a valid bool value func readBool(input string) (value bool, valid bool) { diff --git a/utils_test.go b/utils_test.go index 8ff905069..0d6c6684f 100644 --- a/utils_test.go +++ b/utils_test.go @@ -10,179 +10,12 @@ package mysql import ( "bytes" - "crypto/tls" "encoding/binary" "fmt" - "net/url" "testing" "time" ) -var testDSNs = []struct { - in string - out string - loc *time.Location -}{ - {"username:password@protocol(address)/dbname?param=value", "&{user:username passwd:password net:protocol addr:address dbname:dbname params:map[param:value] loc:%p tls: timeout:0 collation:33 allowAllFiles:false allowOldPasswords:false allowCleartextPasswords:false clientFoundRows:false columnsWithAlias:false interpolateParams:false}", time.UTC}, - {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", "&{user:username passwd:password net:protocol addr:address dbname:dbname params:map[param:value] loc:%p tls: timeout:0 collation:33 allowAllFiles:false allowOldPasswords:false allowCleartextPasswords:false clientFoundRows:false columnsWithAlias:true interpolateParams:false}", time.UTC}, - {"user@unix(/path/to/socket)/dbname?charset=utf8", "&{user:user passwd: net:unix addr:/path/to/socket dbname:dbname params:map[charset:utf8] loc:%p tls: timeout:0 collation:33 allowAllFiles:false allowOldPasswords:false allowCleartextPasswords:false clientFoundRows:false columnsWithAlias:false interpolateParams:false}", time.UTC}, - {"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{user:user passwd:password net:tcp addr:localhost:5555 dbname:dbname params:map[charset:utf8] loc:%p tls: timeout:0 collation:33 allowAllFiles:false allowOldPasswords:false allowCleartextPasswords:false clientFoundRows:false columnsWithAlias:false interpolateParams:false}", time.UTC}, - {"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{user:user passwd:password net:tcp addr:localhost:5555 dbname:dbname params:map[charset:utf8mb4,utf8] loc:%p tls: timeout:0 collation:33 allowAllFiles:false allowOldPasswords:false allowCleartextPasswords:false clientFoundRows:false columnsWithAlias:false interpolateParams:false}", time.UTC}, - {"user:password@/dbname?loc=UTC&timeout=30s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", "&{user:user passwd:password net:tcp addr:127.0.0.1:3306 dbname:dbname params:map[] loc:%p tls: timeout:30000000000 collation:224 allowAllFiles:true allowOldPasswords:true allowCleartextPasswords:false clientFoundRows:true columnsWithAlias:false interpolateParams:false}", time.UTC}, - {"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{user:user passwd:p@ss(word) net:tcp addr:[de:ad:be:ef::ca:fe]:80 dbname:dbname params:map[] loc:%p tls: timeout:0 collation:33 allowAllFiles:false allowOldPasswords:false allowCleartextPasswords:false clientFoundRows:false columnsWithAlias:false interpolateParams:false}", time.Local}, - {"/dbname", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname:dbname params:map[] loc:%p tls: timeout:0 collation:33 allowAllFiles:false allowOldPasswords:false allowCleartextPasswords:false clientFoundRows:false columnsWithAlias:false interpolateParams:false}", time.UTC}, - {"@/", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname: params:map[] loc:%p tls: timeout:0 collation:33 allowAllFiles:false allowOldPasswords:false allowCleartextPasswords:false clientFoundRows:false columnsWithAlias:false interpolateParams:false}", time.UTC}, - {"/", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname: params:map[] loc:%p tls: timeout:0 collation:33 allowAllFiles:false allowOldPasswords:false allowCleartextPasswords:false clientFoundRows:false columnsWithAlias:false interpolateParams:false}", time.UTC}, - {"", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname: params:map[] loc:%p tls: timeout:0 collation:33 allowAllFiles:false allowOldPasswords:false allowCleartextPasswords:false clientFoundRows:false columnsWithAlias:false interpolateParams:false}", time.UTC}, - {"user:p@/ssword@/", "&{user:user passwd:p@/ssword net:tcp addr:127.0.0.1:3306 dbname: params:map[] loc:%p tls: timeout:0 collation:33 allowAllFiles:false allowOldPasswords:false allowCleartextPasswords:false clientFoundRows:false columnsWithAlias:false interpolateParams:false}", time.UTC}, - {"unix/?arg=%2Fsome%2Fpath.ext", "&{user: passwd: net:unix addr:/tmp/mysql.sock dbname: params:map[arg:/some/path.ext] loc:%p tls: timeout:0 collation:33 allowAllFiles:false allowOldPasswords:false allowCleartextPasswords:false clientFoundRows:false columnsWithAlias:false interpolateParams:false}", time.UTC}, -} - -func TestDSNParser(t *testing.T) { - var cfg *config - var err error - var res string - - for i, tst := range testDSNs { - cfg, err = parseDSN(tst.in) - if err != nil { - t.Error(err.Error()) - } - - // pointer not static - cfg.tls = nil - - res = fmt.Sprintf("%+v", cfg) - if res != fmt.Sprintf(tst.out, tst.loc) { - t.Errorf("%d. parseDSN(%q) => %q, want %q", i, tst.in, res, fmt.Sprintf(tst.out, tst.loc)) - } - } -} - -func TestDSNParserInvalid(t *testing.T) { - var invalidDSNs = []string{ - "@net(addr/", // no closing brace - "@tcp(/", // no closing brace - "tcp(/", // no closing brace - "(/", // no closing brace - "net(addr)//", // unescaped - "user:pass@tcp(1.2.3.4:3306)", // no trailing slash - //"/dbname?arg=/some/unescaped/path", - } - - for i, tst := range invalidDSNs { - if _, err := parseDSN(tst); err == nil { - t.Errorf("invalid DSN #%d. (%s) didn't error!", i, tst) - } - } -} - -func TestDSNWithCustomTLS(t *testing.T) { - baseDSN := "user:password@tcp(localhost:5555)/dbname?tls=" - tlsCfg := tls.Config{} - - RegisterTLSConfig("utils_test", &tlsCfg) - - // Custom TLS is missing - tst := baseDSN + "invalid_tls" - cfg, err := parseDSN(tst) - if err == nil { - t.Errorf("Invalid custom TLS in DSN (%s) but did not error. Got config: %#v", tst, cfg) - } - - tst = baseDSN + "utils_test" - - // Custom TLS with a server name - name := "foohost" - tlsCfg.ServerName = name - cfg, err = parseDSN(tst) - - if err != nil { - t.Error(err.Error()) - } else if cfg.tls.ServerName != name { - t.Errorf("Did not get the correct TLS ServerName (%s) parsing DSN (%s).", name, tst) - } - - // Custom TLS without a server name - name = "localhost" - tlsCfg.ServerName = "" - cfg, err = parseDSN(tst) - - if err != nil { - t.Error(err.Error()) - } else if cfg.tls.ServerName != name { - t.Errorf("Did not get the correct ServerName (%s) parsing DSN (%s).", name, tst) - } - - DeregisterTLSConfig("utils_test") -} - -func TestDSNWithCustomTLS_queryEscape(t *testing.T) { - const configKey = "&%!:" - dsn := "user:password@tcp(localhost:5555)/dbname?tls=" + url.QueryEscape(configKey) - name := "foohost" - tlsCfg := tls.Config{ServerName: name} - - RegisterTLSConfig(configKey, &tlsCfg) - - cfg, err := parseDSN(dsn) - - if err != nil { - t.Error(err.Error()) - } else if cfg.tls.ServerName != name { - t.Errorf("Did not get the correct TLS ServerName (%s) parsing DSN (%s).", name, dsn) - } -} - -func TestDSNUnsafeCollation(t *testing.T) { - _, err := parseDSN("/dbname?collation=gbk_chinese_ci&interpolateParams=true") - if err != errInvalidDSNUnsafeCollation { - t.Errorf("Expected %v, Got %v", errInvalidDSNUnsafeCollation, err) - } - - _, err = parseDSN("/dbname?collation=gbk_chinese_ci&interpolateParams=false") - if err != nil { - t.Errorf("Expected %v, Got %v", nil, err) - } - - _, err = parseDSN("/dbname?collation=gbk_chinese_ci") - if err != nil { - t.Errorf("Expected %v, Got %v", nil, err) - } - - _, err = parseDSN("/dbname?collation=ascii_bin&interpolateParams=true") - if err != nil { - t.Errorf("Expected %v, Got %v", nil, err) - } - - _, err = parseDSN("/dbname?collation=latin1_german1_ci&interpolateParams=true") - if err != nil { - t.Errorf("Expected %v, Got %v", nil, err) - } - - _, err = parseDSN("/dbname?collation=utf8_general_ci&interpolateParams=true") - if err != nil { - t.Errorf("Expected %v, Got %v", nil, err) - } - - _, err = parseDSN("/dbname?collation=utf8mb4_general_ci&interpolateParams=true") - if err != nil { - t.Errorf("Expected %v, Got %v", nil, err) - } -} - -func BenchmarkParseDSN(b *testing.B) { - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - for _, tst := range testDSNs { - if _, err := parseDSN(tst.in); err != nil { - b.Error(err.Error()) - } - } - } -} - func TestScanNullTime(t *testing.T) { var scanTests = []struct { in interface{} From ec658e9d0e971fafb335b79ddacb69e8d395d100 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Wed, 13 Jan 2016 14:37:47 +0100 Subject: [PATCH 10/65] Fix getSystemVar with parseTime=true --- connection.go | 1 + 1 file changed, 1 insertion(+) diff --git a/connection.go b/connection.go index 82fee76a0..c708796f8 100644 --- a/connection.go +++ b/connection.go @@ -353,6 +353,7 @@ func (mc *mysqlConn) getSystemVar(name string) ([]byte, error) { if err == nil { rows := new(textRows) rows.mc = mc + rows.columns = []mysqlField{{fieldType: fieldTypeVarChar}} if resLen > 0 { // Columns From de0bbfa4d67d14b1200efaba0cb6bd5e5181e8d5 Mon Sep 17 00:00:00 2001 From: Chakrit Wichian Date: Fri, 25 Sep 2015 15:03:51 +0700 Subject: [PATCH 11/65] read_timeout and write_timeout --- benchmark_test.go | 12 +++++++----- buffer.go | 12 +++++++++++- driver.go | 2 +- dsn.go | 35 +++++++++++++++++++++++++---------- dsn_test.go | 26 +++++++++++++------------- packets.go | 6 ++++++ 6 files changed, 63 insertions(+), 30 deletions(-) diff --git a/benchmark_test.go b/benchmark_test.go index 7ccb46fcc..bcceedfdb 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -215,14 +215,16 @@ func BenchmarkRoundtripBin(b *testing.B) { } func BenchmarkInterpolation(b *testing.B) { + cfg := &Config{ + InterpolateParams: true, + Loc: time.UTC, + } + mc := &mysqlConn{ - cfg: &Config{ - InterpolateParams: true, - Loc: time.UTC, - }, + cfg: cfg, maxPacketAllowed: maxPacketSize, maxWriteSize: maxPacketSize - 1, - buf: newBuffer(nil), + buf: newBuffer(nil, cfg), } args := []driver.Value{ diff --git a/buffer.go b/buffer.go index 509ce89e4..060f5285c 100644 --- a/buffer.go +++ b/buffer.go @@ -9,6 +9,8 @@ package mysql import "io" +import "net" +import "time" const defaultBufSize = 4096 @@ -22,13 +24,15 @@ type buffer struct { rd io.Reader idx int length int + cfg *Config } -func newBuffer(rd io.Reader) buffer { +func newBuffer(rd io.Reader, cfg *Config) buffer { var b [defaultBufSize]byte return buffer{ buf: b[:], rd: rd, + cfg: cfg, } } @@ -54,6 +58,12 @@ func (b *buffer) fill(need int) error { b.idx = 0 for { + if conn, ok := b.rd.(net.Conn); ok && b.cfg.ReadTimeout > 0 { + if err := conn.SetReadDeadline(time.Now().Add(b.cfg.ReadTimeout)); err != nil { + return err + } + } + nn, err := b.rd.Read(b.buf[n:]) n += nn diff --git a/driver.go b/driver.go index 1d7723b82..ef412844f 100644 --- a/driver.go +++ b/driver.go @@ -81,7 +81,7 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) { } } - mc.buf = newBuffer(mc.netConn) + mc.buf = newBuffer(mc.netConn, mc.cfg) // Reading Handshake Initialization Packet cipher, err := mc.readInitPacket() diff --git a/dsn.go b/dsn.go index 45b4899d9..0cf269ba5 100644 --- a/dsn.go +++ b/dsn.go @@ -27,16 +27,18 @@ var ( // Config is a configuration parsed from a DSN string type Config struct { - User string // Username - Passwd string // Password - Net string // Network type - Addr string // Network address - DBName string // Database name - Params map[string]string // Connection parameters - Loc *time.Location // Location for time.Time values - TLS *tls.Config // TLS configuration - Timeout time.Duration // Dial timeout - Collation uint8 // Connection collation + User string // Username + Passwd string // Password + Net string // Network type + Addr string // Network address + DBName string // Database name + Params map[string]string // Connection parameters + Loc *time.Location // Location for time.Time values + TLS *tls.Config // TLS configuration + Timeout time.Duration // Dial timeout + ReadTimeout time.Duration // I/O read timeout + WriteTimeout time.Duration // I/O write timeout + Collation uint8 // Connection collation AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE AllowCleartextPasswords bool // Allows the cleartext client side plugin @@ -256,6 +258,19 @@ func parseDSNParams(cfg *Config, params string) (err error) { return } + // I/O Timeouts + case "read_timeout": + cfg.ReadTimeout, err = time.ParseDuration(value) + if err != nil { + return + } + + case "write_timeout": + cfg.WriteTimeout, err = time.ParseDuration(value) + if err != nil { + return + } + // TLS-Encryption case "tls": boolValue, isBool := readBool(value) diff --git a/dsn_test.go b/dsn_test.go index 4ac1f562c..9a0ee99c3 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -19,19 +19,19 @@ var testDSNs = []struct { in string out string }{ - {"username:password@protocol(address)/dbname?param=value", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false ParseTime:false Strict:false}"}, - {"user@unix(/path/to/socket)/dbname?charset=utf8", "&{User:user Passwd: Net:unix Addr:/path/to/socket DBName:dbname Params:map[charset:utf8] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8mb4,utf8] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"user:password@/dbname?loc=UTC&timeout=30s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", "&{User:user Passwd:password Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Loc:UTC TLS: Timeout:30s Collation:224 AllowAllFiles:true AllowCleartextPasswords:false AllowOldPasswords:true ClientFoundRows:true ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{User:user Passwd:p@ss(word) Net:tcp Addr:[de:ad:be:ef::ca:fe]:80 DBName:dbname Params:map[] Loc:Local TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"/dbname", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"@/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"user:p@/ssword@/", "&{User:user Passwd:p@/ssword Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"unix/?arg=%2Fsome%2Fpath.ext", "&{User: Passwd: Net:unix Addr:/tmp/mysql.sock DBName: Params:map[arg:/some/path.ext] Loc:UTC TLS: Timeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"username:password@protocol(address)/dbname?param=value", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false ParseTime:false Strict:false}"}, + {"user@unix(/path/to/socket)/dbname?charset=utf8", "&{User:user Passwd: Net:unix Addr:/path/to/socket DBName:dbname Params:map[charset:utf8] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8mb4,utf8] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"user:password@/dbname?loc=UTC&timeout=30s&read_timeout=1s&write_timeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", "&{User:user Passwd:password Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Loc:UTC TLS: Timeout:30s ReadTimeout:1s WriteTimeout:1s Collation:224 AllowAllFiles:true AllowCleartextPasswords:false AllowOldPasswords:true ClientFoundRows:true ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{User:user Passwd:p@ss(word) Net:tcp Addr:[de:ad:be:ef::ca:fe]:80 DBName:dbname Params:map[] Loc:Local TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"/dbname", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"@/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"user:p@/ssword@/", "&{User:user Passwd:p@/ssword Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"unix/?arg=%2Fsome%2Fpath.ext", "&{User: Passwd: Net:unix Addr:/tmp/mysql.sock DBName: Params:map[arg:/some/path.ext] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, } func TestDSNParser(t *testing.T) { diff --git a/packets.go b/packets.go index 532c56c96..81b2326ee 100644 --- a/packets.go +++ b/packets.go @@ -100,6 +100,12 @@ func (mc *mysqlConn) writePacket(data []byte) error { data[3] = mc.sequence // Write packet + if mc.cfg.WriteTimeout > 0 { + if err := mc.netConn.SetWriteDeadline(time.Now().Add(mc.cfg.WriteTimeout)); err != nil { + return err + } + } + n, err := mc.netConn.Write(data[:4+size]) if err == nil && n == 4+size { mc.sequence++ From 68ca989dd40fb96ae66976689f8df4b696fb0765 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Tue, 12 Jan 2016 23:12:54 +0100 Subject: [PATCH 12/65] Improve timeouts implementation - use local attributes for timeouts - do not type assert to net.Conn each time - DSN params are CamelCase --- benchmark_test.go | 12 +++++------- buffer.go | 29 +++++++++++++++-------------- connection.go | 3 ++- driver.go | 6 +++++- dsn.go | 27 ++++++++++++++------------- dsn_test.go | 2 +- packets.go | 6 +++--- 7 files changed, 45 insertions(+), 40 deletions(-) diff --git a/benchmark_test.go b/benchmark_test.go index bcceedfdb..7ccb46fcc 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -215,16 +215,14 @@ func BenchmarkRoundtripBin(b *testing.B) { } func BenchmarkInterpolation(b *testing.B) { - cfg := &Config{ - InterpolateParams: true, - Loc: time.UTC, - } - mc := &mysqlConn{ - cfg: cfg, + cfg: &Config{ + InterpolateParams: true, + Loc: time.UTC, + }, maxPacketAllowed: maxPacketSize, maxWriteSize: maxPacketSize - 1, - buf: newBuffer(nil, cfg), + buf: newBuffer(nil), } args := []driver.Value{ diff --git a/buffer.go b/buffer.go index 060f5285c..2001feacd 100644 --- a/buffer.go +++ b/buffer.go @@ -8,9 +8,11 @@ package mysql -import "io" -import "net" -import "time" +import ( + "io" + "net" + "time" +) const defaultBufSize = 4096 @@ -20,19 +22,18 @@ const defaultBufSize = 4096 // The buffer is similar to bufio.Reader / Writer but zero-copy-ish // Also highly optimized for this particular use case. type buffer struct { - buf []byte - rd io.Reader - idx int - length int - cfg *Config + buf []byte + nc net.Conn + idx int + length int + timeout time.Duration } -func newBuffer(rd io.Reader, cfg *Config) buffer { +func newBuffer(nc net.Conn) buffer { var b [defaultBufSize]byte return buffer{ buf: b[:], - rd: rd, - cfg: cfg, + nc: nc, } } @@ -58,13 +59,13 @@ func (b *buffer) fill(need int) error { b.idx = 0 for { - if conn, ok := b.rd.(net.Conn); ok && b.cfg.ReadTimeout > 0 { - if err := conn.SetReadDeadline(time.Now().Add(b.cfg.ReadTimeout)); err != nil { + if b.timeout > 0 { + if err := b.nc.SetReadDeadline(time.Now().Add(b.timeout)); err != nil { return err } } - nn, err := b.rd.Read(b.buf[n:]) + nn, err := b.nc.Read(b.buf[n:]) n += nn switch err { diff --git a/connection.go b/connection.go index c708796f8..c3899de0e 100644 --- a/connection.go +++ b/connection.go @@ -24,6 +24,7 @@ type mysqlConn struct { cfg *Config maxPacketAllowed int maxWriteSize int + writeTimeout time.Duration flags clientFlag status statusFlag sequence uint8 @@ -98,7 +99,7 @@ func (mc *mysqlConn) cleanup() { mc.netConn = nil } mc.cfg = nil - mc.buf.rd = nil + mc.buf.nc = nil } func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) { diff --git a/driver.go b/driver.go index ef412844f..965b663b7 100644 --- a/driver.go +++ b/driver.go @@ -81,7 +81,11 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) { } } - mc.buf = newBuffer(mc.netConn, mc.cfg) + mc.buf = newBuffer(mc.netConn) + + // Set I/O timeouts + mc.buf.timeout = mc.cfg.ReadTimeout + mc.writeTimeout = mc.cfg.WriteTimeout // Reading Handshake Initialization Packet cipher, err := mc.readInitPacket() diff --git a/dsn.go b/dsn.go index 0cf269ba5..8ca73dc68 100644 --- a/dsn.go +++ b/dsn.go @@ -243,6 +243,13 @@ func parseDSNParams(cfg *Config, params string) (err error) { return errors.New("Invalid Bool value: " + value) } + // I/O read Timeout + case "readTimeout": + cfg.ReadTimeout, err = time.ParseDuration(value) + if err != nil { + return + } + // Strict mode case "strict": var isBool bool @@ -258,19 +265,6 @@ func parseDSNParams(cfg *Config, params string) (err error) { return } - // I/O Timeouts - case "read_timeout": - cfg.ReadTimeout, err = time.ParseDuration(value) - if err != nil { - return - } - - case "write_timeout": - cfg.WriteTimeout, err = time.ParseDuration(value) - if err != nil { - return - } - // TLS-Encryption case "tls": boolValue, isBool := readBool(value) @@ -297,6 +291,13 @@ func parseDSNParams(cfg *Config, params string) (err error) { } } + // I/O write Timeout + case "writeTimeout": + cfg.WriteTimeout, err = time.ParseDuration(value) + if err != nil { + return + } + default: // lazy init if cfg.Params == nil { diff --git a/dsn_test.go b/dsn_test.go index 9a0ee99c3..3e2a4b37a 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -24,7 +24,7 @@ var testDSNs = []struct { {"user@unix(/path/to/socket)/dbname?charset=utf8", "&{User:user Passwd: Net:unix Addr:/path/to/socket DBName:dbname Params:map[charset:utf8] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, {"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, {"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8mb4,utf8] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"user:password@/dbname?loc=UTC&timeout=30s&read_timeout=1s&write_timeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", "&{User:user Passwd:password Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Loc:UTC TLS: Timeout:30s ReadTimeout:1s WriteTimeout:1s Collation:224 AllowAllFiles:true AllowCleartextPasswords:false AllowOldPasswords:true ClientFoundRows:true ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", "&{User:user Passwd:password Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Loc:UTC TLS: Timeout:30s ReadTimeout:1s WriteTimeout:1s Collation:224 AllowAllFiles:true AllowCleartextPasswords:false AllowOldPasswords:true ClientFoundRows:true ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, {"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{User:user Passwd:p@ss(word) Net:tcp Addr:[de:ad:be:ef::ca:fe]:80 DBName:dbname Params:map[] Loc:Local TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, {"/dbname", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, {"@/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, diff --git a/packets.go b/packets.go index 81b2326ee..88d990a2d 100644 --- a/packets.go +++ b/packets.go @@ -100,8 +100,8 @@ func (mc *mysqlConn) writePacket(data []byte) error { data[3] = mc.sequence // Write packet - if mc.cfg.WriteTimeout > 0 { - if err := mc.netConn.SetWriteDeadline(time.Now().Add(mc.cfg.WriteTimeout)); err != nil { + if mc.writeTimeout > 0 { + if err := mc.netConn.SetWriteDeadline(time.Now().Add(mc.writeTimeout)); err != nil { return err } } @@ -284,7 +284,7 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte) error { return err } mc.netConn = tlsConn - mc.buf.rd = tlsConn + mc.buf.nc = tlsConn } // Filler [23 bytes] (all 0x00) From 2cd96dd9ba18cbc7b96e2ba863d9b5b66509056b Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Tue, 12 Jan 2016 23:51:10 +0100 Subject: [PATCH 13/65] Update README.md Add readTimeout and writeTimeout params --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 706b7ef2e..c02b6a245 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,16 @@ Default: false `parseTime=true` changes the output type of `DATE` and `DATETIME` values to `time.Time` instead of `[]byte` / `string` +##### `readTimeout` + +``` +Type: decimal number +Default: 0 +``` + +I/O read timeout. The value must be a string of decimal numbers, each with optional fraction and a unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. + + ##### `strict` ``` @@ -265,6 +275,16 @@ Default: false `tls=true` enables TLS / SSL encrypted connection to the server. Use `skip-verify` if you want to use a self-signed or invalid certificate (server side). Use a custom value registered with [`mysql.RegisterTLSConfig`](http://godoc.org/github.com/go-sql-driver/mysql#RegisterTLSConfig). +##### `writeTimeout` + +``` +Type: decimal number +Default: 0 +``` + +I/O write timeout. The value must be a string of decimal numbers, each with optional fraction and a unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. + + ##### System Variables All other parameters are interpreted as system variables: From f7f9f33777eb46a937c22408a288518520129627 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Tue, 19 Jan 2016 16:03:51 +0100 Subject: [PATCH 14/65] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c02b6a245..f3190e8ab 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ Type: decimal number Default: 0 ``` -I/O read timeout. The value must be a string of decimal numbers, each with optional fraction and a unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. +I/O read timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. ##### `strict` @@ -261,7 +261,7 @@ Type: decimal number Default: OS default ``` -*Driver* side connection timeout. The value must be a string of decimal numbers, each with optional fraction and a unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. To set a server side timeout, use the parameter [`wait_timeout`](http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_wait_timeout). +*Driver* side connection timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. To set a server side timeout, use the parameter [`wait_timeout`](http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_wait_timeout). ##### `tls` @@ -282,7 +282,7 @@ Type: decimal number Default: 0 ``` -I/O write timeout. The value must be a string of decimal numbers, each with optional fraction and a unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. +I/O write timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. ##### System Variables From 618fbd9cc98d2c37b359995ed64e991b6b9e02f1 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Tue, 19 Jan 2016 18:08:36 +0100 Subject: [PATCH 15/65] lint --- driver.go | 4 ++-- errors.go | 2 +- infile.go | 4 ++-- packets.go | 27 +++++++++++++-------------- utils.go | 4 ++-- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/driver.go b/driver.go index 965b663b7..899f955fb 100644 --- a/driver.go +++ b/driver.go @@ -4,7 +4,7 @@ // 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/. -// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// Package mysql provides a MySQL driver for Go's database/sql package // // The driver should be used via the database/sql package: // @@ -22,7 +22,7 @@ import ( "net" ) -// This struct is exported to make the driver directly accessible. +// MySQLDriver is exported to make the driver directly accessible. // In general the driver is used via the database/sql package. type MySQLDriver struct{} diff --git a/errors.go b/errors.go index 44cf30db6..2b12553bd 100644 --- a/errors.go +++ b/errors.go @@ -32,7 +32,7 @@ var ( ErrBusyBuffer = errors.New("Busy buffer") ) -var errLog Logger = log.New(os.Stderr, "[MySQL] ", log.Ldate|log.Ltime|log.Lshortfile) +var errLog = Logger(log.New(os.Stderr, "[MySQL] ", log.Ldate|log.Ltime|log.Lshortfile)) // Logger is used to log critical error messages. type Logger interface { diff --git a/infile.go b/infile.go index 9c898b705..64c3619d8 100644 --- a/infile.go +++ b/infile.go @@ -175,8 +175,8 @@ func (mc *mysqlConn) handleInFileRequest(name string) (err error) { // read OK packet if err == nil { return mc.readResultOK() - } else { - mc.readPacket() } + + mc.readPacket() return err } diff --git a/packets.go b/packets.go index 88d990a2d..29e04a606 100644 --- a/packets.go +++ b/packets.go @@ -47,9 +47,8 @@ func (mc *mysqlConn) readPacket() ([]byte, error) { if data[3] != mc.sequence { if data[3] > mc.sequence { return nil, ErrPktSyncMul - } else { - return nil, ErrPktSync } + return nil, ErrPktSync } mc.sequence++ @@ -539,13 +538,13 @@ func (mc *mysqlConn) handleOkPacket(data []byte) error { // warning count [2 bytes] if !mc.strict { return nil - } else { - pos := 1 + n + m + 2 - if binary.LittleEndian.Uint16(data[pos:pos+2]) > 0 { - return mc.getWarnings() - } - return nil } + + pos := 1 + n + m + 2 + if binary.LittleEndian.Uint16(data[pos:pos+2]) > 0 { + return mc.getWarnings() + } + return nil } // Read Packets as Field Packets until EOF-Packet or an Error appears @@ -742,13 +741,13 @@ func (stmt *mysqlStmt) readPrepareResultPacket() (uint16, error) { // Warning count [16 bit uint] if !stmt.mc.strict { return columnCount, nil - } else { - // Check for warnings count > 0, only available in MySQL > 4.1 - if len(data) >= 12 && binary.LittleEndian.Uint16(data[10:12]) > 0 { - return columnCount, stmt.mc.getWarnings() - } - return columnCount, nil } + + // Check for warnings count > 0, only available in MySQL > 4.1 + if len(data) >= 12 && binary.LittleEndian.Uint16(data[10:12]) > 0 { + return columnCount, stmt.mc.getWarnings() + } + return columnCount, nil } return 0, err } diff --git a/utils.go b/utils.go index e267cce4e..993e17c09 100644 --- a/utils.go +++ b/utils.go @@ -642,7 +642,7 @@ func escapeBytesBackslash(buf, v []byte) []byte { pos += 2 default: buf[pos] = c - pos += 1 + pos++ } } @@ -687,7 +687,7 @@ func escapeStringBackslash(buf []byte, v string) []byte { pos += 2 default: buf[pos] = c - pos += 1 + pos++ } } From ca130be66a8fb9c7f935b7baf95fd225b548282f Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Tue, 19 Jan 2016 18:09:59 +0100 Subject: [PATCH 16/65] lazy init tlsConfigRegister --- utils.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/utils.go b/utils.go index 993e17c09..174e97814 100644 --- a/utils.go +++ b/utils.go @@ -23,10 +23,6 @@ var ( tlsConfigRegister map[string]*tls.Config // Register for custom tls.Configs ) -func init() { - tlsConfigRegister = make(map[string]*tls.Config) -} - // RegisterTLSConfig registers a custom tls.Config to be used with sql.Open. // Use the key as a value in the DSN where tls=value. // @@ -55,13 +51,19 @@ func RegisterTLSConfig(key string, config *tls.Config) error { return fmt.Errorf("Key '%s' is reserved", key) } + if tlsConfigRegister == nil { + tlsConfigRegister = make(map[string]*tls.Config) + } + tlsConfigRegister[key] = config return nil } // DeregisterTLSConfig removes the tls.Config associated with key. func DeregisterTLSConfig(key string) { - delete(tlsConfigRegister, key) + if tlsConfigRegister != nil { + delete(tlsConfigRegister, key) + } } // Returns the bool value of the input. From a059889267dc7170331388008528b3b44479bffb Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Tue, 19 Jan 2016 18:30:29 +0100 Subject: [PATCH 17/65] reformat errors --- dsn.go | 32 ++++++++++++++++---------------- errors.go | 26 +++++++++++++------------- infile.go | 4 ++-- packets.go | 14 +++++++------- utils.go | 10 +++++----- 5 files changed, 43 insertions(+), 43 deletions(-) diff --git a/dsn.go b/dsn.go index 8ca73dc68..31fd530ee 100644 --- a/dsn.go +++ b/dsn.go @@ -19,10 +19,10 @@ import ( ) var ( - errInvalidDSNUnescaped = errors.New("Invalid DSN: Did you forget to escape a param value?") - errInvalidDSNAddr = errors.New("Invalid DSN: Network Address not terminated (missing closing brace)") - errInvalidDSNNoSlash = errors.New("Invalid DSN: Missing the slash separating the database name") - errInvalidDSNUnsafeCollation = errors.New("Invalid DSN: interpolateParams can be used with ascii, latin1, utf8 and utf8mb4 charset") + errInvalidDSNUnescaped = errors.New("invalid DSN: did you forget to escape a param value?") + errInvalidDSNAddr = errors.New("invalid DSN: network address not terminated (missing closing brace)") + errInvalidDSNNoSlash = errors.New("invalid DSN: missing the slash separating the database name") + errInvalidDSNUnsafeCollation = errors.New("invalid DSN: interpolateParams can not be used with unsafe collations") ) // Config is a configuration parsed from a DSN string @@ -141,7 +141,7 @@ func ParseDSN(dsn string) (cfg *Config, err error) { case "unix": cfg.Addr = "/tmp/mysql.sock" default: - return nil, errors.New("Default addr for network '" + cfg.Net + "' unknown") + return nil, errors.New("default addr for network '" + cfg.Net + "' unknown") } } @@ -166,7 +166,7 @@ func parseDSNParams(cfg *Config, params string) (err error) { var isBool bool cfg.AllowAllFiles, isBool = readBool(value) if !isBool { - return fmt.Errorf("Invalid Bool value: %s", value) + return errors.New("invalid bool value: " + value) } // Use cleartext authentication mode (MySQL 5.5.10+) @@ -174,7 +174,7 @@ func parseDSNParams(cfg *Config, params string) (err error) { var isBool bool cfg.AllowCleartextPasswords, isBool = readBool(value) if !isBool { - return fmt.Errorf("Invalid Bool value: %s", value) + return errors.New("invalid bool value: " + value) } // Use old authentication mode (pre MySQL 4.1) @@ -182,7 +182,7 @@ func parseDSNParams(cfg *Config, params string) (err error) { var isBool bool cfg.AllowOldPasswords, isBool = readBool(value) if !isBool { - return fmt.Errorf("Invalid Bool value: %s", value) + return errors.New("invalid bool value: " + value) } // Switch "rowsAffected" mode @@ -190,7 +190,7 @@ func parseDSNParams(cfg *Config, params string) (err error) { var isBool bool cfg.ClientFoundRows, isBool = readBool(value) if !isBool { - return fmt.Errorf("Invalid Bool value: %s", value) + return errors.New("invalid bool value: " + value) } // Collation @@ -210,19 +210,19 @@ func parseDSNParams(cfg *Config, params string) (err error) { var isBool bool cfg.ColumnsWithAlias, isBool = readBool(value) if !isBool { - return fmt.Errorf("Invalid Bool value: %s", value) + return errors.New("invalid bool value: " + value) } // Compression case "compress": - return errors.New("Compression not implemented yet") + return errors.New("compression not implemented yet") // Enable client side placeholder substitution case "interpolateParams": var isBool bool cfg.InterpolateParams, isBool = readBool(value) if !isBool { - return fmt.Errorf("Invalid Bool value: %s", value) + return errors.New("invalid bool value: " + value) } // Time Location @@ -240,7 +240,7 @@ func parseDSNParams(cfg *Config, params string) (err error) { var isBool bool cfg.ParseTime, isBool = readBool(value) if !isBool { - return errors.New("Invalid Bool value: " + value) + return errors.New("invalid bool value: " + value) } // I/O read Timeout @@ -255,7 +255,7 @@ func parseDSNParams(cfg *Config, params string) (err error) { var isBool bool cfg.Strict, isBool = readBool(value) if !isBool { - return errors.New("Invalid Bool value: " + value) + return errors.New("invalid bool value: " + value) } // Dial Timeout @@ -273,7 +273,7 @@ func parseDSNParams(cfg *Config, params string) (err error) { cfg.TLS = &tls.Config{} } } else if value, err := url.QueryUnescape(value); err != nil { - return fmt.Errorf("Invalid value for tls config name: %v", err) + return fmt.Errorf("invalid value for TLS config name: %v", err) } else { if strings.ToLower(value) == "skip-verify" { cfg.TLS = &tls.Config{InsecureSkipVerify: true} @@ -287,7 +287,7 @@ func parseDSNParams(cfg *Config, params string) (err error) { cfg.TLS = tlsConfig } else { - return fmt.Errorf("Invalid value / unknown config name: %s", value) + return errors.New("invalid value / unknown config name: " + value) } } diff --git a/errors.go b/errors.go index 2b12553bd..d1000d469 100644 --- a/errors.go +++ b/errors.go @@ -19,20 +19,20 @@ import ( // Various errors the driver might return. Can change between driver versions. var ( - ErrInvalidConn = errors.New("Invalid Connection") - ErrMalformPkt = errors.New("Malformed Packet") - ErrNoTLS = errors.New("TLS encryption requested but server does not support TLS") - ErrOldPassword = errors.New("This user requires old password authentication. If you still want to use it, please add 'allowOldPasswords=1' to your DSN. See also https://github.com/go-sql-driver/mysql/wiki/old_passwords") - ErrCleartextPassword = errors.New("This user requires clear text authentication. If you still want to use it, please add 'allowCleartextPasswords=1' to your DSN.") - ErrUnknownPlugin = errors.New("The authentication plugin is not supported.") - ErrOldProtocol = errors.New("MySQL-Server does not support required Protocol 41+") - ErrPktSync = errors.New("Commands out of sync. You can't run this command now") - ErrPktSyncMul = errors.New("Commands out of sync. Did you run multiple statements at once?") - ErrPktTooLarge = errors.New("Packet for query is too large. You can change this value on the server by adjusting the 'max_allowed_packet' variable.") - ErrBusyBuffer = errors.New("Busy buffer") + ErrInvalidConn = errors.New("invalid connection") + ErrMalformPkt = errors.New("malformed packet") + ErrNoTLS = errors.New("TLS requested but server does not support TLS") + ErrOldPassword = errors.New("this user requires old password authentication. If you still want to use it, please add 'allowOldPasswords=1' to your DSN. See also https://github.com/go-sql-driver/mysql/wiki/old_passwords") + ErrCleartextPassword = errors.New("this user requires clear text authentication. If you still want to use it, please add 'allowCleartextPasswords=1' to your DSN") + ErrUnknownPlugin = errors.New("this authentication plugin is not supported") + ErrOldProtocol = errors.New("MySQL server does not support required protocol 41+") + ErrPktSync = errors.New("commands out of sync. You can't run this command now") + ErrPktSyncMul = errors.New("commands out of sync. Did you run multiple statements at once?") + ErrPktTooLarge = errors.New("packet for query is too large. Try adjusting the 'max_allowed_packet' variable on the server") + ErrBusyBuffer = errors.New("busy buffer") ) -var errLog = Logger(log.New(os.Stderr, "[MySQL] ", log.Ldate|log.Ltime|log.Lshortfile)) +var errLog = Logger(log.New(os.Stderr, "[mysql] ", log.Ldate|log.Ltime|log.Lshortfile)) // Logger is used to log critical error messages. type Logger interface { @@ -56,7 +56,7 @@ type MySQLError struct { } func (me *MySQLError) Error() string { - return fmt.Sprintf("Error %d: %s", me.Number, me.Message) + return fmt.Sprintf("error %d: %s", me.Number, me.Message) } // MySQLWarnings is an error type which represents a group of one or more MySQL diff --git a/infile.go b/infile.go index 64c3619d8..e3e5e47d9 100644 --- a/infile.go +++ b/infile.go @@ -139,12 +139,12 @@ func (mc *mysqlConn) handleInFileRequest(name string) (err error) { } else if fileSize <= mc.maxPacketAllowed { data = make([]byte, 4+mc.maxWriteSize) } else { - err = fmt.Errorf("Local File '%s' too large: Size: %d, Max: %d", name, fileSize, mc.maxPacketAllowed) + err = fmt.Errorf("local file '%s' too large: size: %d, max: %d", name, fileSize, mc.maxPacketAllowed) } } } } else { - err = fmt.Errorf("Local File '%s' is not registered. Use the DSN parameter 'allowAllFiles=true' to allow all files", name) + err = fmt.Errorf("local file '%s' is not registered", name) } } diff --git a/packets.go b/packets.go index 29e04a606..96f4478da 100644 --- a/packets.go +++ b/packets.go @@ -145,7 +145,7 @@ func (mc *mysqlConn) readInitPacket() ([]byte, error) { // protocol version [1 byte] if data[0] < minProtocolVersion { return nil, fmt.Errorf( - "Unsupported MySQL Protocol Version %d. Protocol Version %d or higher is required", + "unsupported protocol version %d. Version %d or higher is required", data[0], minProtocolVersion, ) @@ -563,7 +563,7 @@ func (mc *mysqlConn) readColumns(count int) ([]mysqlField, error) { if i == count { return columns, nil } - return nil, fmt.Errorf("ColumnsCount mismatch n:%d len:%d", count, len(columns)) + return nil, fmt.Errorf("columns count mismatch n:%d len:%d", count, len(columns)) } // Catalog @@ -809,7 +809,7 @@ func (stmt *mysqlStmt) writeCommandLongData(paramID int, arg []byte) error { func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error { if len(args) != stmt.paramCount { return fmt.Errorf( - "Arguments count mismatch (Got: %d Has: %d)", + "arguments count mismatch (got: %d; has: %d)", len(args), stmt.paramCount, ) @@ -995,7 +995,7 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error { paramValues = append(paramValues, val...) default: - return fmt.Errorf("Can't convert type: %T", arg) + return fmt.Errorf("can not convert type: %T", arg) } } @@ -1143,7 +1143,7 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { dstlen = 8 + 1 + decimals default: return fmt.Errorf( - "MySQL protocol error, illegal decimals value %d", + "protocol error, illegal decimals value %d", rows.columns[i].decimals, ) } @@ -1162,7 +1162,7 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { dstlen = 19 + 1 + decimals default: return fmt.Errorf( - "MySQL protocol error, illegal decimals value %d", + "protocol error, illegal decimals value %d", rows.columns[i].decimals, ) } @@ -1179,7 +1179,7 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { // Please report if this happens! default: - return fmt.Errorf("Unknown FieldType %d", rows.columns[i].fieldType) + return fmt.Errorf("unknown field type %d", rows.columns[i].fieldType) } } diff --git a/utils.go b/utils.go index 174e97814..d523b7ffd 100644 --- a/utils.go +++ b/utils.go @@ -48,7 +48,7 @@ var ( // func RegisterTLSConfig(key string, config *tls.Config) error { if _, isBool := readBool(key); isBool || strings.ToLower(key) == "skip-verify" { - return fmt.Errorf("Key '%s' is reserved", key) + return fmt.Errorf("key '%s' is reserved", key) } if tlsConfigRegister == nil { @@ -260,7 +260,7 @@ func parseDateTime(str string, loc *time.Location) (t time.Time, err error) { } t, err = time.Parse(timeFormat[:len(str)], str) default: - err = fmt.Errorf("Invalid Time-String: %s", str) + err = fmt.Errorf("invalid time string: %s", str) return } @@ -309,7 +309,7 @@ func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Va loc, ), nil } - return nil, fmt.Errorf("Invalid DATETIME-packet length %d", num) + return nil, fmt.Errorf("invalid DATETIME packet length %d", num) } // zeroDateTime is used in formatBinaryDateTime to avoid an allocation @@ -344,7 +344,7 @@ func formatBinaryDateTime(src []byte, length uint8, justTime bool) (driver.Value switch len(src) { case 8, 12: default: - return nil, fmt.Errorf("Invalid TIME-packet length %d", len(src)) + return nil, fmt.Errorf("invalid TIME packet length %d", len(src)) } // +2 to enable negative time and 100+ hours dst = make([]byte, 0, length+2) @@ -378,7 +378,7 @@ func formatBinaryDateTime(src []byte, length uint8, justTime bool) (driver.Value if length > 10 { t += "TIME" } - return nil, fmt.Errorf("illegal %s-packet length %d", t, len(src)) + return nil, fmt.Errorf("illegal %s packet length %d", t, len(src)) } dst = make([]byte, 0, length) // start with the date From 4cad0fb5065f5b398585a791b14bdbeadfe97739 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Tue, 19 Jan 2016 18:43:35 +0100 Subject: [PATCH 18/65] Revert formating of MySQLError.Error() users may rely on it --- errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/errors.go b/errors.go index d1000d469..1543a8054 100644 --- a/errors.go +++ b/errors.go @@ -56,7 +56,7 @@ type MySQLError struct { } func (me *MySQLError) Error() string { - return fmt.Sprintf("error %d: %s", me.Number, me.Message) + return fmt.Sprintf("Error %d: %s", me.Number, me.Message) } // MySQLWarnings is an error type which represents a group of one or more MySQL From 11d51abbce1f7261a76b89ea423b2dae3d1c5a64 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Tue, 19 Jan 2016 18:45:42 +0100 Subject: [PATCH 19/65] reformat errors in tests --- benchmark_test.go | 4 +- driver_test.go | 164 +++++++++++++++++++++++----------------------- 2 files changed, 84 insertions(+), 84 deletions(-) diff --git a/benchmark_test.go b/benchmark_test.go index 7ccb46fcc..8f721139b 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -49,9 +49,9 @@ func initDB(b *testing.B, queries ...string) *sql.DB { for _, query := range queries { if _, err := db.Exec(query); err != nil { if w, ok := err.(MySQLWarnings); ok { - b.Logf("Warning on %q: %v", query, w) + b.Logf("warning on %q: %v", query, w) } else { - b.Fatalf("Error on %q: %v", query, err) + b.Fatalf("error on %q: %v", query, err) } } } diff --git a/driver_test.go b/driver_test.go index 0e9571a59..319d34f91 100644 --- a/driver_test.go +++ b/driver_test.go @@ -78,12 +78,12 @@ type DBTest struct { func runTests(t *testing.T, dsn string, tests ...func(dbt *DBTest)) { if !available { - t.Skipf("MySQL-Server not running on %s", netAddr) + t.Skipf("MySQL server not running on %s", netAddr) } db, err := sql.Open("mysql", dsn) if err != nil { - t.Fatalf("Error connecting: %s", err.Error()) + t.Fatalf("error connecting: %s", err.Error()) } defer db.Close() @@ -94,7 +94,7 @@ func runTests(t *testing.T, dsn string, tests ...func(dbt *DBTest)) { if _, err := ParseDSN(dsn2); err != errInvalidDSNUnsafeCollation { db2, err = sql.Open("mysql", dsn2) if err != nil { - t.Fatalf("Error connecting: %s", err.Error()) + t.Fatalf("error connecting: %s", err.Error()) } defer db2.Close() } @@ -115,13 +115,13 @@ func (dbt *DBTest) fail(method, query string, err error) { if len(query) > 300 { query = "[query too large to print]" } - dbt.Fatalf("Error on %s %s: %s", method, query, err.Error()) + dbt.Fatalf("error on %s %s: %s", method, query, err.Error()) } func (dbt *DBTest) mustExec(query string, args ...interface{}) (res sql.Result) { res, err := dbt.db.Exec(query, args...) if err != nil { - dbt.fail("Exec", query, err) + dbt.fail("exec", query, err) } return res } @@ -129,7 +129,7 @@ func (dbt *DBTest) mustExec(query string, args ...interface{}) (res sql.Result) func (dbt *DBTest) mustQuery(query string, args ...interface{}) (rows *sql.Rows) { rows, err := dbt.db.Query(query, args...) if err != nil { - dbt.fail("Query", query, err) + dbt.fail("query", query, err) } return rows } @@ -140,7 +140,7 @@ func TestEmptyQuery(t *testing.T) { rows := dbt.mustQuery("--") // will hang before #255 if rows.Next() { - dbt.Errorf("Next on rows must be false") + dbt.Errorf("next on rows must be false") } }) } @@ -164,7 +164,7 @@ func TestCRUD(t *testing.T) { dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) } if count != 1 { - dbt.Fatalf("Expected 1 affected row, got %d", count) + dbt.Fatalf("expected 1 affected row, got %d", count) } id, err := res.LastInsertId() @@ -172,7 +172,7 @@ func TestCRUD(t *testing.T) { dbt.Fatalf("res.LastInsertId() returned error: %s", err.Error()) } if id != 0 { - dbt.Fatalf("Expected InsertID 0, got %d", id) + dbt.Fatalf("expected InsertId 0, got %d", id) } // Read @@ -197,7 +197,7 @@ func TestCRUD(t *testing.T) { dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) } if count != 1 { - dbt.Fatalf("Expected 1 affected row, got %d", count) + dbt.Fatalf("expected 1 affected row, got %d", count) } // Check Update @@ -222,7 +222,7 @@ func TestCRUD(t *testing.T) { dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) } if count != 1 { - dbt.Fatalf("Expected 1 affected row, got %d", count) + dbt.Fatalf("expected 1 affected row, got %d", count) } // Check for unexpected rows @@ -232,7 +232,7 @@ func TestCRUD(t *testing.T) { dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) } if count != 0 { - dbt.Fatalf("Expected 0 affected row, got %d", count) + dbt.Fatalf("expected 0 affected row, got %d", count) } }) } @@ -653,14 +653,14 @@ func TestNULL(t *testing.T) { dbt.Fatal(err) } if nb.Valid { - dbt.Error("Valid NullBool which should be invalid") + dbt.Error("valid NullBool which should be invalid") } // Valid if err = nonNullStmt.QueryRow().Scan(&nb); err != nil { dbt.Fatal(err) } if !nb.Valid { - dbt.Error("Invalid NullBool which should be valid") + dbt.Error("invalid NullBool which should be valid") } else if nb.Bool != true { dbt.Errorf("Unexpected NullBool value: %t (should be true)", nb.Bool) } @@ -672,16 +672,16 @@ func TestNULL(t *testing.T) { dbt.Fatal(err) } if nf.Valid { - dbt.Error("Valid NullFloat64 which should be invalid") + dbt.Error("valid NullFloat64 which should be invalid") } // Valid if err = nonNullStmt.QueryRow().Scan(&nf); err != nil { dbt.Fatal(err) } if !nf.Valid { - dbt.Error("Invalid NullFloat64 which should be valid") + dbt.Error("invalid NullFloat64 which should be valid") } else if nf.Float64 != float64(1) { - dbt.Errorf("Unexpected NullFloat64 value: %f (should be 1.0)", nf.Float64) + dbt.Errorf("unexpected NullFloat64 value: %f (should be 1.0)", nf.Float64) } // NullInt64 @@ -691,16 +691,16 @@ func TestNULL(t *testing.T) { dbt.Fatal(err) } if ni.Valid { - dbt.Error("Valid NullInt64 which should be invalid") + dbt.Error("valid NullInt64 which should be invalid") } // Valid if err = nonNullStmt.QueryRow().Scan(&ni); err != nil { dbt.Fatal(err) } if !ni.Valid { - dbt.Error("Invalid NullInt64 which should be valid") + dbt.Error("invalid NullInt64 which should be valid") } else if ni.Int64 != int64(1) { - dbt.Errorf("Unexpected NullInt64 value: %d (should be 1)", ni.Int64) + dbt.Errorf("unexpected NullInt64 value: %d (should be 1)", ni.Int64) } // NullString @@ -710,16 +710,16 @@ func TestNULL(t *testing.T) { dbt.Fatal(err) } if ns.Valid { - dbt.Error("Valid NullString which should be invalid") + dbt.Error("valid NullString which should be invalid") } // Valid if err = nonNullStmt.QueryRow().Scan(&ns); err != nil { dbt.Fatal(err) } if !ns.Valid { - dbt.Error("Invalid NullString which should be valid") + dbt.Error("invalid NullString which should be valid") } else if ns.String != `1` { - dbt.Error("Unexpected NullString value:" + ns.String + " (should be `1`)") + dbt.Error("unexpected NullString value:" + ns.String + " (should be `1`)") } // nil-bytes @@ -729,14 +729,14 @@ func TestNULL(t *testing.T) { dbt.Fatal(err) } if b != nil { - dbt.Error("Non-nil []byte wich should be nil") + dbt.Error("non-nil []byte wich should be nil") } // Read non-nil if err = nonNullStmt.QueryRow().Scan(&b); err != nil { dbt.Fatal(err) } if b == nil { - dbt.Error("Nil []byte wich should be non-nil") + dbt.Error("nil []byte wich should be non-nil") } // Insert nil b = nil @@ -745,7 +745,7 @@ func TestNULL(t *testing.T) { dbt.Fatal(err) } if !success { - dbt.Error("Inserting []byte(nil) as NULL failed") + dbt.Error("inserting []byte(nil) as NULL failed") } // Check input==output with input==nil b = nil @@ -753,7 +753,7 @@ func TestNULL(t *testing.T) { dbt.Fatal(err) } if b != nil { - dbt.Error("Non-nil echo from nil input") + dbt.Error("non-nil echo from nil input") } // Check input==output with input!=nil b = []byte("") @@ -820,7 +820,7 @@ func TestUint64(t *testing.T) { sb != shigh, sc != stop, sd != sall: - dbt.Fatal("Unexpected result value") + dbt.Fatal("unexpected result value") } }) } @@ -924,7 +924,7 @@ func TestLoadData(t *testing.T) { } if i != 4 { - dbt.Fatalf("Rows count mismatch. Got %d, want 4", i) + dbt.Fatalf("rows count mismatch. Got %d, want 4", i) } } file, err := ioutil.TempFile("", "gotest") @@ -945,8 +945,8 @@ func TestLoadData(t *testing.T) { // negative test _, err = dbt.db.Exec("LOAD DATA LOCAL INFILE 'doesnotexist' INTO TABLE test") if err == nil { - dbt.Fatal("Load non-existent file didn't fail") - } else if err.Error() != "Local File 'doesnotexist' is not registered. Use the DSN parameter 'allowAllFiles=true' to allow all files" { + dbt.Fatal("load non-existent file didn't fail") + } else if err.Error() != "local file 'doesnotexist' is not registered" { dbt.Fatal(err.Error()) } @@ -966,7 +966,7 @@ func TestLoadData(t *testing.T) { // negative test _, err = dbt.db.Exec("LOAD DATA LOCAL INFILE 'Reader::doesnotexist' INTO TABLE test") if err == nil { - dbt.Fatal("Load non-existent Reader didn't fail") + dbt.Fatal("load non-existent Reader didn't fail") } else if err.Error() != "Reader 'doesnotexist' is not registered" { dbt.Fatal(err.Error()) } @@ -1046,7 +1046,7 @@ func TestStrict(t *testing.T) { var checkWarnings = func(err error, mode string, idx int) { if err == nil { - dbt.Errorf("Expected STRICT error on query [%s] %s", mode, queries[idx].in) + dbt.Errorf("expected STRICT error on query [%s] %s", mode, queries[idx].in) } if warnings, ok := err.(MySQLWarnings); ok { @@ -1055,18 +1055,18 @@ func TestStrict(t *testing.T) { codes[i] = warnings[i].Code } if len(codes) != len(queries[idx].codes) { - dbt.Errorf("Unexpected STRICT error count on query [%s] %s: Wanted %v, Got %v", mode, queries[idx].in, queries[idx].codes, codes) + dbt.Errorf("unexpected STRICT error count on query [%s] %s: Wanted %v, Got %v", mode, queries[idx].in, queries[idx].codes, codes) } for i := range warnings { if codes[i] != queries[idx].codes[i] { - dbt.Errorf("Unexpected STRICT error codes on query [%s] %s: Wanted %v, Got %v", mode, queries[idx].in, queries[idx].codes, codes) + dbt.Errorf("unexpected STRICT error codes on query [%s] %s: Wanted %v, Got %v", mode, queries[idx].in, queries[idx].codes, codes) return } } } else { - dbt.Errorf("Unexpected error on query [%s] %s: %s", mode, queries[idx].in, err.Error()) + dbt.Errorf("unexpected error on query [%s] %s: %s", mode, queries[idx].in, err.Error()) } } @@ -1082,7 +1082,7 @@ func TestStrict(t *testing.T) { for i := range queries { stmt, err = dbt.db.Prepare(queries[i].in) if err != nil { - dbt.Errorf("Error on preparing query %s: %s", queries[i].in, err.Error()) + dbt.Errorf("error on preparing query %s: %s", queries[i].in, err.Error()) } _, err = stmt.Exec() @@ -1090,7 +1090,7 @@ func TestStrict(t *testing.T) { err = stmt.Close() if err != nil { - dbt.Errorf("Error on closing stmt for query %s: %s", queries[i].in, err.Error()) + dbt.Errorf("error on closing stmt for query %s: %s", queries[i].in, err.Error()) } } }) @@ -1100,9 +1100,9 @@ func TestTLS(t *testing.T) { tlsTest := func(dbt *DBTest) { if err := dbt.db.Ping(); err != nil { if err == ErrNoTLS { - dbt.Skip("Server does not support TLS") + dbt.Skip("server does not support TLS") } else { - dbt.Fatalf("Error on Ping: %s", err.Error()) + dbt.Fatalf("error on Ping: %s", err.Error()) } } @@ -1115,7 +1115,7 @@ func TestTLS(t *testing.T) { } if value == nil { - dbt.Fatal("No Cipher") + dbt.Fatal("no Cipher") } } } @@ -1132,42 +1132,42 @@ func TestTLS(t *testing.T) { func TestReuseClosedConnection(t *testing.T) { // this test does not use sql.database, it uses the driver directly if !available { - t.Skipf("MySQL-Server not running on %s", netAddr) + t.Skipf("MySQL server not running on %s", netAddr) } md := &MySQLDriver{} conn, err := md.Open(dsn) if err != nil { - t.Fatalf("Error connecting: %s", err.Error()) + t.Fatalf("error connecting: %s", err.Error()) } stmt, err := conn.Prepare("DO 1") if err != nil { - t.Fatalf("Error preparing statement: %s", err.Error()) + t.Fatalf("error preparing statement: %s", err.Error()) } _, err = stmt.Exec(nil) if err != nil { - t.Fatalf("Error executing statement: %s", err.Error()) + t.Fatalf("error executing statement: %s", err.Error()) } err = conn.Close() if err != nil { - t.Fatalf("Error closing connection: %s", err.Error()) + t.Fatalf("error closing connection: %s", err.Error()) } defer func() { if err := recover(); err != nil { - t.Errorf("Panic after reusing a closed connection: %v", err) + t.Errorf("panic after reusing a closed connection: %v", err) } }() _, err = stmt.Exec(nil) if err != nil && err != driver.ErrBadConn { - t.Errorf("Unexpected error '%s', expected '%s'", + t.Errorf("unexpected error '%s', expected '%s'", err.Error(), driver.ErrBadConn.Error()) } } func TestCharset(t *testing.T) { if !available { - t.Skipf("MySQL-Server not running on %s", netAddr) + t.Skipf("MySQL server not running on %s", netAddr) } mustSetCharset := func(charsetParam, expected string) { @@ -1176,14 +1176,14 @@ func TestCharset(t *testing.T) { defer rows.Close() if !rows.Next() { - dbt.Fatalf("Error getting connection charset: %s", rows.Err()) + dbt.Fatalf("error getting connection charset: %s", rows.Err()) } var got string rows.Scan(&got) if got != expected { - dbt.Fatalf("Expected connection charset %s but got %s", expected, got) + dbt.Fatalf("expected connection charset %s but got %s", expected, got) } }) } @@ -1205,14 +1205,14 @@ func TestFailingCharset(t *testing.T) { _, err := dbt.db.Exec("SELECT 1") if err == nil { dbt.db.Close() - t.Fatalf("Connection must not succeed without a valid charset") + t.Fatalf("connection must not succeed without a valid charset") } }) } func TestCollation(t *testing.T) { if !available { - t.Skipf("MySQL-Server not running on %s", netAddr) + t.Skipf("MySQL server not running on %s", netAddr) } defaultCollation := "utf8_general_ci" @@ -1242,7 +1242,7 @@ func TestCollation(t *testing.T) { } if got != expected { - dbt.Fatalf("Expected connection collation %s but got %s", expected, got) + dbt.Fatalf("expected connection collation %s but got %s", expected, got) } }) } @@ -1307,7 +1307,7 @@ func TestTimezoneConversion(t *testing.T) { // Retrieve time from DB rows := dbt.mustQuery("SELECT ts FROM test") if !rows.Next() { - dbt.Fatal("Didn't get any rows out") + dbt.Fatal("did not get any rows out") } var dbTime time.Time @@ -1318,7 +1318,7 @@ func TestTimezoneConversion(t *testing.T) { // Check that dates match if reftime.Unix() != dbTime.Unix() { - dbt.Errorf("Times don't match.\n") + dbt.Errorf("times do not match.\n") dbt.Errorf(" Now(%v)=%v\n", usCentral, reftime) dbt.Errorf(" Now(UTC)=%v\n", dbTime) } @@ -1344,7 +1344,7 @@ func TestRowsClose(t *testing.T) { } if rows.Next() { - dbt.Fatal("Unexpected row after rows.Close()") + dbt.Fatal("unexpected row after rows.Close()") } err = rows.Err() @@ -1376,7 +1376,7 @@ func TestCloseStmtBeforeRows(t *testing.T) { } if !rows.Next() { - dbt.Fatal("Getting row failed") + dbt.Fatal("getting row failed") } else { err = rows.Err() if err != nil { @@ -1386,7 +1386,7 @@ func TestCloseStmtBeforeRows(t *testing.T) { var out bool err = rows.Scan(&out) if err != nil { - dbt.Fatalf("Error on rows.Scan(): %s", err.Error()) + dbt.Fatalf("error on rows.Scan(): %s", err.Error()) } if out != true { dbt.Errorf("true != %t", out) @@ -1422,7 +1422,7 @@ func TestStmtMultiRows(t *testing.T) { // 1 if !rows1.Next() { - dbt.Fatal("1st rows1.Next failed") + dbt.Fatal("first rows1.Next failed") } else { err = rows1.Err() if err != nil { @@ -1431,7 +1431,7 @@ func TestStmtMultiRows(t *testing.T) { err = rows1.Scan(&out) if err != nil { - dbt.Fatalf("Error on rows.Scan(): %s", err.Error()) + dbt.Fatalf("error on rows.Scan(): %s", err.Error()) } if out != true { dbt.Errorf("true != %t", out) @@ -1439,7 +1439,7 @@ func TestStmtMultiRows(t *testing.T) { } if !rows2.Next() { - dbt.Fatal("1st rows2.Next failed") + dbt.Fatal("first rows2.Next failed") } else { err = rows2.Err() if err != nil { @@ -1448,7 +1448,7 @@ func TestStmtMultiRows(t *testing.T) { err = rows2.Scan(&out) if err != nil { - dbt.Fatalf("Error on rows.Scan(): %s", err.Error()) + dbt.Fatalf("error on rows.Scan(): %s", err.Error()) } if out != true { dbt.Errorf("true != %t", out) @@ -1457,7 +1457,7 @@ func TestStmtMultiRows(t *testing.T) { // 2 if !rows1.Next() { - dbt.Fatal("2nd rows1.Next failed") + dbt.Fatal("second rows1.Next failed") } else { err = rows1.Err() if err != nil { @@ -1466,14 +1466,14 @@ func TestStmtMultiRows(t *testing.T) { err = rows1.Scan(&out) if err != nil { - dbt.Fatalf("Error on rows.Scan(): %s", err.Error()) + dbt.Fatalf("error on rows.Scan(): %s", err.Error()) } if out != false { dbt.Errorf("false != %t", out) } if rows1.Next() { - dbt.Fatal("Unexpected row on rows1") + dbt.Fatal("unexpected row on rows1") } err = rows1.Close() if err != nil { @@ -1482,7 +1482,7 @@ func TestStmtMultiRows(t *testing.T) { } if !rows2.Next() { - dbt.Fatal("2nd rows2.Next failed") + dbt.Fatal("second rows2.Next failed") } else { err = rows2.Err() if err != nil { @@ -1491,14 +1491,14 @@ func TestStmtMultiRows(t *testing.T) { err = rows2.Scan(&out) if err != nil { - dbt.Fatalf("Error on rows.Scan(): %s", err.Error()) + dbt.Fatalf("error on rows.Scan(): %s", err.Error()) } if out != false { dbt.Errorf("false != %t", out) } if rows2.Next() { - dbt.Fatal("Unexpected row on rows2") + dbt.Fatal("unexpected row on rows2") } err = rows2.Close() if err != nil { @@ -1543,7 +1543,7 @@ func TestConcurrent(t *testing.T) { if err != nil { dbt.Fatalf("%s", err.Error()) } - dbt.Logf("Testing up to %d concurrent connections \r\n", max) + dbt.Logf("testing up to %d concurrent connections \r\n", max) var remaining, succeeded int32 = int32(max), 0 @@ -1567,7 +1567,7 @@ func TestConcurrent(t *testing.T) { if err != nil { if err.Error() != "Error 1040: Too many connections" { - fatalf("Error on Conn %d: %s", id, err.Error()) + fatalf("error on conn %d: %s", id, err.Error()) } return } @@ -1575,13 +1575,13 @@ func TestConcurrent(t *testing.T) { // keep the connection busy until all connections are open for remaining > 0 { if _, err = tx.Exec("DO 1"); err != nil { - fatalf("Error on Conn %d: %s", id, err.Error()) + fatalf("error on conn %d: %s", id, err.Error()) return } } if err = tx.Commit(); err != nil { - fatalf("Error on Conn %d: %s", id, err.Error()) + fatalf("error on conn %d: %s", id, err.Error()) return } @@ -1597,14 +1597,14 @@ func TestConcurrent(t *testing.T) { dbt.Fatal(fatalError) } - dbt.Logf("Reached %d concurrent connections\r\n", succeeded) + dbt.Logf("reached %d concurrent connections\r\n", succeeded) }) } // Tests custom dial functions func TestCustomDial(t *testing.T) { if !available { - t.Skipf("MySQL-Server not running on %s", netAddr) + t.Skipf("MySQL server not running on %s", netAddr) } // our custom dial function which justs wraps net.Dial here @@ -1614,16 +1614,16 @@ func TestCustomDial(t *testing.T) { db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@mydial(%s)/%s?timeout=30s&strict=true", user, pass, addr, dbname)) if err != nil { - t.Fatalf("Error connecting: %s", err.Error()) + t.Fatalf("error connecting: %s", err.Error()) } defer db.Close() if _, err = db.Exec("DO 1"); err != nil { - t.Fatalf("Connection failed: %s", err.Error()) + t.Fatalf("connection failed: %s", err.Error()) } } -func TestSqlInjection(t *testing.T) { +func TestSQLInjection(t *testing.T) { createTest := func(arg string) func(dbt *DBTest) { return func(dbt *DBTest) { dbt.mustExec("CREATE TABLE test (v INTEGER)") @@ -1636,9 +1636,9 @@ func TestSqlInjection(t *testing.T) { if err == sql.ErrNoRows { return // success, sql injection failed } else if err == nil { - dbt.Errorf("Sql injection successful with arg: %s", arg) + dbt.Errorf("sql injection successful with arg: %s", arg) } else { - dbt.Errorf("Error running query with arg: %s; err: %s", arg, err.Error()) + dbt.Errorf("error running query with arg: %s; err: %s", arg, err.Error()) } } } @@ -1705,14 +1705,14 @@ func TestUnixSocketAuthFail(t *testing.T) { // Get socket file from MySQL. err := dbt.db.QueryRow("SELECT @@socket").Scan(&socket) if err != nil { - t.Fatalf("Error on SELECT @@socket: %s", err.Error()) + t.Fatalf("error on SELECT @@socket: %s", err.Error()) } } t.Logf("socket: %s", socket) badDSN := fmt.Sprintf("%s:%s@unix(%s)/%s?timeout=30s&strict=true", user, badPass, socket, dbname) db, err := sql.Open("mysql", badDSN) if err != nil { - t.Fatalf("Error connecting: %s", err.Error()) + t.Fatalf("error connecting: %s", err.Error()) } defer db.Close() From 280e61d233e8e32d63235afed8a27d2df3c5afe7 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Wed, 20 Jan 2016 15:47:09 +0100 Subject: [PATCH 20/65] packets: grammar in err message --- packets.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packets.go b/packets.go index 96f4478da..6ac1cccea 100644 --- a/packets.go +++ b/packets.go @@ -563,7 +563,7 @@ func (mc *mysqlConn) readColumns(count int) ([]mysqlField, error) { if i == count { return columns, nil } - return nil, fmt.Errorf("columns count mismatch n:%d len:%d", count, len(columns)) + return nil, fmt.Errorf("column count mismatch n:%d len:%d", count, len(columns)) } // Catalog @@ -809,7 +809,7 @@ func (stmt *mysqlStmt) writeCommandLongData(paramID int, arg []byte) error { func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error { if len(args) != stmt.paramCount { return fmt.Errorf( - "arguments count mismatch (got: %d; has: %d)", + "argument count mismatch (got: %d; has: %d)", len(args), stmt.paramCount, ) From 8cbeffa8f656fc645d53357c4705af7c4e2af53b Mon Sep 17 00:00:00 2001 From: Idhor Date: Sun, 12 Apr 2015 16:38:45 +0200 Subject: [PATCH 21/65] Enable Multi Results support and discard additional results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - packets.go: flag clientMultiResults, update status when receiving an EOF packet, discard additional results on readRow when EOF is reached - statement.go: currently a nil rows.mc is used as an eof, don’t set it if there are no columns to avoid that Next() waits indefinitely - rows.go: discard additional results on close and avoid panic on Columns() --- packets.go | 45 +++++++++++++++++++++++++++++++++++++++++++-- rows.go | 8 +++++++- statement.go | 2 +- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packets.go b/packets.go index 6ac1cccea..c6589109c 100644 --- a/packets.go +++ b/packets.go @@ -224,6 +224,7 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte) error { clientTransactions | clientLocalFiles | clientPluginAuth | + clientMultiResults | mc.flags&clientLongFlag if mc.cfg.ClientFoundRows { @@ -519,6 +520,10 @@ func (mc *mysqlConn) handleErrorPacket(data []byte) error { } } +func readStatus(b []byte) statusFlag { + return statusFlag(b[0]) | statusFlag(b[1])<<8 +} + // Ok Packet // http://dev.mysql.com/doc/internals/en/generic-response-packets.html#packet-OK_Packet func (mc *mysqlConn) handleOkPacket(data []byte) error { @@ -533,7 +538,7 @@ func (mc *mysqlConn) handleOkPacket(data []byte) error { mc.insertId, _, m = readLengthEncodedInteger(data[1+n:]) // server_status [2 bytes] - mc.status = statusFlag(data[1+n+m]) | statusFlag(data[1+n+m+1])<<8 + mc.status = readStatus(data[1+n+m : 1+n+m+2]) // warning count [2 bytes] if !mc.strict { @@ -652,6 +657,11 @@ func (rows *textRows) readRow(dest []driver.Value) error { // EOF Packet if data[0] == iEOF && len(data) == 5 { + // server_status [2 bytes] + rows.mc.status = readStatus(data[3:]) + if err := rows.mc.discardMoreResultsIfExists(); err != nil { + return err + } rows.mc = nil return io.EOF } @@ -709,6 +719,10 @@ func (mc *mysqlConn) readUntilEOF() error { if err == nil && data[0] != iEOF { continue } + if err == nil && data[0] == iEOF && len(data) == 5 { + mc.status = readStatus(data[3:]) + } + return err // Err or EOF } } @@ -1013,6 +1027,28 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error { return mc.writePacket(data) } +func (mc *mysqlConn) discardMoreResultsIfExists() error { + for mc.status&statusMoreResultsExists != 0 { + resLen, err := mc.readResultSetHeaderPacket() + if err != nil { + return err + } + if resLen > 0 { + // columns + if err := mc.readUntilEOF(); err != nil { + return err + } + // rows + if err := mc.readUntilEOF(); err != nil { + return err + } + } else { + mc.status &^= statusMoreResultsExists + } + } + return nil +} + // http://dev.mysql.com/doc/internals/en/binary-protocol-resultset-row.html func (rows *binaryRows) readRow(dest []driver.Value) error { data, err := rows.mc.readPacket() @@ -1022,11 +1058,16 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { // packet indicator [1 byte] if data[0] != iOK { - rows.mc = nil // EOF Packet if data[0] == iEOF && len(data) == 5 { + rows.mc.status = readStatus(data[3:]) + if err := rows.mc.discardMoreResultsIfExists(); err != nil { + return err + } + rows.mc = nil return io.EOF } + rows.mc = nil // Error otherwise return rows.mc.handleErrorPacket(data) diff --git a/rows.go b/rows.go index 5d21948ad..9853f8323 100644 --- a/rows.go +++ b/rows.go @@ -38,7 +38,7 @@ type emptyRows struct{} func (rows *mysqlRows) Columns() []string { columns := make([]string, len(rows.columns)) - if rows.mc.cfg.ColumnsWithAlias { + if rows.mc != nil && rows.mc.cfg.ColumnsWithAlias { for i := range columns { if tableName := rows.columns[i].tableName; len(tableName) > 0 { columns[i] = tableName + "." + rows.columns[i].name @@ -65,6 +65,12 @@ func (rows *mysqlRows) Close() error { // Remove unread packets from stream err := mc.readUntilEOF() + if err == nil { + if err = mc.discardMoreResultsIfExists(); err != nil { + return err + } + } + rows.mc = nil return err } diff --git a/statement.go b/statement.go index 6e869b340..ead9a6bf4 100644 --- a/statement.go +++ b/statement.go @@ -101,9 +101,9 @@ func (stmt *mysqlStmt) Query(args []driver.Value) (driver.Rows, error) { } rows := new(binaryRows) - rows.mc = mc if resLen > 0 { + rows.mc = mc // Columns // If not cached, read them and cache them if stmt.columns == nil { From 5ce0b98124bb085dde89a228f8ba7e72c6cffdfa Mon Sep 17 00:00:00 2001 From: Idhor Date: Wed, 20 Jan 2016 16:51:34 +0100 Subject: [PATCH 22/65] Add Idhor (Luca Looz) to AUTHORS for multi-results --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index e86676ff4..3c61a1a87 100644 --- a/AUTHORS +++ b/AUTHORS @@ -31,6 +31,7 @@ Julien Schmidt Kamil Dziedzic Kevin Malachowski Leonardo YongUk Kim +Luca Looz Lucas Liu Luke Scott Michael Woolnough From 1bdf5bdf85466821c9868c489be4bfc43c803b62 Mon Sep 17 00:00:00 2001 From: Badoet Endoet Date: Mon, 11 May 2015 17:20:05 +0800 Subject: [PATCH 23/65] added the multiStatements param to the dsn parameter --- dsn.go | 9 +++++++++ packets.go | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/dsn.go b/dsn.go index 31fd530ee..2203e2291 100644 --- a/dsn.go +++ b/dsn.go @@ -46,6 +46,7 @@ type Config struct { ClientFoundRows bool // Return number of matching rows instead of rows changed ColumnsWithAlias bool // Prepend table alias to column names InterpolateParams bool // Interpolate placeholders into query string + MultiStatements bool // Allow multiple statements in one query ParseTime bool // Parse time values to time.Time Strict bool // Return warnings as errors } @@ -235,6 +236,14 @@ func parseDSNParams(cfg *Config, params string) (err error) { return } + // multiple statements in one query + case "multiStatements": + var isBool bool + cfg.MultiStatements, isBool = readBool(value) + if !isBool { + return errors.New("invalid bool value: " + value) + } + // time.Time parsing case "parseTime": var isBool bool diff --git a/packets.go b/packets.go index c6589109c..1b13013cf 100644 --- a/packets.go +++ b/packets.go @@ -236,6 +236,10 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte) error { clientFlags |= clientSSL } + if mc.cfg.MultiStatements { + clientFlags |= clientMultiStatements + } + // User Password scrambleBuff := scramblePassword(cipher, []byte(mc.cfg.Passwd)) From 71c5db63045271a268cbf59b3a548af98e69f5a5 Mon Sep 17 00:00:00 2001 From: Badoet Endoet Date: Mon, 11 May 2015 19:48:44 +0800 Subject: [PATCH 24/65] add test for the new dsn param --- dsn_test.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/dsn_test.go b/dsn_test.go index 3e2a4b37a..d825069fc 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -19,19 +19,20 @@ var testDSNs = []struct { in string out string }{ - {"username:password@protocol(address)/dbname?param=value", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false ParseTime:false Strict:false}"}, - {"user@unix(/path/to/socket)/dbname?charset=utf8", "&{User:user Passwd: Net:unix Addr:/path/to/socket DBName:dbname Params:map[charset:utf8] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8mb4,utf8] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", "&{User:user Passwd:password Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Loc:UTC TLS: Timeout:30s ReadTimeout:1s WriteTimeout:1s Collation:224 AllowAllFiles:true AllowCleartextPasswords:false AllowOldPasswords:true ClientFoundRows:true ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{User:user Passwd:p@ss(word) Net:tcp Addr:[de:ad:be:ef::ca:fe]:80 DBName:dbname Params:map[] Loc:Local TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"/dbname", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"@/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"user:p@/ssword@/", "&{User:user Passwd:p@/ssword Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, - {"unix/?arg=%2Fsome%2Fpath.ext", "&{User: Passwd: Net:unix Addr:/tmp/mysql.sock DBName: Params:map[arg:/some/path.ext] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false ParseTime:false Strict:false}"}, + {"username:password@protocol(address)/dbname?param=value", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true&multiStatements=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:true ParseTime:false Strict:false}"}, + {"user@unix(/path/to/socket)/dbname?charset=utf8", "&{User:user Passwd: Net:unix Addr:/path/to/socket DBName:dbname Params:map[charset:utf8] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8mb4,utf8] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", "&{User:user Passwd:password Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Loc:UTC TLS: Timeout:30s ReadTimeout:1s WriteTimeout:1s Collation:224 AllowAllFiles:true AllowCleartextPasswords:false AllowOldPasswords:true ClientFoundRows:true ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{User:user Passwd:p@ss(word) Net:tcp Addr:[de:ad:be:ef::ca:fe]:80 DBName:dbname Params:map[] Loc:Local TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"/dbname", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"@/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"user:p@/ssword@/", "&{User:user Passwd:p@/ssword Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"unix/?arg=%2Fsome%2Fpath.ext", "&{User: Passwd: Net:unix Addr:/tmp/mysql.sock DBName: Params:map[arg:/some/path.ext] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, } func TestDSNParser(t *testing.T) { From 4aa920ddb859e542ef720fcd7291fbadc8404a27 Mon Sep 17 00:00:00 2001 From: Badoet Endoet Date: Tue, 12 May 2015 12:05:02 +0800 Subject: [PATCH 25/65] TestMultiQuery discard additional OK response after Multi Statement Exec Calls --- AUTHORS | 1 + driver_test.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++ packets.go | 1 + 3 files changed, 83 insertions(+) diff --git a/AUTHORS b/AUTHORS index 3c61a1a87..986f017cb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -39,6 +39,7 @@ Nicola Peduzzi Runrioter Wung Soroush Pour Stan Putrya +Stanley Gunawan Xiaobing Jiang Xiuming Chen diff --git a/driver_test.go b/driver_test.go index 319d34f91..a902fe323 100644 --- a/driver_test.go +++ b/driver_test.go @@ -76,6 +76,28 @@ type DBTest struct { db *sql.DB } +func runTestsWithMultiStatement(t *testing.T, dsn string, tests ...func(dbt *DBTest)) { + if !available { + t.Skipf("MySQL-Server not running on %s", netAddr) + } + + dsn3 := dsn + "&multiStatements=true" + var db3 *sql.DB + if _, err := parseDSN(dsn3); err != errInvalidDSNUnsafeCollation { + db3, err = sql.Open("mysql", dsn3) + if err != nil { + t.Fatalf("Error connecting: %s", err.Error()) + } + defer db3.Close() + } + + dbt3 := &DBTest{t, db3} + for _, test := range tests { + test(dbt3) + dbt3.db.Exec("DROP TABLE IF EXISTS test") + } +} + func runTests(t *testing.T, dsn string, tests ...func(dbt *DBTest)) { if !available { t.Skipf("MySQL server not running on %s", netAddr) @@ -99,8 +121,19 @@ func runTests(t *testing.T, dsn string, tests ...func(dbt *DBTest)) { defer db2.Close() } + dsn3 := dsn + "&multiStatements=true" + var db3 *sql.DB + if _, err := parseDSN(dsn3); err != errInvalidDSNUnsafeCollation { + db3, err = sql.Open("mysql", dsn3) + if err != nil { + t.Fatalf("Error connecting: %s", err.Error()) + } + defer db3.Close() + } + dbt := &DBTest{t, db} dbt2 := &DBTest{t, db2} + dbt3 := &DBTest{t, db3} for _, test := range tests { test(dbt) dbt.db.Exec("DROP TABLE IF EXISTS test") @@ -108,6 +141,10 @@ func runTests(t *testing.T, dsn string, tests ...func(dbt *DBTest)) { test(dbt2) dbt2.db.Exec("DROP TABLE IF EXISTS test") } + if db3 != nil { + test(dbt3) + dbt3.db.Exec("DROP TABLE IF EXISTS test") + } } } @@ -237,6 +274,50 @@ func TestCRUD(t *testing.T) { }) } +func TestMultiQuery(t *testing.T) { + runTestsWithMultiStatement(t, dsn, func(dbt *DBTest) { + // Create Table + dbt.mustExec("CREATE TABLE `test` (`id` int(11) NOT NULL, `value` int(11) NOT NULL) ") + + // Create Data + res := dbt.mustExec("INSERT INTO test VALUES (1, 1)") + count, err := res.RowsAffected() + if err != nil { + dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) + } + if count != 1 { + dbt.Fatalf("Expected 1 affected row, got %d", count) + } + + // Update + res = dbt.mustExec("UPDATE test SET value = 3 WHERE id = 1; UPDATE test SET value = 4 WHERE id = 1; UPDATE test SET value = 5 WHERE id = 1;") + count, err = res.RowsAffected() + if err != nil { + dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) + } + if count != 1 { + dbt.Fatalf("Expected 1 affected row, got %d", count) + } + + // Read + var out int + rows := dbt.mustQuery("SELECT value FROM test WHERE id=1;") + if rows.Next() { + rows.Scan(&out) + if 5 != out { + dbt.Errorf("5 != %t", out) + } + + if rows.Next() { + dbt.Error("unexpected data") + } + } else { + dbt.Error("no data") + } + + }) +} + func TestInt(t *testing.T) { runTests(t, dsn, func(dbt *DBTest) { types := [5]string{"TINYINT", "SMALLINT", "MEDIUMINT", "INT", "BIGINT"} diff --git a/packets.go b/packets.go index 1b13013cf..91e9a85b1 100644 --- a/packets.go +++ b/packets.go @@ -543,6 +543,7 @@ func (mc *mysqlConn) handleOkPacket(data []byte) error { // server_status [2 bytes] mc.status = readStatus(data[1+n+m : 1+n+m+2]) + mc.discardMoreResultsIfExists() // warning count [2 bytes] if !mc.strict { From acb3ebdd872afb15b2c5824f23ae1781a284a427 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Wed, 20 Jan 2016 22:52:29 +0100 Subject: [PATCH 26/65] Fix driver tests --- driver_test.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/driver_test.go b/driver_test.go index a902fe323..cd2a9168e 100644 --- a/driver_test.go +++ b/driver_test.go @@ -78,23 +78,23 @@ type DBTest struct { func runTestsWithMultiStatement(t *testing.T, dsn string, tests ...func(dbt *DBTest)) { if !available { - t.Skipf("MySQL-Server not running on %s", netAddr) + t.Skipf("MySQL server not running on %s", netAddr) } - dsn3 := dsn + "&multiStatements=true" - var db3 *sql.DB - if _, err := parseDSN(dsn3); err != errInvalidDSNUnsafeCollation { - db3, err = sql.Open("mysql", dsn3) + dsn += "&multiStatements=true" + var db *sql.DB + if _, err := ParseDSN(dsn); err != errInvalidDSNUnsafeCollation { + db, err = sql.Open("mysql", dsn) if err != nil { - t.Fatalf("Error connecting: %s", err.Error()) + t.Fatalf("error connecting: %s", err.Error()) } - defer db3.Close() + defer db.Close() } - dbt3 := &DBTest{t, db3} + dbt := &DBTest{t, db} for _, test := range tests { - test(dbt3) - dbt3.db.Exec("DROP TABLE IF EXISTS test") + test(dbt) + dbt.db.Exec("DROP TABLE IF EXISTS test") } } @@ -123,10 +123,10 @@ func runTests(t *testing.T, dsn string, tests ...func(dbt *DBTest)) { dsn3 := dsn + "&multiStatements=true" var db3 *sql.DB - if _, err := parseDSN(dsn3); err != errInvalidDSNUnsafeCollation { + if _, err := ParseDSN(dsn3); err != errInvalidDSNUnsafeCollation { db3, err = sql.Open("mysql", dsn3) if err != nil { - t.Fatalf("Error connecting: %s", err.Error()) + t.Fatalf("error connecting: %s", err.Error()) } defer db3.Close() } @@ -286,7 +286,7 @@ func TestMultiQuery(t *testing.T) { dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) } if count != 1 { - dbt.Fatalf("Expected 1 affected row, got %d", count) + dbt.Fatalf("expected 1 affected row, got %d", count) } // Update @@ -296,7 +296,7 @@ func TestMultiQuery(t *testing.T) { dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) } if count != 1 { - dbt.Fatalf("Expected 1 affected row, got %d", count) + dbt.Fatalf("expected 1 affected row, got %d", count) } // Read From c1e44c429be3daae425c329724f2f4739c4742dc Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Wed, 20 Jan 2016 22:58:45 +0100 Subject: [PATCH 27/65] check discardMoreResultsIfExists err --- packets.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packets.go b/packets.go index 91e9a85b1..f58d12533 100644 --- a/packets.go +++ b/packets.go @@ -543,7 +543,9 @@ func (mc *mysqlConn) handleOkPacket(data []byte) error { // server_status [2 bytes] mc.status = readStatus(data[1+n+m : 1+n+m+2]) - mc.discardMoreResultsIfExists() + if err := mc.discardMoreResultsIfExists(); err != nil { + return err + } // warning count [2 bytes] if !mc.strict { From 023343e2a99eb9bbe70a867202bacf462ae68844 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Thu, 21 Jan 2016 00:23:17 +0100 Subject: [PATCH 28/65] README: add multiStatements --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index f3190e8ab..da3ace49f 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,16 @@ Note that this sets the location for time.Time values but does not change MySQL' Please keep in mind, that param values must be [url.QueryEscape](http://golang.org/pkg/net/url/#QueryEscape)'ed. Alternatively you can manually replace the `/` with `%2F`. For example `US/Pacific` would be `loc=US%2FPacific`. +##### `multiStatements` + +``` +Type: bool +Valid Values: true, false +Default: false +``` + +Allow multiple statements in one query. While this allows batch queries, it also greatly increases the risk of SQL injections. + ##### `parseTime` From 416bd115d629893dabcb41b95f42a5c37b9f216a Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Sat, 23 Jan 2016 00:58:37 +0100 Subject: [PATCH 29/65] README: Add note on discarded results --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da3ace49f..78a6bddf4 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,7 @@ Valid Values: true, false Default: false ``` -Allow multiple statements in one query. While this allows batch queries, it also greatly increases the risk of SQL injections. +Allow multiple statements in one query. While this allows batch queries, it also greatly increases the risk of SQL injections. Only the result of the first query is returned, all other results are silently discarded. ##### `parseTime` From 8d8957e76761441305a1c0614893be5459f301db Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Sat, 23 Jan 2016 01:47:13 +0100 Subject: [PATCH 30/65] Add field type JSON support --- const.go | 3 ++- packets.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/const.go b/const.go index dddc12908..88cfff3fd 100644 --- a/const.go +++ b/const.go @@ -107,7 +107,8 @@ const ( fieldTypeBit ) const ( - fieldTypeNewDecimal byte = iota + 0xf6 + fieldTypeJSON byte = iota + 0xf5 + fieldTypeNewDecimal fieldTypeEnum fieldTypeSet fieldTypeTinyBLOB diff --git a/packets.go b/packets.go index 6ac1cccea..f5be21720 100644 --- a/packets.go +++ b/packets.go @@ -1106,7 +1106,7 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { case fieldTypeDecimal, fieldTypeNewDecimal, fieldTypeVarChar, fieldTypeBit, fieldTypeEnum, fieldTypeSet, fieldTypeTinyBLOB, fieldTypeMediumBLOB, fieldTypeLongBLOB, fieldTypeBLOB, - fieldTypeVarString, fieldTypeString, fieldTypeGeometry: + fieldTypeVarString, fieldTypeString, fieldTypeGeometry, fieldTypeJSON: var isNull bool var n int dest[i], isNull, n, err = readLengthEncodedString(data[pos:]) From bba2f88dafaa2266c1ad8477e823dcb6253bd242 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Sat, 30 Jan 2016 00:10:55 +0100 Subject: [PATCH 31/65] Rename discardMoreResultsIfExists --- packets.go | 8 ++++---- rows.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packets.go b/packets.go index f58d12533..fab128e46 100644 --- a/packets.go +++ b/packets.go @@ -543,7 +543,7 @@ func (mc *mysqlConn) handleOkPacket(data []byte) error { // server_status [2 bytes] mc.status = readStatus(data[1+n+m : 1+n+m+2]) - if err := mc.discardMoreResultsIfExists(); err != nil { + if err := mc.discardResults(); err != nil { return err } @@ -666,7 +666,7 @@ func (rows *textRows) readRow(dest []driver.Value) error { if data[0] == iEOF && len(data) == 5 { // server_status [2 bytes] rows.mc.status = readStatus(data[3:]) - if err := rows.mc.discardMoreResultsIfExists(); err != nil { + if err := rows.mc.discardResults(); err != nil { return err } rows.mc = nil @@ -1034,7 +1034,7 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error { return mc.writePacket(data) } -func (mc *mysqlConn) discardMoreResultsIfExists() error { +func (mc *mysqlConn) discardResults() error { for mc.status&statusMoreResultsExists != 0 { resLen, err := mc.readResultSetHeaderPacket() if err != nil { @@ -1068,7 +1068,7 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { // EOF Packet if data[0] == iEOF && len(data) == 5 { rows.mc.status = readStatus(data[3:]) - if err := rows.mc.discardMoreResultsIfExists(); err != nil { + if err := rows.mc.discardResults(); err != nil { return err } rows.mc = nil diff --git a/rows.go b/rows.go index 9853f8323..c08255eee 100644 --- a/rows.go +++ b/rows.go @@ -66,7 +66,7 @@ func (rows *mysqlRows) Close() error { // Remove unread packets from stream err := mc.readUntilEOF() if err == nil { - if err = mc.discardMoreResultsIfExists(); err != nil { + if err = mc.discardResults(); err != nil { return err } } From 8b688fbe796c3b9ae9a10838ba391a236ed3f982 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Sun, 31 Jan 2016 02:51:49 +0100 Subject: [PATCH 32/65] DSN: Add cfg.Format method --- collations.go | 22 ++--- dsn.go | 228 +++++++++++++++++++++++++++++++++++++++++++++----- dsn_test.go | 64 +++++++++----- packets.go | 18 ++-- 4 files changed, 278 insertions(+), 54 deletions(-) diff --git a/collations.go b/collations.go index 6c1d613d5..82079cfb9 100644 --- a/collations.go +++ b/collations.go @@ -8,7 +8,7 @@ package mysql -const defaultCollation byte = 33 // utf8_general_ci +const defaultCollation = "utf8_general_ci" // A list of available collations mapped to the internal ID. // To update this map use the following MySQL query: @@ -237,14 +237,14 @@ var collations = map[string]byte{ // A blacklist of collations which is unsafe to interpolate parameters. // These multibyte encodings may contains 0x5c (`\`) in their trailing bytes. -var unsafeCollations = map[byte]bool{ - 1: true, // big5_chinese_ci - 13: true, // sjis_japanese_ci - 28: true, // gbk_chinese_ci - 84: true, // big5_bin - 86: true, // gb2312_bin - 87: true, // gbk_bin - 88: true, // sjis_bin - 95: true, // cp932_japanese_ci - 96: true, // cp932_bin +var unsafeCollations = map[string]bool{ + "big5_chinese_ci": true, + "sjis_japanese_ci": true, + "gbk_chinese_ci": true, + "big5_bin": true, + "gb2312_bin": true, + "gbk_bin": true, + "sjis_bin": true, + "cp932_japanese_ci": true, + "cp932_bin": true, } diff --git a/dsn.go b/dsn.go index 2203e2291..29cce2d86 100644 --- a/dsn.go +++ b/dsn.go @@ -9,6 +9,7 @@ package mysql import ( + "bytes" "crypto/tls" "errors" "fmt" @@ -33,12 +34,13 @@ type Config struct { Addr string // Network address DBName string // Database name Params map[string]string // Connection parameters + Collation string // Connection collation Loc *time.Location // Location for time.Time values - TLS *tls.Config // TLS configuration + TLSConfig string // TLS configuration name + tls *tls.Config // TLS configuration Timeout time.Duration // Dial timeout ReadTimeout time.Duration // I/O read timeout WriteTimeout time.Duration // I/O write timeout - Collation uint8 // Connection collation AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE AllowCleartextPasswords bool // Allows the cleartext client side plugin @@ -51,6 +53,194 @@ type Config struct { Strict bool // Return warnings as errors } +// FormatDSN formats the given Config into a DSN string which can be passed to +// the driver. +func (cfg *Config) FormatDSN() string { + var buf bytes.Buffer + + // [username[:password]@] + if len(cfg.User) > 0 { + buf.WriteString(cfg.User) + if len(cfg.Passwd) > 0 { + buf.WriteByte(':') + buf.WriteString(cfg.Passwd) + } + buf.WriteByte('@') + } + + // [protocol[(address)]] + if len(cfg.Net) > 0 { + buf.WriteString(cfg.Net) + if len(cfg.Addr) > 0 { + buf.WriteByte('(') + buf.WriteString(cfg.Addr) + buf.WriteByte(')') + } + } + + // /dbname + buf.WriteByte('/') + buf.WriteString(cfg.DBName) + + // [?param1=value1&...¶mN=valueN] + hasParam := false + + if cfg.AllowAllFiles { + hasParam = true + buf.WriteString("?allowAllFiles=true") + } + + if cfg.AllowCleartextPasswords { + if hasParam { + buf.WriteString("&allowCleartextPasswords=true") + } else { + hasParam = true + buf.WriteString("?allowCleartextPasswords=true") + } + } + + if cfg.AllowOldPasswords { + if hasParam { + buf.WriteString("&allowOldPasswords=true") + } else { + hasParam = true + buf.WriteString("?allowOldPasswords=true") + } + } + + if cfg.ClientFoundRows { + if hasParam { + buf.WriteString("&clientFoundRows=true") + } else { + hasParam = true + buf.WriteString("?clientFoundRows=true") + } + } + + if col := cfg.Collation; col != defaultCollation && len(col) > 0 { + if hasParam { + buf.WriteString("&collation=") + } else { + hasParam = true + buf.WriteString("?collation=") + } + buf.WriteString(col) + } + + if cfg.ColumnsWithAlias { + if hasParam { + buf.WriteString("&columnsWithAlias=true") + } else { + hasParam = true + buf.WriteString("?columnsWithAlias=true") + } + } + + if cfg.InterpolateParams { + if hasParam { + buf.WriteString("&interpolateParams=true") + } else { + hasParam = true + buf.WriteString("?interpolateParams=true") + } + } + + if cfg.Loc != time.UTC && cfg.Loc != nil { + if hasParam { + buf.WriteString("&loc=") + } else { + hasParam = true + buf.WriteString("?loc=") + } + buf.WriteString(url.QueryEscape(cfg.Loc.String())) + } + + if cfg.MultiStatements { + if hasParam { + buf.WriteString("&multiStatements=true") + } else { + hasParam = true + buf.WriteString("?multiStatements=true") + } + } + + if cfg.ParseTime { + if hasParam { + buf.WriteString("&parseTime=true") + } else { + hasParam = true + buf.WriteString("?parseTime=true") + } + } + + if cfg.ReadTimeout > 0 { + if hasParam { + buf.WriteString("&readTimeout=") + } else { + hasParam = true + buf.WriteString("?readTimeout=") + } + buf.WriteString(cfg.ReadTimeout.String()) + } + + if cfg.Strict { + if hasParam { + buf.WriteString("&strict=true") + } else { + hasParam = true + buf.WriteString("?strict=true") + } + } + + if cfg.Timeout > 0 { + if hasParam { + buf.WriteString("&timeout=") + } else { + hasParam = true + buf.WriteString("?timeout=") + } + buf.WriteString(cfg.Timeout.String()) + } + + if len(cfg.TLSConfig) > 0 { + if hasParam { + buf.WriteString("&tls=") + } else { + hasParam = true + buf.WriteString("?tls=") + } + buf.WriteString(url.QueryEscape(cfg.TLSConfig)) + } + + if cfg.WriteTimeout > 0 { + if hasParam { + buf.WriteString("&writeTimeout=") + } else { + hasParam = true + buf.WriteString("?writeTimeout=") + } + buf.WriteString(cfg.WriteTimeout.String()) + } + + // other params + if cfg.Params != nil { + for param, value := range cfg.Params { + if hasParam { + buf.WriteByte('&') + } else { + hasParam = true + buf.WriteByte('?') + } + + buf.WriteString(param) + buf.WriteByte('=') + buf.WriteString(url.QueryEscape(value)) + } + } + + return buf.String() +} + // ParseDSN parses the DSN string to a Config func ParseDSN(dsn string) (cfg *Config, err error) { // New config with some default values @@ -196,15 +386,7 @@ func parseDSNParams(cfg *Config, params string) (err error) { // Collation case "collation": - collation, ok := collations[value] - if !ok { - // Note possibility for false negatives: - // could be triggered although the collation is valid if the - // collations map does not contain entries the server supports. - err = errors.New("unknown collation") - return - } - cfg.Collation = collation + cfg.Collation = value break case "columnsWithAlias": @@ -279,14 +461,21 @@ func parseDSNParams(cfg *Config, params string) (err error) { boolValue, isBool := readBool(value) if isBool { if boolValue { - cfg.TLS = &tls.Config{} + cfg.TLSConfig = "true" + cfg.tls = &tls.Config{} + } else { + cfg.TLSConfig = "false" } - } else if value, err := url.QueryUnescape(value); err != nil { - return fmt.Errorf("invalid value for TLS config name: %v", err) + } else if vl := strings.ToLower(value); vl == "skip-verify" { + cfg.TLSConfig = vl + cfg.tls = &tls.Config{InsecureSkipVerify: true} } else { - if strings.ToLower(value) == "skip-verify" { - cfg.TLS = &tls.Config{InsecureSkipVerify: true} - } else if tlsConfig, ok := tlsConfigRegister[value]; ok { + name, err := url.QueryUnescape(value) + if err != nil { + return fmt.Errorf("invalid value for TLS config name: %v", err) + } + + if tlsConfig, ok := tlsConfigRegister[name]; ok { if len(tlsConfig.ServerName) == 0 && !tlsConfig.InsecureSkipVerify { host, _, err := net.SplitHostPort(cfg.Addr) if err == nil { @@ -294,9 +483,10 @@ func parseDSNParams(cfg *Config, params string) (err error) { } } - cfg.TLS = tlsConfig + cfg.TLSConfig = name + cfg.tls = tlsConfig } else { - return errors.New("invalid value / unknown config name: " + value) + return errors.New("invalid value / unknown config name: " + name) } } diff --git a/dsn_test.go b/dsn_test.go index d825069fc..80949e18a 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -19,20 +19,20 @@ var testDSNs = []struct { in string out string }{ - {"username:password@protocol(address)/dbname?param=value", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true&multiStatements=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:true ParseTime:false Strict:false}"}, - {"user@unix(/path/to/socket)/dbname?charset=utf8", "&{User:user Passwd: Net:unix Addr:/path/to/socket DBName:dbname Params:map[charset:utf8] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8mb4,utf8] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", "&{User:user Passwd:password Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Loc:UTC TLS: Timeout:30s ReadTimeout:1s WriteTimeout:1s Collation:224 AllowAllFiles:true AllowCleartextPasswords:false AllowOldPasswords:true ClientFoundRows:true ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{User:user Passwd:p@ss(word) Net:tcp Addr:[de:ad:be:ef::ca:fe]:80 DBName:dbname Params:map[] Loc:Local TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"/dbname", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"@/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"user:p@/ssword@/", "&{User:user Passwd:p@/ssword Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"unix/?arg=%2Fsome%2Fpath.ext", "&{User: Passwd: Net:unix Addr:/tmp/mysql.sock DBName: Params:map[arg:/some/path.ext] Loc:UTC TLS: Timeout:0 ReadTimeout:0 WriteTimeout:0 Collation:33 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"username:password@protocol(address)/dbname?param=value", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true&multiStatements=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:true ParseTime:false Strict:false}"}, + {"user@unix(/path/to/socket)/dbname?charset=utf8", "&{User:user Passwd: Net:unix Addr:/path/to/socket DBName:dbname Params:map[charset:utf8] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8] Collation:utf8_general_ci Loc:UTC TLSConfig:true tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8mb4,utf8] Collation:utf8_general_ci Loc:UTC TLSConfig:skip-verify tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", "&{User:user Passwd:password Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Collation:utf8mb4_unicode_ci Loc:UTC TLSConfig: tls: Timeout:30s ReadTimeout:1s WriteTimeout:1s AllowAllFiles:true AllowCleartextPasswords:false AllowOldPasswords:true ClientFoundRows:true ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{User:user Passwd:p@ss(word) Net:tcp Addr:[de:ad:be:ef::ca:fe]:80 DBName:dbname Params:map[] Collation:utf8_general_ci Loc:Local TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"/dbname", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"@/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"user:p@/ssword@/", "&{User:user Passwd:p@/ssword Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"unix/?arg=%2Fsome%2Fpath.ext", "&{User: Passwd: Net:unix Addr:/tmp/mysql.sock DBName: Params:map[arg:/some/path.ext] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, } func TestDSNParser(t *testing.T) { @@ -47,7 +47,7 @@ func TestDSNParser(t *testing.T) { } // pointer not static - cfg.TLS = nil + cfg.tls = nil res = fmt.Sprintf("%+v", cfg) if res != tst.out { @@ -74,6 +74,32 @@ func TestDSNParserInvalid(t *testing.T) { } } +func TestDSNReformat(t *testing.T) { + for i, tst := range testDSNs { + dsn1 := tst.in + cfg1, err := ParseDSN(dsn1) + if err != nil { + t.Error(err.Error()) + continue + } + cfg1.tls = nil // pointer not static + res1 := fmt.Sprintf("%+v", cfg1) + + dsn2 := cfg1.FormatDSN() + cfg2, err := ParseDSN(dsn2) + if err != nil { + t.Error(err.Error()) + continue + } + cfg2.tls = nil // pointer not static + res2 := fmt.Sprintf("%+v", cfg2) + + if res1 != res2 { + t.Errorf("%d. %q does not match %q", i, res2, res1) + } + } +} + func TestDSNWithCustomTLS(t *testing.T) { baseDSN := "User:password@tcp(localhost:5555)/dbname?tls=" tlsCfg := tls.Config{} @@ -96,7 +122,7 @@ func TestDSNWithCustomTLS(t *testing.T) { if err != nil { t.Error(err.Error()) - } else if cfg.TLS.ServerName != name { + } else if cfg.tls.ServerName != name { t.Errorf("did not get the correct TLS ServerName (%s) parsing DSN (%s).", name, tst) } @@ -107,14 +133,14 @@ func TestDSNWithCustomTLS(t *testing.T) { if err != nil { t.Error(err.Error()) - } else if cfg.TLS.ServerName != name { + } else if cfg.tls.ServerName != name { t.Errorf("did not get the correct ServerName (%s) parsing DSN (%s).", name, tst) } DeregisterTLSConfig("utils_test") } -func TestDSNWithCustomTLS_queryEscape(t *testing.T) { +func TestDSNWithCustomTLSQueryEscape(t *testing.T) { const configKey = "&%!:" dsn := "User:password@tcp(localhost:5555)/dbname?tls=" + url.QueryEscape(configKey) name := "foohost" @@ -126,7 +152,7 @@ func TestDSNWithCustomTLS_queryEscape(t *testing.T) { if err != nil { t.Error(err.Error()) - } else if cfg.TLS.ServerName != name { + } else if cfg.tls.ServerName != name { t.Errorf("did not get the correct TLS ServerName (%s) parsing DSN (%s).", name, dsn) } } diff --git a/packets.go b/packets.go index bf0927a54..d5ea888fd 100644 --- a/packets.go +++ b/packets.go @@ -13,6 +13,7 @@ import ( "crypto/tls" "database/sql/driver" "encoding/binary" + "errors" "fmt" "io" "math" @@ -166,7 +167,7 @@ func (mc *mysqlConn) readInitPacket() ([]byte, error) { if mc.flags&clientProtocol41 == 0 { return nil, ErrOldProtocol } - if mc.flags&clientSSL == 0 && mc.cfg.TLS != nil { + if mc.flags&clientSSL == 0 && mc.cfg.tls != nil { return nil, ErrNoTLS } pos += 2 @@ -232,7 +233,7 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte) error { } // To enable TLS / SSL - if mc.cfg.TLS != nil { + if mc.cfg.tls != nil { clientFlags |= clientSSL } @@ -272,18 +273,25 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte) error { data[11] = 0x00 // Charset [1 byte] - data[12] = mc.cfg.Collation + var found bool + data[12], found = collations[mc.cfg.Collation] + if !found { + // Note possibility for false negatives: + // could be triggered although the collation is valid if the + // collations map does not contain entries the server supports. + return errors.New("unknown collation") + } // SSL Connection Request Packet // http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::SSLRequest - if mc.cfg.TLS != nil { + if mc.cfg.tls != nil { // Send TLS / SSL request packet if err := mc.writePacket(data[:(4+4+1+23)+4]); err != nil { return err } // Switch to TLS - tlsConn := tls.Client(mc.netConn, mc.cfg.TLS) + tlsConn := tls.Client(mc.netConn, mc.cfg.tls) if err := tlsConn.Handshake(); err != nil { return err } From edc78806a0b42364059540b70046d0b174eba2fc Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Wed, 3 Feb 2016 11:58:20 +0100 Subject: [PATCH 33/65] README: Add reference to Config.FormatDSN --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 78a6bddf4..2e24e6cba 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ This has the same effect as an empty DSN string: ``` +Alternatively, [Config.FormatDSN](https://godoc.org/github.com/go-sql-driver/mysql#FormatDSN) can be used to create a DSN string by filling a struct. + #### Password Passwords can consist of any character. Escaping is **not** necessary. From 267b128680c46286b9ca13475c3cca5de8f79bd7 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Wed, 3 Feb 2016 12:00:21 +0100 Subject: [PATCH 34/65] README: Fix Config.FormatDSN url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e24e6cba..3adedc6a5 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ This has the same effect as an empty DSN string: ``` -Alternatively, [Config.FormatDSN](https://godoc.org/github.com/go-sql-driver/mysql#FormatDSN) can be used to create a DSN string by filling a struct. +Alternatively, [Config.FormatDSN](https://godoc.org/github.com/go-sql-driver/mysql#Config.FormatDSN) can be used to create a DSN string by filling a struct. #### Password Passwords can consist of any character. Escaping is **not** necessary. From 86752215ce94ed3d45995a44a83190eaaf8af8ba Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Wed, 17 Feb 2016 22:29:38 +0100 Subject: [PATCH 35/65] Create ISSUE_TEMPLATE.md See dear-github/dear-github#125 --- ISSUE_TEMPLATE.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 ISSUE_TEMPLATE.md diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..d9771f1dd --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,21 @@ +### Issue description +Tell us what should happen and what happens instead + +### Example code +```go +If possible, please enter some example code here to reproduce the issue. +``` + +### Error log +``` +If you have an error log, please paste it here. +``` + +### Configuration +*Driver version (or git SHA):* + +*Go version:* run `go version` in your console + +*Server version:* E.g. MySQL 5.6, MariaDB 10.0.20 + +*Server OS:* E.g. Debian 8.1 (Jessie), Windows 10 From 56df6f7a078d525c6882b2116547b08a25304991 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Wed, 17 Feb 2016 22:30:33 +0100 Subject: [PATCH 36/65] CONTRIBUTING.md: remove issue template --- CONTRIBUTING.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f87c19824..71df4833f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,14 +4,6 @@ Before creating a new Issue, please check first if a similar Issue [already exists](https://github.com/go-sql-driver/mysql/issues?state=open) or was [recently closed](https://github.com/go-sql-driver/mysql/issues?direction=desc&page=1&sort=updated&state=closed). -Please provide the following minimum information: -* Your Go-MySQL-Driver version (or git SHA) -* Your Go version (run `go version` in your console) -* A detailed issue description -* Error Log if present -* If possible, a short example - - ## Contributing Code By contributing to this project, you share your code under the Mozilla Public License 2, as specified in the LICENSE file. From 97f01d093504f91bd6b45bad0dce2f822082cf38 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Wed, 17 Feb 2016 22:43:32 +0100 Subject: [PATCH 37/65] Create PULL_REQUEST_TEMPLATE.md --- PULL_REQUEST_TEMPLATE.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 PULL_REQUEST_TEMPLATE.md diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..a2dd4bf1a --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +### Description +Please explain the changes you made here. + +### Checklist +- [ ] Code compiles correctly +- [ ] Created tests, if possible +- [ ] All tests passing +- [ ] Extended the README / documentation, if necessary +- [ ] Added myself / the copyright holder to the AUTHORS file From 1309049fd598c1faa0290fd70ab1afde2b6df0f9 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Wed, 17 Feb 2016 22:43:58 +0100 Subject: [PATCH 38/65] CONTRIBUTING.md: remove PR checklist --- CONTRIBUTING.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 71df4833f..8fe16bcb4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,15 +9,6 @@ Before creating a new Issue, please check first if a similar Issue [already exis By contributing to this project, you share your code under the Mozilla Public License 2, as specified in the LICENSE file. Don't forget to add yourself to the AUTHORS file. -### Pull Requests Checklist - -Please check the following points before submitting your pull request: -- [x] Code compiles correctly -- [x] Created tests, if possible -- [x] All tests pass -- [x] Extended the README / documentation, if necessary -- [x] Added yourself to the AUTHORS file - ### Code Review Everyone is invited to review and comment on pull requests. From e294c69faa7fe51f9b86daf8025f16891b2e78c1 Mon Sep 17 00:00:00 2001 From: INADA Naoki Date: Wed, 24 Feb 2016 08:30:32 +0900 Subject: [PATCH 39/65] LOAD LOCAL can send file larger than max_allowed_packet And max_allowed_packet may be too big for each packet. Use 16KB (default value of net_buffer_length) instead. --- infile.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/infile.go b/infile.go index e3e5e47d9..0f975bbc2 100644 --- a/infile.go +++ b/infile.go @@ -96,6 +96,10 @@ func deferredClose(err *error, closer io.Closer) { func (mc *mysqlConn) handleInFileRequest(name string) (err error) { var rdr io.Reader var data []byte + packetSize := 16 * 1024 // 16KB is small enough for disk readahead and large enough for TCP + if mc.maxWriteSize < packetSize { + packetSize = mc.maxWriteSize + } if idx := strings.Index(name, "Reader::"); idx == 0 || (idx > 0 && name[idx-1] == '/') { // io.Reader // The server might return an an absolute path. See issue #355. @@ -108,8 +112,6 @@ func (mc *mysqlConn) handleInFileRequest(name string) (err error) { if inMap { rdr = handler() if rdr != nil { - data = make([]byte, 4+mc.maxWriteSize) - if cl, ok := rdr.(io.Closer); ok { defer deferredClose(&err, cl) } @@ -134,12 +136,8 @@ func (mc *mysqlConn) handleInFileRequest(name string) (err error) { // get file size if fi, err = file.Stat(); err == nil { rdr = file - if fileSize := int(fi.Size()); fileSize <= mc.maxWriteSize { - data = make([]byte, 4+fileSize) - } else if fileSize <= mc.maxPacketAllowed { - data = make([]byte, 4+mc.maxWriteSize) - } else { - err = fmt.Errorf("local file '%s' too large: size: %d, max: %d", name, fileSize, mc.maxPacketAllowed) + if fileSize := int(fi.Size()); fileSize < packetSize { + packetSize = fileSize } } } @@ -150,6 +148,7 @@ func (mc *mysqlConn) handleInFileRequest(name string) (err error) { // send content packets if err == nil { + data := make([]byte, 4+packetSize) var n int for err == nil { n, err = rdr.Read(data[4:]) From 5ec60c2df7513482e621970f1727aaf3588292f4 Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Tue, 1 Mar 2016 10:06:08 +0100 Subject: [PATCH 40/65] add requirements to Config field comments --- dsn.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsn.go b/dsn.go index 29cce2d86..2be200493 100644 --- a/dsn.go +++ b/dsn.go @@ -29,9 +29,9 @@ var ( // Config is a configuration parsed from a DSN string type Config struct { User string // Username - Passwd string // Password + Passwd string // Password, requires User Net string // Network type - Addr string // Network address + Addr string // Network address, requires Net DBName string // Database name Params map[string]string // Connection parameters Collation string // Connection collation From fcfd2fed4fba75824abc9035b0aa1cd6c1ad6410 Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Tue, 1 Mar 2016 10:14:50 +0100 Subject: [PATCH 41/65] doc: tests should fail without the change --- PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index a2dd4bf1a..6f5c7ebeb 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -3,7 +3,7 @@ Please explain the changes you made here. ### Checklist - [ ] Code compiles correctly -- [ ] Created tests, if possible +- [ ] Created tests which fail without the change (if possible) - [ ] All tests passing - [ ] Extended the README / documentation, if necessary - [ ] Added myself / the copyright holder to the AUTHORS file From 193c826fa81d46a5594a0232452d74a1b448ff3c Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Tue, 1 Mar 2016 13:52:25 +0100 Subject: [PATCH 42/65] comment formatting --- dsn.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsn.go b/dsn.go index 2be200493..73138bc57 100644 --- a/dsn.go +++ b/dsn.go @@ -29,9 +29,9 @@ var ( // Config is a configuration parsed from a DSN string type Config struct { User string // Username - Passwd string // Password, requires User + Passwd string // Password (requires User) Net string // Network type - Addr string // Network address, requires Net + Addr string // Network address (requires Net) DBName string // Database name Params map[string]string // Connection parameters Collation string // Connection collation From 922457ad48a2381d4751bc4ea6dc13bb68ca3788 Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Tue, 1 Mar 2016 14:00:25 +0100 Subject: [PATCH 43/65] document multiStatement parameter restriction, fixes #426 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3adedc6a5..c64aae264 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,8 @@ Default: false Allow multiple statements in one query. While this allows batch queries, it also greatly increases the risk of SQL injections. Only the result of the first query is returned, all other results are silently discarded. +When `multiStatements` is used, `?` parameters must only be used in the first statement. + ##### `parseTime` From 5e1ab9e08ed7969517355d896ebb9f9156ba9f35 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Wed, 2 Mar 2016 09:10:16 +0800 Subject: [PATCH 44/65] Travis: Test with Go 1.6 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index c48cedd87..211969dfa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ go: - 1.3 - 1.4 - 1.5 + - 1.6 - tip before_script: From 0e81889e30976534839798a7c9e8e05245b66f07 Mon Sep 17 00:00:00 2001 From: Paul Bonser Date: Wed, 2 Mar 2016 18:44:17 -0600 Subject: [PATCH 45/65] test and fix for MysSQL float parsing into float64 when placeholders are used --- driver_test.go | 48 +++++++++++++++++++++++++++++++++++++++++++++++- packets.go | 2 +- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/driver_test.go b/driver_test.go index cd2a9168e..8275f23f1 100644 --- a/driver_test.go +++ b/driver_test.go @@ -365,7 +365,7 @@ func TestInt(t *testing.T) { }) } -func TestFloat(t *testing.T) { +func TestFloat32(t *testing.T) { runTests(t, dsn, func(dbt *DBTest) { types := [2]string{"FLOAT", "DOUBLE"} in := float32(42.23) @@ -388,6 +388,52 @@ func TestFloat(t *testing.T) { }) } +func TestFloat64(t *testing.T) { + runTests(t, dsn, func(dbt *DBTest) { + types := [2]string{"FLOAT", "DOUBLE"} + var expected float64 = 42.23 + var out float64 + var rows *sql.Rows + for _, v := range types { + dbt.mustExec("CREATE TABLE test (value " + v + ")") + dbt.mustExec("INSERT INTO test VALUES (42.23)") + rows = dbt.mustQuery("SELECT value FROM test") + if rows.Next() { + rows.Scan(&out) + if expected != out { + dbt.Errorf("%s: %g != %g", v, expected, out) + } + } else { + dbt.Errorf("%s: no data", v) + } + dbt.mustExec("DROP TABLE IF EXISTS test") + } + }) +} + +func TestFloat64Placeholder(t *testing.T) { + runTests(t, dsn, func(dbt *DBTest) { + types := [2]string{"FLOAT", "DOUBLE"} + var expected float64 = 42.23 + var out float64 + var rows *sql.Rows + for _, v := range types { + dbt.mustExec("CREATE TABLE test (id int, value " + v + ")") + dbt.mustExec("INSERT INTO test VALUES (1, 42.23)") + rows = dbt.mustQuery("SELECT value FROM test WHERE id = ?", 1) + if rows.Next() { + rows.Scan(&out) + if expected != out { + dbt.Errorf("%s: %g != %g", v, expected, out) + } + } else { + dbt.Errorf("%s: no data", v) + } + dbt.mustExec("DROP TABLE IF EXISTS test") + } + }) +} + func TestString(t *testing.T) { runTests(t, dsn, func(dbt *DBTest) { types := [6]string{"CHAR(255)", "VARCHAR(255)", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT"} diff --git a/packets.go b/packets.go index d5ea888fd..8d9166578 100644 --- a/packets.go +++ b/packets.go @@ -1149,7 +1149,7 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { continue case fieldTypeFloat: - dest[i] = float64(math.Float32frombits(binary.LittleEndian.Uint32(data[pos : pos+4]))) + dest[i] = float32(math.Float32frombits(binary.LittleEndian.Uint32(data[pos : pos+4]))) pos += 4 continue From f47acb0bcb0de44571aa4d663513be115aae388d Mon Sep 17 00:00:00 2001 From: Paul Bonser Date: Wed, 2 Mar 2016 18:46:41 -0600 Subject: [PATCH 46/65] Add myself to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 986f017cb..a5cc5779a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -36,6 +36,7 @@ Lucas Liu Luke Scott Michael Woolnough Nicola Peduzzi +Paul Bonser Runrioter Wung Soroush Pour Stan Putrya From 5a9482c4cda1b3c35d0f2b606f17d782ca0ce49e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Eeden?= Date: Mon, 7 Mar 2016 22:09:16 +0100 Subject: [PATCH 47/65] Fix error reported by `go vet` ``` $ go vet driver_test.go:308: arg out for printf verb %t of wrong type: int exit status 1 ``` --- AUTHORS | 1 + driver_test.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 986f017cb..5f50b0546 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,6 +16,7 @@ Arne Hormann Carlos Nieto Chris Moos Daniel Nichter +Daniël van Eeden DisposaBoy Frederick Mayle Gustavo Kristic diff --git a/driver_test.go b/driver_test.go index cd2a9168e..669aeb7c1 100644 --- a/driver_test.go +++ b/driver_test.go @@ -305,7 +305,7 @@ func TestMultiQuery(t *testing.T) { if rows.Next() { rows.Scan(&out) if 5 != out { - dbt.Errorf("5 != %t", out) + dbt.Errorf("5 != %d", out) } if rows.Next() { From 82cddefaf80ad3a19bec9ea4f96f5d6905cd3d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Eeden?= Date: Sun, 26 Apr 2015 12:36:43 +0200 Subject: [PATCH 48/65] Add support for connection attributes. This sets attribute _client_name with the value "Go MySQL Driver" Also sets _os, _platform, _pid and program_name by default. This also decodes the uppper two bytes of the capability flags. The dsn_test.go only tests for one attribute because there is no guaranteed sort order for a map and Printf %+v as used by TestDSNParser(). --- README.md | 10 +++++++++ driver.go | 13 +++++++++++ driver_test.go | 10 ++++----- dsn.go | 52 +++++++++++++++++++++++++++++++++++--------- dsn_test.go | 29 +++++++++++++------------ packets.go | 59 +++++++++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 143 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index c64aae264..c76c14bb5 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,16 @@ Sets the collation used for client-server interaction on connection. In contrast A list of valid charsets for a server is retrievable with `SHOW COLLATION`. +##### `connattrs` + +``` +Type: comma seperated string of name/value pairs +Valid Values: (=,=,...) +Default: none +``` + +Sends custom connection attributes to the server. + ##### `clientFoundRows` ``` diff --git a/driver.go b/driver.go index 899f955fb..dc912336e 100644 --- a/driver.go +++ b/driver.go @@ -20,6 +20,9 @@ import ( "database/sql" "database/sql/driver" "net" + "os" + "os/user" + "strconv" ) // MySQLDriver is exported to make the driver directly accessible. @@ -30,6 +33,10 @@ type MySQLDriver struct{} // Custom dial functions must be registered with RegisterDial type DialFunc func(addr string) (net.Conn, error) +var pid string +var os_user string +var os_user_full string + var dials map[string]DialFunc // RegisterDial registers a custom dial function. It can then be used by the @@ -163,5 +170,11 @@ func handleAuthResult(mc *mysqlConn, cipher []byte) error { } func init() { + pid = strconv.Itoa(os.Getpid()) + os_user_entry, err := user.Current() + if err == nil { + os_user_full = os_user_entry.Name + os_user = os_user_entry.Username + } sql.Register("mysql", &MySQLDriver{}) } diff --git a/driver_test.go b/driver_test.go index 669aeb7c1..0067e84a8 100644 --- a/driver_test.go +++ b/driver_test.go @@ -28,7 +28,7 @@ import ( ) var ( - user string + dbuser string pass string prot string addr string @@ -57,13 +57,13 @@ func init() { } return defaultValue } - user = env("MYSQL_TEST_USER", "root") + dbuser = env("MYSQL_TEST_USER", "root") pass = env("MYSQL_TEST_PASS", "") prot = env("MYSQL_TEST_PROT", "tcp") addr = env("MYSQL_TEST_ADDR", "localhost:3306") dbname = env("MYSQL_TEST_DBNAME", "gotest") netAddr = fmt.Sprintf("%s(%s)", prot, addr) - dsn = fmt.Sprintf("%s:%s@%s/%s?timeout=30s&strict=true", user, pass, netAddr, dbname) + dsn = fmt.Sprintf("%s:%s@%s/%s?timeout=30s&strict=true", dbuser, pass, netAddr, dbname) c, err := net.Dial(prot, addr) if err == nil { available = true @@ -1693,7 +1693,7 @@ func TestCustomDial(t *testing.T) { return net.Dial(prot, addr) }) - db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@mydial(%s)/%s?timeout=30s&strict=true", user, pass, addr, dbname)) + db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@mydial(%s)/%s?timeout=30s&strict=true", dbuser, pass, addr, dbname)) if err != nil { t.Fatalf("error connecting: %s", err.Error()) } @@ -1790,7 +1790,7 @@ func TestUnixSocketAuthFail(t *testing.T) { } } t.Logf("socket: %s", socket) - badDSN := fmt.Sprintf("%s:%s@unix(%s)/%s?timeout=30s&strict=true", user, badPass, socket, dbname) + badDSN := fmt.Sprintf("%s:%s@unix(%s)/%s?timeout=30s&strict=true", dbuser, badPass, socket, dbname) db, err := sql.Open("mysql", badDSN) if err != nil { t.Fatalf("error connecting: %s", err.Error()) diff --git a/dsn.go b/dsn.go index 73138bc57..ef408043d 100644 --- a/dsn.go +++ b/dsn.go @@ -42,15 +42,16 @@ type Config struct { ReadTimeout time.Duration // I/O read timeout WriteTimeout time.Duration // I/O write timeout - AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE - AllowCleartextPasswords bool // Allows the cleartext client side plugin - AllowOldPasswords bool // Allows the old insecure password method - ClientFoundRows bool // Return number of matching rows instead of rows changed - ColumnsWithAlias bool // Prepend table alias to column names - InterpolateParams bool // Interpolate placeholders into query string - MultiStatements bool // Allow multiple statements in one query - ParseTime bool // Parse time values to time.Time - Strict bool // Return warnings as errors + AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE + AllowCleartextPasswords bool // Allows the cleartext client side plugin + AllowOldPasswords bool // Allows the old insecure password method + ClientFoundRows bool // Return number of matching rows instead of rows changed + ColumnsWithAlias bool // Prepend table alias to column names + InterpolateParams bool // Interpolate placeholders into query string + MultiStatements bool // Allow multiple statements in one query + ParseTime bool // Parse time values to time.Time + Strict bool // Return warnings as errors + ConnAttrs map[string]string // Connection Attributes } // FormatDSN formats the given Config into a DSN string which can be passed to @@ -222,6 +223,27 @@ func (cfg *Config) FormatDSN() string { buf.WriteString(cfg.WriteTimeout.String()) } + if len(cfg.ConnAttrs) != 0 { + if hasParam { + buf.WriteString("&connattrs=(") + } else { + hasParam = true + buf.WriteString("?connattrs=(") + } + firstAttr := true + for attrname, attrvalue := range cfg.ConnAttrs { + if firstAttr { + firstAttr = false + } else { + buf.WriteString(",") + } + buf.WriteString(attrname) + buf.WriteString("=") + buf.WriteString(attrvalue) + } + buf.WriteString(")") + } + // other params if cfg.Params != nil { for param, value := range cfg.Params { @@ -496,7 +518,17 @@ func parseDSNParams(cfg *Config, params string) (err error) { if err != nil { return } - + case "connattrs": + if cfg.ConnAttrs == nil { + cfg.ConnAttrs = make(map[string]string) + } + for _, conn_v := range strings.Split(strings.Trim(value, "()"), ",") { + attr := strings.SplitN(conn_v, "=", 2) + if len(attr) != 2 { + return fmt.Errorf("Invalid connection attribute: %s", conn_v) + } + cfg.ConnAttrs[attr[0]] = attr[1] + } default: // lazy init if cfg.Params == nil { diff --git a/dsn_test.go b/dsn_test.go index 80949e18a..0af1b0141 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -19,20 +19,21 @@ var testDSNs = []struct { in string out string }{ - {"username:password@protocol(address)/dbname?param=value", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true&multiStatements=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:true ParseTime:false Strict:false}"}, - {"user@unix(/path/to/socket)/dbname?charset=utf8", "&{User:user Passwd: Net:unix Addr:/path/to/socket DBName:dbname Params:map[charset:utf8] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8] Collation:utf8_general_ci Loc:UTC TLSConfig:true tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8mb4,utf8] Collation:utf8_general_ci Loc:UTC TLSConfig:skip-verify tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", "&{User:user Passwd:password Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Collation:utf8mb4_unicode_ci Loc:UTC TLSConfig: tls: Timeout:30s ReadTimeout:1s WriteTimeout:1s AllowAllFiles:true AllowCleartextPasswords:false AllowOldPasswords:true ClientFoundRows:true ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{User:user Passwd:p@ss(word) Net:tcp Addr:[de:ad:be:ef::ca:fe]:80 DBName:dbname Params:map[] Collation:utf8_general_ci Loc:Local TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"/dbname", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"@/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"user:p@/ssword@/", "&{User:user Passwd:p@/ssword Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"unix/?arg=%2Fsome%2Fpath.ext", "&{User: Passwd: Net:unix Addr:/tmp/mysql.sock DBName: Params:map[arg:/some/path.ext] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"username:password@protocol(address)/dbname?param=value", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, + {"username:password@protocol(address)/dbname?param=value&connattrs=(foo=bar)", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[foo:bar]}"}, + {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, + {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true&multiStatements=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:true ParseTime:false Strict:false ConnAttrs:map[]}"}, + {"user@unix(/path/to/socket)/dbname?charset=utf8", "&{User:user Passwd: Net:unix Addr:/path/to/socket DBName:dbname Params:map[charset:utf8] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, + {"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8] Collation:utf8_general_ci Loc:UTC TLSConfig:true tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, + {"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8mb4,utf8] Collation:utf8_general_ci Loc:UTC TLSConfig:skip-verify tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, + {"user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", "&{User:user Passwd:password Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Collation:utf8mb4_unicode_ci Loc:UTC TLSConfig: tls: Timeout:30s ReadTimeout:1s WriteTimeout:1s AllowAllFiles:true AllowCleartextPasswords:false AllowOldPasswords:true ClientFoundRows:true ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, + {"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{User:user Passwd:p@ss(word) Net:tcp Addr:[de:ad:be:ef::ca:fe]:80 DBName:dbname Params:map[] Collation:utf8_general_ci Loc:Local TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, + {"/dbname", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, + {"@/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, + {"/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, + {"", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, + {"user:p@/ssword@/", "&{User:user Passwd:p@/ssword Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, + {"unix/?arg=%2Fsome%2Fpath.ext", "&{User: Passwd: Net:unix Addr:/tmp/mysql.sock DBName: Params:map[arg:/some/path.ext] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, } func TestDSNParser(t *testing.T) { diff --git a/packets.go b/packets.go index d5ea888fd..3941912d5 100644 --- a/packets.go +++ b/packets.go @@ -17,6 +17,9 @@ import ( "fmt" "io" "math" + "os" + "path" + "runtime" "time" ) @@ -175,10 +178,15 @@ func (mc *mysqlConn) readInitPacket() ([]byte, 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(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) @@ -246,6 +254,40 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte) error { pktLen := 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1 + 1 + len(scrambleBuff) + 21 + 1 + // Default connection attributes + attrlen := 0 + var attrs map[string]string + if mc.flags&clientConnectAttrs != 0 { + clientFlags |= clientConnectAttrs + + attrs = map[string]string{ + "_os": runtime.GOOS, + "_client_name": "Go-MySQL-Driver", + "_pid": pid, + "_platform": runtime.GOARCH, + "program_name": path.Base(os.Args[0]), + } + if len(os_user_full) > 0 { + attrs["_os_user_full"] = os_user_full + } + if len(os_user) > 0 { + attrs["_os_user"] = os_user + } + + // Merge the custom attributes and the default attributes + for cfganame, cfgaval := range mc.cfg.ConnAttrs { + attrs[cfganame] = cfgaval + } + + for attrname, attrvalue := range attrs { + attrlen += len(attrname) + len(attrvalue) + // one byte to store attrname length and one byte to store attrvalue length + attrlen += 2 + } + + pktLen += attrlen + 1 // one byte to store the total length of attrs + } + // To specify a db name if n := len(mc.cfg.DBName); n > 0 { clientFlags |= clientConnectWithDB @@ -326,6 +368,21 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte) error { // Assume native client during response pos += copy(data[pos:], "mysql_native_password") data[pos] = 0x00 + pos++ + + // Connection attributes + if attrlen > 0 { + data[pos] = byte(attrlen) + pos++ + + for attrname, attrvalue := range attrs { + data[pos] = byte(len(attrname)) + pos += 1 + copy(data[pos+1:], attrname) + + data[pos] = byte(len(attrvalue)) + pos += 1 + copy(data[pos+1:], attrvalue) + } + } // Send Auth packet return mc.writePacket(data) From 1421caf44f6464fd2ee8de694c7508ee13f92964 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Tue, 29 Mar 2016 11:03:53 +0200 Subject: [PATCH 49/65] Revert "Merge pull request #350 from dveeden/connattrs_v4" This reverts commit 2625e19062f794bf12c1fef2a41420f23851ccef, reversing changes made to 66312f7fe2678aa0f5ec770f96702f4c4ec5aa8e. --- README.md | 10 --------- driver.go | 13 ----------- driver_test.go | 10 ++++----- dsn.go | 52 +++++++++----------------------------------- dsn_test.go | 29 ++++++++++++------------- packets.go | 59 +------------------------------------------------- 6 files changed, 30 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index c76c14bb5..c64aae264 100644 --- a/README.md +++ b/README.md @@ -169,16 +169,6 @@ Sets the collation used for client-server interaction on connection. In contrast A list of valid charsets for a server is retrievable with `SHOW COLLATION`. -##### `connattrs` - -``` -Type: comma seperated string of name/value pairs -Valid Values: (=,=,...) -Default: none -``` - -Sends custom connection attributes to the server. - ##### `clientFoundRows` ``` diff --git a/driver.go b/driver.go index dc912336e..899f955fb 100644 --- a/driver.go +++ b/driver.go @@ -20,9 +20,6 @@ import ( "database/sql" "database/sql/driver" "net" - "os" - "os/user" - "strconv" ) // MySQLDriver is exported to make the driver directly accessible. @@ -33,10 +30,6 @@ type MySQLDriver struct{} // Custom dial functions must be registered with RegisterDial type DialFunc func(addr string) (net.Conn, error) -var pid string -var os_user string -var os_user_full string - var dials map[string]DialFunc // RegisterDial registers a custom dial function. It can then be used by the @@ -170,11 +163,5 @@ func handleAuthResult(mc *mysqlConn, cipher []byte) error { } func init() { - pid = strconv.Itoa(os.Getpid()) - os_user_entry, err := user.Current() - if err == nil { - os_user_full = os_user_entry.Name - os_user = os_user_entry.Username - } sql.Register("mysql", &MySQLDriver{}) } diff --git a/driver_test.go b/driver_test.go index 0067e84a8..669aeb7c1 100644 --- a/driver_test.go +++ b/driver_test.go @@ -28,7 +28,7 @@ import ( ) var ( - dbuser string + user string pass string prot string addr string @@ -57,13 +57,13 @@ func init() { } return defaultValue } - dbuser = env("MYSQL_TEST_USER", "root") + user = env("MYSQL_TEST_USER", "root") pass = env("MYSQL_TEST_PASS", "") prot = env("MYSQL_TEST_PROT", "tcp") addr = env("MYSQL_TEST_ADDR", "localhost:3306") dbname = env("MYSQL_TEST_DBNAME", "gotest") netAddr = fmt.Sprintf("%s(%s)", prot, addr) - dsn = fmt.Sprintf("%s:%s@%s/%s?timeout=30s&strict=true", dbuser, pass, netAddr, dbname) + dsn = fmt.Sprintf("%s:%s@%s/%s?timeout=30s&strict=true", user, pass, netAddr, dbname) c, err := net.Dial(prot, addr) if err == nil { available = true @@ -1693,7 +1693,7 @@ func TestCustomDial(t *testing.T) { return net.Dial(prot, addr) }) - db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@mydial(%s)/%s?timeout=30s&strict=true", dbuser, pass, addr, dbname)) + db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@mydial(%s)/%s?timeout=30s&strict=true", user, pass, addr, dbname)) if err != nil { t.Fatalf("error connecting: %s", err.Error()) } @@ -1790,7 +1790,7 @@ func TestUnixSocketAuthFail(t *testing.T) { } } t.Logf("socket: %s", socket) - badDSN := fmt.Sprintf("%s:%s@unix(%s)/%s?timeout=30s&strict=true", dbuser, badPass, socket, dbname) + badDSN := fmt.Sprintf("%s:%s@unix(%s)/%s?timeout=30s&strict=true", user, badPass, socket, dbname) db, err := sql.Open("mysql", badDSN) if err != nil { t.Fatalf("error connecting: %s", err.Error()) diff --git a/dsn.go b/dsn.go index ef408043d..73138bc57 100644 --- a/dsn.go +++ b/dsn.go @@ -42,16 +42,15 @@ type Config struct { ReadTimeout time.Duration // I/O read timeout WriteTimeout time.Duration // I/O write timeout - AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE - AllowCleartextPasswords bool // Allows the cleartext client side plugin - AllowOldPasswords bool // Allows the old insecure password method - ClientFoundRows bool // Return number of matching rows instead of rows changed - ColumnsWithAlias bool // Prepend table alias to column names - InterpolateParams bool // Interpolate placeholders into query string - MultiStatements bool // Allow multiple statements in one query - ParseTime bool // Parse time values to time.Time - Strict bool // Return warnings as errors - ConnAttrs map[string]string // Connection Attributes + AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE + AllowCleartextPasswords bool // Allows the cleartext client side plugin + AllowOldPasswords bool // Allows the old insecure password method + ClientFoundRows bool // Return number of matching rows instead of rows changed + ColumnsWithAlias bool // Prepend table alias to column names + InterpolateParams bool // Interpolate placeholders into query string + MultiStatements bool // Allow multiple statements in one query + ParseTime bool // Parse time values to time.Time + Strict bool // Return warnings as errors } // FormatDSN formats the given Config into a DSN string which can be passed to @@ -223,27 +222,6 @@ func (cfg *Config) FormatDSN() string { buf.WriteString(cfg.WriteTimeout.String()) } - if len(cfg.ConnAttrs) != 0 { - if hasParam { - buf.WriteString("&connattrs=(") - } else { - hasParam = true - buf.WriteString("?connattrs=(") - } - firstAttr := true - for attrname, attrvalue := range cfg.ConnAttrs { - if firstAttr { - firstAttr = false - } else { - buf.WriteString(",") - } - buf.WriteString(attrname) - buf.WriteString("=") - buf.WriteString(attrvalue) - } - buf.WriteString(")") - } - // other params if cfg.Params != nil { for param, value := range cfg.Params { @@ -518,17 +496,7 @@ func parseDSNParams(cfg *Config, params string) (err error) { if err != nil { return } - case "connattrs": - if cfg.ConnAttrs == nil { - cfg.ConnAttrs = make(map[string]string) - } - for _, conn_v := range strings.Split(strings.Trim(value, "()"), ",") { - attr := strings.SplitN(conn_v, "=", 2) - if len(attr) != 2 { - return fmt.Errorf("Invalid connection attribute: %s", conn_v) - } - cfg.ConnAttrs[attr[0]] = attr[1] - } + default: // lazy init if cfg.Params == nil { diff --git a/dsn_test.go b/dsn_test.go index 0af1b0141..80949e18a 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -19,21 +19,20 @@ var testDSNs = []struct { in string out string }{ - {"username:password@protocol(address)/dbname?param=value", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, - {"username:password@protocol(address)/dbname?param=value&connattrs=(foo=bar)", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[foo:bar]}"}, - {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, - {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true&multiStatements=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:true ParseTime:false Strict:false ConnAttrs:map[]}"}, - {"user@unix(/path/to/socket)/dbname?charset=utf8", "&{User:user Passwd: Net:unix Addr:/path/to/socket DBName:dbname Params:map[charset:utf8] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, - {"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8] Collation:utf8_general_ci Loc:UTC TLSConfig:true tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, - {"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8mb4,utf8] Collation:utf8_general_ci Loc:UTC TLSConfig:skip-verify tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, - {"user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", "&{User:user Passwd:password Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Collation:utf8mb4_unicode_ci Loc:UTC TLSConfig: tls: Timeout:30s ReadTimeout:1s WriteTimeout:1s AllowAllFiles:true AllowCleartextPasswords:false AllowOldPasswords:true ClientFoundRows:true ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, - {"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{User:user Passwd:p@ss(word) Net:tcp Addr:[de:ad:be:ef::ca:fe]:80 DBName:dbname Params:map[] Collation:utf8_general_ci Loc:Local TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, - {"/dbname", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, - {"@/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, - {"/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, - {"", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, - {"user:p@/ssword@/", "&{User:user Passwd:p@/ssword Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, - {"unix/?arg=%2Fsome%2Fpath.ext", "&{User: Passwd: Net:unix Addr:/tmp/mysql.sock DBName: Params:map[arg:/some/path.ext] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false ConnAttrs:map[]}"}, + {"username:password@protocol(address)/dbname?param=value", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true&multiStatements=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:true ParseTime:false Strict:false}"}, + {"user@unix(/path/to/socket)/dbname?charset=utf8", "&{User:user Passwd: Net:unix Addr:/path/to/socket DBName:dbname Params:map[charset:utf8] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8] Collation:utf8_general_ci Loc:UTC TLSConfig:true tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8mb4,utf8] Collation:utf8_general_ci Loc:UTC TLSConfig:skip-verify tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", "&{User:user Passwd:password Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Collation:utf8mb4_unicode_ci Loc:UTC TLSConfig: tls: Timeout:30s ReadTimeout:1s WriteTimeout:1s AllowAllFiles:true AllowCleartextPasswords:false AllowOldPasswords:true ClientFoundRows:true ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{User:user Passwd:p@ss(word) Net:tcp Addr:[de:ad:be:ef::ca:fe]:80 DBName:dbname Params:map[] Collation:utf8_general_ci Loc:Local TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"/dbname", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"@/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"user:p@/ssword@/", "&{User:user Passwd:p@/ssword Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, + {"unix/?arg=%2Fsome%2Fpath.ext", "&{User: Passwd: Net:unix Addr:/tmp/mysql.sock DBName: Params:map[arg:/some/path.ext] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, } func TestDSNParser(t *testing.T) { diff --git a/packets.go b/packets.go index 3941912d5..d5ea888fd 100644 --- a/packets.go +++ b/packets.go @@ -17,9 +17,6 @@ import ( "fmt" "io" "math" - "os" - "path" - "runtime" "time" ) @@ -178,15 +175,10 @@ func (mc *mysqlConn) readInitPacket() ([]byte, 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(binary.LittleEndian.Uint16(data[pos:pos+2])) << 16) - pos += 2 - // length of auth-plugin-data [1 byte] // reserved (all [00]) [10 bytes] - pos += 1 + 10 + pos += 1 + 2 + 2 + 1 + 10 // second part of the password cipher [mininum 13 bytes], // where len=MAX(13, length of auth-plugin-data - 8) @@ -254,40 +246,6 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte) error { pktLen := 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1 + 1 + len(scrambleBuff) + 21 + 1 - // Default connection attributes - attrlen := 0 - var attrs map[string]string - if mc.flags&clientConnectAttrs != 0 { - clientFlags |= clientConnectAttrs - - attrs = map[string]string{ - "_os": runtime.GOOS, - "_client_name": "Go-MySQL-Driver", - "_pid": pid, - "_platform": runtime.GOARCH, - "program_name": path.Base(os.Args[0]), - } - if len(os_user_full) > 0 { - attrs["_os_user_full"] = os_user_full - } - if len(os_user) > 0 { - attrs["_os_user"] = os_user - } - - // Merge the custom attributes and the default attributes - for cfganame, cfgaval := range mc.cfg.ConnAttrs { - attrs[cfganame] = cfgaval - } - - for attrname, attrvalue := range attrs { - attrlen += len(attrname) + len(attrvalue) - // one byte to store attrname length and one byte to store attrvalue length - attrlen += 2 - } - - pktLen += attrlen + 1 // one byte to store the total length of attrs - } - // To specify a db name if n := len(mc.cfg.DBName); n > 0 { clientFlags |= clientConnectWithDB @@ -368,21 +326,6 @@ func (mc *mysqlConn) writeAuthPacket(cipher []byte) error { // Assume native client during response pos += copy(data[pos:], "mysql_native_password") data[pos] = 0x00 - pos++ - - // Connection attributes - if attrlen > 0 { - data[pos] = byte(attrlen) - pos++ - - for attrname, attrvalue := range attrs { - data[pos] = byte(len(attrname)) - pos += 1 + copy(data[pos+1:], attrname) - - data[pos] = byte(len(attrvalue)) - pos += 1 + copy(data[pos+1:], attrvalue) - } - } // Send Auth packet return mc.writePacket(data) From 152796f08d0dd2539e07171d0a8733cf18efd4ed Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 1 Jun 2016 16:54:27 -0700 Subject: [PATCH 50/65] Fix dsn_test.go to work with Go 1.7 --- dsn_test.go | 74 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/dsn_test.go b/dsn_test.go index 80949e18a..e6f0f83b1 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -12,36 +12,61 @@ import ( "crypto/tls" "fmt" "net/url" + "reflect" "testing" + "time" ) var testDSNs = []struct { in string - out string -}{ - {"username:password@protocol(address)/dbname?param=value", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true&multiStatements=true", "&{User:username Passwd:password Net:protocol Addr:address DBName:dbname Params:map[param:value] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:true InterpolateParams:false MultiStatements:true ParseTime:false Strict:false}"}, - {"user@unix(/path/to/socket)/dbname?charset=utf8", "&{User:user Passwd: Net:unix Addr:/path/to/socket DBName:dbname Params:map[charset:utf8] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8] Collation:utf8_general_ci Loc:UTC TLSConfig:true tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "&{User:user Passwd:password Net:tcp Addr:localhost:5555 DBName:dbname Params:map[charset:utf8mb4,utf8] Collation:utf8_general_ci Loc:UTC TLSConfig:skip-verify tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", "&{User:user Passwd:password Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Collation:utf8mb4_unicode_ci Loc:UTC TLSConfig: tls: Timeout:30s ReadTimeout:1s WriteTimeout:1s AllowAllFiles:true AllowCleartextPasswords:false AllowOldPasswords:true ClientFoundRows:true ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{User:user Passwd:p@ss(word) Net:tcp Addr:[de:ad:be:ef::ca:fe]:80 DBName:dbname Params:map[] Collation:utf8_general_ci Loc:Local TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"/dbname", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName:dbname Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"@/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"/", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"", "&{User: Passwd: Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"user:p@/ssword@/", "&{User:user Passwd:p@/ssword Net:tcp Addr:127.0.0.1:3306 DBName: Params:map[] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, - {"unix/?arg=%2Fsome%2Fpath.ext", "&{User: Passwd: Net:unix Addr:/tmp/mysql.sock DBName: Params:map[arg:/some/path.ext] Collation:utf8_general_ci Loc:UTC TLSConfig: tls: Timeout:0 ReadTimeout:0 WriteTimeout:0 AllowAllFiles:false AllowCleartextPasswords:false AllowOldPasswords:false ClientFoundRows:false ColumnsWithAlias:false InterpolateParams:false MultiStatements:false ParseTime:false Strict:false}"}, -} + out *Config +}{{ + "username:password@protocol(address)/dbname?param=value", + &Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Collation: "utf8_general_ci", Loc: time.UTC}, +}, { + "username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", + &Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Collation: "utf8_general_ci", Loc: time.UTC, ColumnsWithAlias: true}, +}, { + "username:password@protocol(address)/dbname?param=value&columnsWithAlias=true&multiStatements=true", + &Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Collation: "utf8_general_ci", Loc: time.UTC, ColumnsWithAlias: true, MultiStatements: true}, +}, { + "user@unix(/path/to/socket)/dbname?charset=utf8", + &Config{User: "user", Net: "unix", Addr: "/path/to/socket", DBName: "dbname", Params: map[string]string{"charset": "utf8"}, Collation: "utf8_general_ci", Loc: time.UTC}, +}, { + "user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", + &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", Params: map[string]string{"charset": "utf8"}, Collation: "utf8_general_ci", Loc: time.UTC, TLSConfig: "true"}, +}, { + "user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", + &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", Params: map[string]string{"charset": "utf8mb4,utf8"}, Collation: "utf8_general_ci", Loc: time.UTC, TLSConfig: "skip-verify"}, +}, { + "user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", + &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_unicode_ci", Loc: time.UTC, Timeout: 30 * time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, AllowAllFiles: true, AllowOldPasswords: true, ClientFoundRows: true}, +}, { + "user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", + &Config{User: "user", Passwd: "p@ss(word)", Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:80", DBName: "dbname", Collation: "utf8_general_ci", Loc: time.Local}, +}, { + "/dbname", + &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8_general_ci", Loc: time.UTC}, +}, { + "@/", + &Config{Net: "tcp", Addr: "127.0.0.1:3306", Collation: "utf8_general_ci", Loc: time.UTC}, +}, { + "/", + &Config{Net: "tcp", Addr: "127.0.0.1:3306", Collation: "utf8_general_ci", Loc: time.UTC}, +}, { + "", + &Config{Net: "tcp", Addr: "127.0.0.1:3306", Collation: "utf8_general_ci", Loc: time.UTC}, +}, { + "user:p@/ssword@/", + &Config{User: "user", Passwd: "p@/ssword", Net: "tcp", Addr: "127.0.0.1:3306", Collation: "utf8_general_ci", Loc: time.UTC}, +}, { + "unix/?arg=%2Fsome%2Fpath.ext", + &Config{Net: "unix", Addr: "/tmp/mysql.sock", Params: map[string]string{"arg": "/some/path.ext"}, Collation: "utf8_general_ci", Loc: time.UTC}, +}} func TestDSNParser(t *testing.T) { - var cfg *Config - var err error - var res string - for i, tst := range testDSNs { - cfg, err = ParseDSN(tst.in) + cfg, err := ParseDSN(tst.in) if err != nil { t.Error(err.Error()) } @@ -49,9 +74,8 @@ func TestDSNParser(t *testing.T) { // pointer not static cfg.tls = nil - res = fmt.Sprintf("%+v", cfg) - if res != tst.out { - t.Errorf("%d. ParseDSN(%q) => %q, want %q", i, tst.in, res, tst.out) + if !reflect.DeepEqual(cfg, tst.out) { + t.Errorf("%d. ParseDSN(%q) mismatch:\ngot %+v\nwant %+v", i, tst.in, cfg, tst.out) } } } From c6f610244e521d03a0725d224face04af862ce5a Mon Sep 17 00:00:00 2001 From: INADA Naoki Date: Tue, 2 Aug 2016 20:31:16 +0900 Subject: [PATCH 51/65] readUntilEOF should support ERR packet fixes #464 --- packets.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packets.go b/packets.go index 8d9166578..602539942 100644 --- a/packets.go +++ b/packets.go @@ -729,16 +729,19 @@ func (rows *textRows) readRow(dest []driver.Value) error { func (mc *mysqlConn) readUntilEOF() error { for { data, err := mc.readPacket() - - // No Err and no EOF Packet - if err == nil && data[0] != iEOF { - continue - } - if err == nil && data[0] == iEOF && len(data) == 5 { - mc.status = readStatus(data[3:]) + if err != nil { + return err } - return err // Err or EOF + switch data[0] { + case iERR: + return mc.handleErrorPacket(data) + case iEOF: + if len(data) == 5 { + mc.status = readStatus(data[3:]) + } + return nil + } } } From 127a48c554cd8f89377af10729d13b314e084ea5 Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Thu, 20 Oct 2016 16:15:23 +0800 Subject: [PATCH 52/65] travis.yml: Test with Go 1.7 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 211969dfa..c1cc10aaf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ go: - 1.4 - 1.5 - 1.6 + - 1.7 - tip before_script: From 7fcf04e8930f764af31d3f5453aa42b9fc4236d6 Mon Sep 17 00:00:00 2001 From: INADA Naoki Date: Thu, 20 Oct 2016 16:38:32 +0900 Subject: [PATCH 53/65] Add test to reproduce #467 --- connection_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 connection_test.go diff --git a/connection_test.go b/connection_test.go new file mode 100644 index 000000000..fcd645b61 --- /dev/null +++ b/connection_test.go @@ -0,0 +1,49 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// +// Copyright 2013 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 ( + "database/sql/driver" + "testing" +) + +func TestInterpolateParams(t *testing.T) { + mc := &mysqlConn{ + buf: newBuffer(nil), + maxPacketAllowed: maxPacketSize, + cfg: &Config{ + InterpolateParams: true, + }, + } + + q, err := mc.interpolateParams("SELECT ?+?", []driver.Value{int64(42), "gopher"}) + if err != nil { + t.Errorf("Expected err=nil, got %#v", err) + return + } + expected := `SELECT 42+'gopher'` + if q != expected { + t.Errorf("Expected: %q\nGot: %q", expected, q) + } +} + +func TestInterpolateParamsTooManyPlaceholders(t *testing.T) { + mc := &mysqlConn{ + buf: newBuffer(nil), + maxPacketAllowed: maxPacketSize, + cfg: &Config{ + InterpolateParams: true, + }, + } + + q, err := mc.interpolateParams("SELECT ?+?", []driver.Value{int64(42)}) + if err != driver.ErrSkip { + t.Errorf("Expected err=driver.ErrSkip, got err=%#v, q=%#v", err, q) + } +} From a044747e9cdea31131341abda07572071b848386 Mon Sep 17 00:00:00 2001 From: INADA Naoki Date: Thu, 20 Oct 2016 16:42:19 +0900 Subject: [PATCH 54/65] Fix index out of range in interpolateParams. Fixes #467 --- connection.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/connection.go b/connection.go index c3899de0e..8fd3c42c5 100644 --- a/connection.go +++ b/connection.go @@ -153,6 +153,9 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin buf = append(buf, query[i:i+q]...) i += q + if argPos >= len(args) { + return "", driver.ErrSkip + } arg := args[argPos] argPos++ From 82e447e7adb5043262404130cd7e10d61c077f89 Mon Sep 17 00:00:00 2001 From: INADA Naoki Date: Thu, 20 Oct 2016 17:18:52 +0900 Subject: [PATCH 55/65] fixup --- connection.go | 8 +++++--- connection_test.go | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/connection.go b/connection.go index 8fd3c42c5..d37e36dea 100644 --- a/connection.go +++ b/connection.go @@ -135,6 +135,11 @@ func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) { } func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (string, error) { + // Number of ? should be same to len(args) + if strings.Count(query, "?") != len(args) { + return "", driver.ErrSkip + } + buf := mc.buf.takeCompleteBuffer() if buf == nil { // can not take the buffer. Something must be wrong with the connection @@ -153,9 +158,6 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin buf = append(buf, query[i:i+q]...) i += q - if argPos >= len(args) { - return "", driver.ErrSkip - } arg := args[argPos] argPos++ diff --git a/connection_test.go b/connection_test.go index fcd645b61..1325d7ec7 100644 --- a/connection_test.go +++ b/connection_test.go @@ -1,6 +1,6 @@ // Go MySQL Driver - A MySQL-Driver for Go's database/sql package // -// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved. +// Copyright 2016 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, @@ -47,3 +47,19 @@ func TestInterpolateParamsTooManyPlaceholders(t *testing.T) { t.Errorf("Expected err=driver.ErrSkip, got err=%#v, q=%#v", err, q) } } + +// We don't support placeholder in string literal for now. +func TestInterpolateParamsPlaceholderInString(t *testing.T) { + mc := &mysqlConn{ + buf: newBuffer(nil), + maxPacketAllowed: maxPacketSize, + cfg: &Config{ + InterpolateParams: true, + }, + } + + q, err := mc.interpolateParams("SELECT 'abc?xyz',?", []driver.Value{int64(42)}) + if err != driver.ErrSkip { + t.Errorf("Expected err=driver.ErrSkip, got err=%#v, q=%#v", err, q) + } +} From dbc3fe23a4aced10a53834106e288b155d17e2ee Mon Sep 17 00:00:00 2001 From: INADA Naoki Date: Thu, 20 Oct 2016 19:29:09 +0900 Subject: [PATCH 56/65] add comment --- connection_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/connection_test.go b/connection_test.go index 1325d7ec7..7111e4a6b 100644 --- a/connection_test.go +++ b/connection_test.go @@ -49,6 +49,7 @@ func TestInterpolateParamsTooManyPlaceholders(t *testing.T) { } // We don't support placeholder in string literal for now. +// https://github.com/go-sql-driver/mysql/pull/490 func TestInterpolateParamsPlaceholderInString(t *testing.T) { mc := &mysqlConn{ buf: newBuffer(nil), @@ -59,6 +60,7 @@ func TestInterpolateParamsPlaceholderInString(t *testing.T) { } q, err := mc.interpolateParams("SELECT 'abc?xyz',?", []driver.Value{int64(42)}) + // When InterpolateParams support string literal, this should return `"SELECT 'abc?xyz', 42` if err != driver.ErrSkip { t.Errorf("Expected err=driver.ErrSkip, got err=%#v, q=%#v", err, q) } From 1983bc3dfe6926f139ae8e95db54c3a6ee28518e Mon Sep 17 00:00:00 2001 From: Lennart Rudolph Date: Tue, 25 Oct 2016 08:58:28 -0700 Subject: [PATCH 57/65] README: Include Difference in Connection Strings depending on Cloud SQL Generation Fixes #481, #484 --- AUTHORS | 1 + README.md | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 3774919d7..37bf11a2b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -31,6 +31,7 @@ Julien Lefevre Julien Schmidt Kamil Dziedzic Kevin Malachowski +Lennart Rudolph Leonardo YongUk Kim Luca Looz Lucas Liu diff --git a/README.md b/README.md index c64aae264..388632816 100644 --- a/README.md +++ b/README.md @@ -337,11 +337,16 @@ TCP on a remote host, e.g. Amazon RDS: id:password@tcp(your-amazonaws-uri.com:3306)/dbname ``` -Google Cloud SQL on App Engine: +Google Cloud SQL on App Engine (First Generation MySQL Server): ``` user@cloudsql(project-id:instance-name)/dbname ``` +Google Cloud SQL on App Engine (Second Generation MySQL Server): +``` +user@cloudsql(project-id:regionname:instance-name)/dbname +``` + TCP using default port (3306) on localhost: ``` user:password@tcp/dbname?charset=utf8mb4,utf8&sys_var=esc%40ped From b81e73ccadaaf2c4545a06da826f1ba95dbd2214 Mon Sep 17 00:00:00 2001 From: Xie Zhenye Date: Wed, 26 Oct 2016 16:54:14 +0800 Subject: [PATCH 58/65] Add maxAllowedPacket DSN Parameter Allows to set the driver-side max_packet_allowed value manually * add docs * add author, add test * fix AUTHOR file and README * change the param name from MaxPacketAllowed to MaxAllowedPacket * fix maxAllowedPacket param name in README * rename all maxPacketAllowed to maxAllowedPacket * fix doc format * fix doc --- AUTHORS | 1 + README.md | 9 +++++++++ benchmark_test.go | 2 +- connection.go | 4 ++-- connection_test.go | 6 +++--- driver.go | 22 +++++++++++++--------- dsn.go | 45 +++++++++++++++++++++++++++++++-------------- dsn_test.go | 4 ++-- packets.go | 8 ++++---- 9 files changed, 66 insertions(+), 35 deletions(-) diff --git a/AUTHORS b/AUTHORS index 37bf11a2b..466ac86ab 100644 --- a/AUTHORS +++ b/AUTHORS @@ -45,6 +45,7 @@ Stan Putrya Stanley Gunawan Xiaobing Jiang Xiuming Chen +Zhenye Xie # Organizations diff --git a/README.md b/README.md index 388632816..c35ba2e92 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,15 @@ Default: 0 I/O write timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. +##### `maxAllowedPacket` +``` +Type: decimal number +Default: 0 +``` + +Max packet size allowed in bytes. Use `maxAllowedPacket=0` to automatically fetch the `max_allowed_packet` variable from server. + + ##### System Variables All other parameters are interpreted as system variables: diff --git a/benchmark_test.go b/benchmark_test.go index 8f721139b..7da833a2a 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -220,7 +220,7 @@ func BenchmarkInterpolation(b *testing.B) { InterpolateParams: true, Loc: time.UTC, }, - maxPacketAllowed: maxPacketSize, + maxAllowedPacket: maxPacketSize, maxWriteSize: maxPacketSize - 1, buf: newBuffer(nil), } diff --git a/connection.go b/connection.go index d37e36dea..d82c728f3 100644 --- a/connection.go +++ b/connection.go @@ -22,7 +22,7 @@ type mysqlConn struct { affectedRows uint64 insertId uint64 cfg *Config - maxPacketAllowed int + maxAllowedPacket int maxWriteSize int writeTimeout time.Duration flags clientFlag @@ -246,7 +246,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin return "", driver.ErrSkip } - if len(buf)+4 > mc.maxPacketAllowed { + if len(buf)+4 > mc.maxAllowedPacket { return "", driver.ErrSkip } } diff --git a/connection_test.go b/connection_test.go index 7111e4a6b..65325f101 100644 --- a/connection_test.go +++ b/connection_test.go @@ -16,7 +16,7 @@ import ( func TestInterpolateParams(t *testing.T) { mc := &mysqlConn{ buf: newBuffer(nil), - maxPacketAllowed: maxPacketSize, + maxAllowedPacket: maxPacketSize, cfg: &Config{ InterpolateParams: true, }, @@ -36,7 +36,7 @@ func TestInterpolateParams(t *testing.T) { func TestInterpolateParamsTooManyPlaceholders(t *testing.T) { mc := &mysqlConn{ buf: newBuffer(nil), - maxPacketAllowed: maxPacketSize, + maxAllowedPacket: maxPacketSize, cfg: &Config{ InterpolateParams: true, }, @@ -53,7 +53,7 @@ func TestInterpolateParamsTooManyPlaceholders(t *testing.T) { func TestInterpolateParamsPlaceholderInString(t *testing.T) { mc := &mysqlConn{ buf: newBuffer(nil), - maxPacketAllowed: maxPacketSize, + maxAllowedPacket: maxPacketSize, cfg: &Config{ InterpolateParams: true, }, diff --git a/driver.go b/driver.go index 899f955fb..562ddeffb 100644 --- a/driver.go +++ b/driver.go @@ -50,7 +50,7 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) { // New mysqlConn mc := &mysqlConn{ - maxPacketAllowed: maxPacketSize, + maxAllowedPacket: maxPacketSize, maxWriteSize: maxPacketSize - 1, } mc.cfg, err = ParseDSN(dsn) @@ -109,15 +109,19 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) { return nil, err } - // Get max allowed packet size - maxap, err := mc.getSystemVar("max_allowed_packet") - if err != nil { - mc.Close() - return nil, err + if mc.cfg.MaxAllowedPacket > 0 { + mc.maxAllowedPacket = mc.cfg.MaxAllowedPacket + } else { + // Get max allowed packet size + maxap, err := mc.getSystemVar("max_allowed_packet") + if err != nil { + mc.Close() + return nil, err + } + mc.maxAllowedPacket = stringToInt(maxap) - 1 } - mc.maxPacketAllowed = stringToInt(maxap) - 1 - if mc.maxPacketAllowed < maxPacketSize { - mc.maxWriteSize = mc.maxPacketAllowed + if mc.maxAllowedPacket < maxPacketSize { + mc.maxWriteSize = mc.maxAllowedPacket } // Handle DSN Params diff --git a/dsn.go b/dsn.go index 73138bc57..896be9ef5 100644 --- a/dsn.go +++ b/dsn.go @@ -15,6 +15,7 @@ import ( "fmt" "net" "net/url" + "strconv" "strings" "time" ) @@ -28,19 +29,20 @@ var ( // Config is a configuration parsed from a DSN string type Config struct { - User string // Username - Passwd string // Password (requires User) - Net string // Network type - Addr string // Network address (requires Net) - DBName string // Database name - Params map[string]string // Connection parameters - Collation string // Connection collation - Loc *time.Location // Location for time.Time values - TLSConfig string // TLS configuration name - tls *tls.Config // TLS configuration - Timeout time.Duration // Dial timeout - ReadTimeout time.Duration // I/O read timeout - WriteTimeout time.Duration // I/O write timeout + User string // Username + Passwd string // Password (requires User) + Net string // Network type + Addr string // Network address (requires Net) + DBName string // Database name + Params map[string]string // Connection parameters + Collation string // Connection collation + Loc *time.Location // Location for time.Time values + MaxAllowedPacket int // Max packet size allowed + TLSConfig string // TLS configuration name + tls *tls.Config // TLS configuration + Timeout time.Duration // Dial timeout + ReadTimeout time.Duration // I/O read timeout + WriteTimeout time.Duration // I/O write timeout AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE AllowCleartextPasswords bool // Allows the cleartext client side plugin @@ -222,6 +224,17 @@ func (cfg *Config) FormatDSN() string { buf.WriteString(cfg.WriteTimeout.String()) } + if cfg.MaxAllowedPacket > 0 { + if hasParam { + buf.WriteString("&maxAllowedPacket=") + } else { + hasParam = true + buf.WriteString("?maxAllowedPacket=") + } + buf.WriteString(strconv.Itoa(cfg.MaxAllowedPacket)) + + } + // other params if cfg.Params != nil { for param, value := range cfg.Params { @@ -496,7 +509,11 @@ func parseDSNParams(cfg *Config, params string) (err error) { if err != nil { return } - + case "maxAllowedPacket": + cfg.MaxAllowedPacket, err = strconv.Atoi(value) + if err != nil { + return + } default: // lazy init if cfg.Params == nil { diff --git a/dsn_test.go b/dsn_test.go index e6f0f83b1..0693192ad 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -39,8 +39,8 @@ var testDSNs = []struct { "user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", Params: map[string]string{"charset": "utf8mb4,utf8"}, Collation: "utf8_general_ci", Loc: time.UTC, TLSConfig: "skip-verify"}, }, { - "user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci", - &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_unicode_ci", Loc: time.UTC, Timeout: 30 * time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, AllowAllFiles: true, AllowOldPasswords: true, ClientFoundRows: true}, + "user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci&maxAllowedPacket=16777216", + &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_unicode_ci", Loc: time.UTC, Timeout: 30 * time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, AllowAllFiles: true, AllowOldPasswords: true, ClientFoundRows: true, MaxAllowedPacket: 16777216}, }, { "user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", &Config{User: "user", Passwd: "p@ss(word)", Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:80", DBName: "dbname", Collation: "utf8_general_ci", Loc: time.Local}, diff --git a/packets.go b/packets.go index 602539942..f06752b02 100644 --- a/packets.go +++ b/packets.go @@ -80,7 +80,7 @@ func (mc *mysqlConn) readPacket() ([]byte, error) { func (mc *mysqlConn) writePacket(data []byte) error { pktLen := len(data) - 4 - if pktLen > mc.maxPacketAllowed { + if pktLen > mc.maxAllowedPacket { return ErrPktTooLarge } @@ -786,7 +786,7 @@ func (stmt *mysqlStmt) readPrepareResultPacket() (uint16, error) { // http://dev.mysql.com/doc/internals/en/com-stmt-send-long-data.html func (stmt *mysqlStmt) writeCommandLongData(paramID int, arg []byte) error { - maxLen := stmt.mc.maxPacketAllowed - 1 + maxLen := stmt.mc.maxAllowedPacket - 1 pktLen := maxLen // After the header (bytes 0-3) follows before the data: @@ -977,7 +977,7 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error { paramTypes[i+i] = fieldTypeString paramTypes[i+i+1] = 0x00 - if len(v) < mc.maxPacketAllowed-pos-len(paramValues)-(len(args)-(i+1))*64 { + if len(v) < mc.maxAllowedPacket-pos-len(paramValues)-(len(args)-(i+1))*64 { paramValues = appendLengthEncodedInteger(paramValues, uint64(len(v)), ) @@ -999,7 +999,7 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error { paramTypes[i+i] = fieldTypeString paramTypes[i+i+1] = 0x00 - if len(v) < mc.maxPacketAllowed-pos-len(paramValues)-(len(args)-(i+1))*64 { + if len(v) < mc.maxAllowedPacket-pos-len(paramValues)-(len(args)-(i+1))*64 { paramValues = appendLengthEncodedInteger(paramValues, uint64(len(v)), ) From 68ea39f3732247888d5141d11870315ffcd65cee Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Thu, 27 Oct 2016 05:32:36 +0800 Subject: [PATCH 59/65] README: Clarify strict mode (#500) The strict mode may not be confused with the server-side strict mode set via the sql_mode system variable. Fixes #376 * README: Set strict mode via sql_mode in DSN example --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c35ba2e92..639a8aa73 100644 --- a/README.md +++ b/README.md @@ -263,9 +263,11 @@ Valid Values: true, false Default: false ``` -`strict=true` enables the strict mode in which MySQL warnings are treated as errors. +`strict=true` enables a driver-side strict mode in which MySQL warnings are treated as errors. This mode should not be used in production as it may lead to data corruption in certain situations. -By default MySQL also treats notes as warnings. Use [`sql_notes=false`](http://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_sql_notes) to ignore notes. See the [examples](#examples) for an DSN example. +A server-side strict mode, which is safe for production use, can be set via the [`sql_mode`](https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html) system variable. + +By default MySQL also treats notes as warnings. Use [`sql_notes=false`](http://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_sql_notes) to ignore notes. ##### `timeout` @@ -331,9 +333,9 @@ root:pw@unix(/tmp/mysql.sock)/myDatabase?loc=Local user:password@tcp(localhost:5555)/dbname?tls=skip-verify&autocommit=true ``` -Use the [strict mode](#strict) but ignore notes: +Treat warnings as errors by setting the system variable [`sql_mode`](https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html): ``` -user:password@/dbname?strict=true&sql_notes=false +user:password@/dbname?sql_mode=TRADITIONAL ``` TCP via IPv6: From de9c517d47aa8e0796ec16de51cacfd4dd9cec7d Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Wed, 26 Oct 2016 16:59:34 +0800 Subject: [PATCH 60/65] README: Sort variables alphabetically Also make linebreaks consistent --- README.md | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 639a8aa73..a110cf1d3 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,14 @@ Note that this sets the location for time.Time values but does not change MySQL' Please keep in mind, that param values must be [url.QueryEscape](http://golang.org/pkg/net/url/#QueryEscape)'ed. Alternatively you can manually replace the `/` with `%2F`. For example `US/Pacific` would be `loc=US%2FPacific`. +##### `maxAllowedPacket` +``` +Type: decimal number +Default: 0 +``` + +Max packet size allowed in bytes. Use `maxAllowedPacket=0` to automatically fetch the `max_allowed_packet` variable from server. + ##### `multiStatements` ``` @@ -233,7 +241,6 @@ Allow multiple statements in one query. While this allows batch queries, it also When `multiStatements` is used, `?` parameters must only be used in the first statement. - ##### `parseTime` ``` @@ -254,7 +261,6 @@ Default: 0 I/O read timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. - ##### `strict` ``` @@ -269,7 +275,6 @@ A server-side strict mode, which is safe for production use, can be set via the By default MySQL also treats notes as warnings. Use [`sql_notes=false`](http://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_sql_notes) to ignore notes. - ##### `timeout` ``` @@ -279,7 +284,6 @@ Default: OS default *Driver* side connection timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. To set a server side timeout, use the parameter [`wait_timeout`](http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_wait_timeout). - ##### `tls` ``` @@ -290,7 +294,6 @@ Default: false `tls=true` enables TLS / SSL encrypted connection to the server. Use `skip-verify` if you want to use a self-signed or invalid certificate (server side). Use a custom value registered with [`mysql.RegisterTLSConfig`](http://godoc.org/github.com/go-sql-driver/mysql#RegisterTLSConfig). - ##### `writeTimeout` ``` @@ -301,15 +304,6 @@ Default: 0 I/O write timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. -##### `maxAllowedPacket` -``` -Type: decimal number -Default: 0 -``` - -Max packet size allowed in bytes. Use `maxAllowedPacket=0` to automatically fetch the `max_allowed_packet` variable from server. - - ##### System Variables All other parameters are interpreted as system variables: From 2a6c6079c7eff49a7e9d641e109d922f124a3e4c Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Thu, 27 Oct 2016 00:05:54 +0800 Subject: [PATCH 61/65] Update CHANGELOG --- CHANGELOG.md | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 381d91825..617ad80fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,16 +3,30 @@ Changes: - Go 1.1 is no longer supported - - Use decimals field from MySQL to format time types (#249) + - Use decimals fields in MySQL to format time types (#249) - Buffer optimizations (#269) - TLS ServerName defaults to the host (#283) + - Refactoring (#400, #410, #437) + - Adjusted documentation for second generation CloudSQL (#485) -Bugfixes: +New Features: - Enable microsecond resolution on TIME, DATETIME and TIMESTAMP (#249) + - Support for returning table alias on Columns() (#289, #359, #382) + - Placeholder interpolation, can be actived with the DSN parameter `interpolateParams=true` (#309, #318, #490) + - Support for uint64 parameters with high bit set (#332, #345) + - Cleartext authentication plugin support (#327) + - Exported ParseDSN function and the Config struct (#403, #419, #429) + - Read / Write timeouts (#401) + - Support for JSON field type (#414) + - Support for multi-statements and multi-results (#411, #431) + - DSN parameter to set the driver-side max_allowed_packet value manually (#489) + +Bugfixes: + - Fixed handling of queries without columns and rows (#255) - Fixed a panic when SetKeepAlive() failed (#298) - - Support receiving ERR packet while reading rows (#321) + - Handle ERR packets while reading rows (#321) - Fixed reading NULL length-encoded integers in MySQL 5.6+ (#349) - Fixed absolute paths support in LOAD LOCAL DATA INFILE (#356) - Actually zero out bytes in handshake response (#378) @@ -20,13 +34,10 @@ Bugfixes: - Fixed tests with MySQL 5.7.9+ (#380) - QueryUnescape TLS config names (#397) - Fixed "broken pipe" error by writing to closed socket (#390) - -New Features: - - Support for returning table alias on Columns() (#289, #359, #382) - - Placeholder interpolation, can be actived with the DSN parameter `interpolateParams=true` (#309, #318) - - Support for uint64 parameters with high bit set (#332, #345) - - Cleartext authentication plugin support (#327) - + - Fixed LOAD LOCAL DATA INFILE buffering (#424) + - Fixed parsing of floats into float64 when placeholders are used (#434) + - Fixed DSN tests with Go 1.7+ (#459) + - Handle ERR packets while waiting for EOF (#473) ## Version 1.2 (2014-06-03) From ce924a41eea897745442daaa1739089b0f3f561d Mon Sep 17 00:00:00 2001 From: twocode <08300720306@fudan.edu.cn> Date: Tue, 1 Nov 2016 19:13:14 +0800 Subject: [PATCH 62/65] Support authentication switch with mysql_native_password authentication (#494) * Support authentication switch with mysql_native_password authentication. Besides, fix bug that cipher needs to refreshed from authentication switch request packet from server. * revert connection.go which was modified by accident. * Address comments. * Keep DSN params sorted * DSN: Add code to format allowNativePasswords --- AUTHORS | 1 + README.md | 9 +++++++++ driver.go | 15 ++++++++++----- dsn.go | 18 ++++++++++++++++++ errors.go | 3 ++- infile.go | 3 ++- packets.go | 44 +++++++++++++++++++++++++++++++++++--------- 7 files changed, 77 insertions(+), 16 deletions(-) diff --git a/AUTHORS b/AUTHORS index 466ac86ab..148ea93aa 100644 --- a/AUTHORS +++ b/AUTHORS @@ -43,6 +43,7 @@ Runrioter Wung Soroush Pour Stan Putrya Stanley Gunawan +Xiangyu Hu Xiaobing Jiang Xiuming Chen Zhenye Xie diff --git a/README.md b/README.md index a110cf1d3..3dcc23544 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,15 @@ Default: false `allowCleartextPasswords=true` allows using the [cleartext client side plugin](http://dev.mysql.com/doc/en/cleartext-authentication-plugin.html) if required by an account, such as one defined with the [PAM authentication plugin](http://dev.mysql.com/doc/en/pam-authentication-plugin.html). Sending passwords in clear text may be a security problem in some configurations. To avoid problems if there is any possibility that the password would be intercepted, clients should connect to MySQL Server using a method that protects the password. Possibilities include [TLS / SSL](#tls), IPsec, or a private network. +##### `allowNativePasswords` + +``` +Type: bool +Valid Values: true, false +Default: false +``` +`allowNativePasswords=true` allows the usage of the mysql native password method. + ##### `allowOldPasswords` ``` diff --git a/driver.go b/driver.go index 562ddeffb..f5ee4728e 100644 --- a/driver.go +++ b/driver.go @@ -101,7 +101,7 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) { } // Handle response to auth packet, switch methods if possible - if err = handleAuthResult(mc, cipher); err != nil { + if err = handleAuthResult(mc); err != nil { // Authentication failed and MySQL has already closed the connection // (https://dev.mysql.com/doc/internals/en/authentication-fails.html). // Do not send COM_QUIT, just cleanup and return the error. @@ -134,9 +134,9 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) { return mc, nil } -func handleAuthResult(mc *mysqlConn, cipher []byte) error { +func handleAuthResult(mc *mysqlConn) error { // Read Result Packet - err := mc.readResultOK() + cipher, err := mc.readResultOK() if err == nil { return nil // auth successful } @@ -153,7 +153,7 @@ func handleAuthResult(mc *mysqlConn, cipher []byte) error { if err = mc.writeOldAuthPacket(cipher); err != nil { return err } - err = mc.readResultOK() + _, err = mc.readResultOK() } else if mc.cfg.AllowCleartextPasswords && err == ErrCleartextPassword { // Retry with clear text password for // http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html @@ -161,7 +161,12 @@ func handleAuthResult(mc *mysqlConn, cipher []byte) error { if err = mc.writeClearAuthPacket(); err != nil { return err } - err = mc.readResultOK() + _, err = mc.readResultOK() + } else if mc.cfg.AllowNativePasswords && err == ErrNativePassword { + if err = mc.writeNativeAuthPacket(cipher); err != nil { + return err + } + _, err = mc.readResultOK() } return err } diff --git a/dsn.go b/dsn.go index 896be9ef5..ac00dcedd 100644 --- a/dsn.go +++ b/dsn.go @@ -46,6 +46,7 @@ type Config struct { AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE AllowCleartextPasswords bool // Allows the cleartext client side plugin + AllowNativePasswords bool // Allows the native password authentication method AllowOldPasswords bool // Allows the old insecure password method ClientFoundRows bool // Return number of matching rows instead of rows changed ColumnsWithAlias bool // Prepend table alias to column names @@ -101,6 +102,15 @@ func (cfg *Config) FormatDSN() string { } } + if cfg.AllowNativePasswords { + if hasParam { + buf.WriteString("&allowNativePasswords=true") + } else { + hasParam = true + buf.WriteString("?allowNativePasswords=true") + } + } + if cfg.AllowOldPasswords { if hasParam { buf.WriteString("&allowOldPasswords=true") @@ -381,6 +391,14 @@ func parseDSNParams(cfg *Config, params string) (err error) { return errors.New("invalid bool value: " + value) } + // Use native password authentication + case "allowNativePasswords": + var isBool bool + cfg.AllowNativePasswords, isBool = readBool(value) + if !isBool { + return errors.New("invalid bool value: " + value) + } + // Use old authentication mode (pre MySQL 4.1) case "allowOldPasswords": var isBool bool diff --git a/errors.go b/errors.go index 1543a8054..857854e14 100644 --- a/errors.go +++ b/errors.go @@ -22,8 +22,9 @@ var ( ErrInvalidConn = errors.New("invalid connection") ErrMalformPkt = errors.New("malformed packet") ErrNoTLS = errors.New("TLS requested but server does not support TLS") - ErrOldPassword = errors.New("this user requires old password authentication. If you still want to use it, please add 'allowOldPasswords=1' to your DSN. See also https://github.com/go-sql-driver/mysql/wiki/old_passwords") ErrCleartextPassword = errors.New("this user requires clear text authentication. If you still want to use it, please add 'allowCleartextPasswords=1' to your DSN") + ErrNativePassword = errors.New("this user requires mysql native password authentication.") + ErrOldPassword = errors.New("this user requires old password authentication. If you still want to use it, please add 'allowOldPasswords=1' to your DSN. See also https://github.com/go-sql-driver/mysql/wiki/old_passwords") ErrUnknownPlugin = errors.New("this authentication plugin is not supported") ErrOldProtocol = errors.New("MySQL server does not support required protocol 41+") ErrPktSync = errors.New("commands out of sync. You can't run this command now") diff --git a/infile.go b/infile.go index 0f975bbc2..547357cfa 100644 --- a/infile.go +++ b/infile.go @@ -173,7 +173,8 @@ func (mc *mysqlConn) handleInFileRequest(name string) (err error) { // read OK packet if err == nil { - return mc.readResultOK() + _, err = mc.readResultOK() + return err } mc.readPacket() diff --git a/packets.go b/packets.go index f06752b02..9160beb09 100644 --- a/packets.go +++ b/packets.go @@ -372,6 +372,26 @@ func (mc *mysqlConn) writeClearAuthPacket() error { return mc.writePacket(data) } +// Native password authentication method +// http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchResponse +func (mc *mysqlConn) writeNativeAuthPacket(cipher []byte) error { + scrambleBuff := scramblePassword(cipher, []byte(mc.cfg.Passwd)) + + // Calculate the packet length and add a tailing 0 + pktLen := len(scrambleBuff) + data := mc.buf.takeSmallBuffer(4 + pktLen) + if data == nil { + // can not take the buffer. Something must be wrong with the connection + errLog.Print(ErrBusyBuffer) + return driver.ErrBadConn + } + + // Add the scramble + copy(data[4:], scrambleBuff) + + return mc.writePacket(data) +} + /****************************************************************************** * Command Packets * ******************************************************************************/ @@ -445,36 +465,42 @@ func (mc *mysqlConn) writeCommandPacketUint32(command byte, arg uint32) error { ******************************************************************************/ // Returns error if Packet is not an 'Result OK'-Packet -func (mc *mysqlConn) readResultOK() error { +func (mc *mysqlConn) readResultOK() ([]byte, error) { data, err := mc.readPacket() if err == nil { // packet indicator switch data[0] { case iOK: - return mc.handleOkPacket(data) + return nil, mc.handleOkPacket(data) case iEOF: if len(data) > 1 { - plugin := string(data[1:bytes.IndexByte(data, 0x00)]) + pluginEndIndex := bytes.IndexByte(data, 0x00) + plugin := string(data[1:pluginEndIndex]) + cipher := data[pluginEndIndex+1 : len(data)-1] + if plugin == "mysql_old_password" { // using old_passwords - return ErrOldPassword + return cipher, ErrOldPassword } else if plugin == "mysql_clear_password" { // using clear text password - return ErrCleartextPassword + return cipher, ErrCleartextPassword + } else if plugin == "mysql_native_password" { + // using mysql default authentication method + return cipher, ErrNativePassword } else { - return ErrUnknownPlugin + return cipher, ErrUnknownPlugin } } else { - return ErrOldPassword + return nil, ErrOldPassword } default: // Error otherwise - return mc.handleErrorPacket(data) + return nil, mc.handleErrorPacket(data) } } - return err + return nil, err } // Result Set Header Packet From 709234dc7e43f1bd62b0ea42613ad0b3bddd6ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Mart=C3=ADnez?= Date: Tue, 1 Nov 2016 11:24:08 -0200 Subject: [PATCH 63/65] add circle yml --- circle.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 circle.yml diff --git a/circle.yml b/circle.yml new file mode 100644 index 000000000..328933472 --- /dev/null +++ b/circle.yml @@ -0,0 +1,3 @@ +test: + override: + - go test -race From 8eb10539b7cb32cbbeb0624f1d7f19901b37348c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Mart=C3=ADnez?= Date: Tue, 1 Nov 2016 11:57:10 -0200 Subject: [PATCH 64/65] use our own schema --- circle.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/circle.yml b/circle.yml index 328933472..41a159bbe 100644 --- a/circle.yml +++ b/circle.yml @@ -1,3 +1,16 @@ +machine: + environment: + GOPATH: /home/ubuntu/.go_workspace:/home/ubuntu/.go_project + +dependencies: + pre: + - mkdir -p ~/.go_project/src/github.com/VividCortex + - ln -s ~/$CIRCLE_PROJECT_REPONAME ~/.go_project/src/github.com/VividCortex + override: + - curl -L -s https://raw.github.com/VividCortex/johnny-deps/master/bin/jd > ~/bin/jd + - perl ~/bin/jd -v -v build -s github.com/VividCortex/$CIRCLE_PROJECT_REPONAME + - ~/.go_workspace/src/github.com/VividCortex/schema/run.sh + test: override: - go test -race From c9a68f7de952ba18240964e2bcb36d836d3a1c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Mart=C3=ADnez?= Date: Tue, 1 Nov 2016 12:16:50 -0200 Subject: [PATCH 65/65] no need, just use the regular one --- circle.yml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/circle.yml b/circle.yml index 41a159bbe..5c541cca0 100644 --- a/circle.yml +++ b/circle.yml @@ -1,16 +1,3 @@ -machine: - environment: - GOPATH: /home/ubuntu/.go_workspace:/home/ubuntu/.go_project - -dependencies: - pre: - - mkdir -p ~/.go_project/src/github.com/VividCortex - - ln -s ~/$CIRCLE_PROJECT_REPONAME ~/.go_project/src/github.com/VividCortex - override: - - curl -L -s https://raw.github.com/VividCortex/johnny-deps/master/bin/jd > ~/bin/jd - - perl ~/bin/jd -v -v build -s github.com/VividCortex/$CIRCLE_PROJECT_REPONAME - - ~/.go_workspace/src/github.com/VividCortex/schema/run.sh - test: override: - - go test -race + - MYSQL_TEST_DBNAME=circle_test go test