diff --git a/weed/command/scaffold/filer.toml b/weed/command/scaffold/filer.toml index d65e97207..073acba83 100644 --- a/weed/command/scaffold/filer.toml +++ b/weed/command/scaffold/filer.toml @@ -58,9 +58,11 @@ enabled = false # [username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN] dsn = "root@tcp(localhost:3306)/seaweedfs?collation=utf8mb4_bin" enable_tls = false -ca_crt = "" # ca.crt dir when enable_tls set true -client_crt = "" # mysql client.crt dir when enable_tls set true -client_key = "" # mysql client.key dir when enable_tls set true +ca_crt = "" # path to CA cert (PEM) — optional; if empty, the system trust store is used +client_crt = "" # path to client cert (PEM) — only when server requires mTLS; must be set together with client_key +client_key = "" # path to client key (PEM) — only when server requires mTLS; must be set together with client_crt +tls_insecure_skip_verify = false # skip server cert verification (use only for testing or with self-signed certs) +tls_server_name = "" # override SNI / cert hostname; leave empty to use `hostname` above hostname = "localhost" port = 3306 username = "root" @@ -72,6 +74,11 @@ connection_max_lifetime_seconds = 300 interpolateParams = false # if insert/upsert failing, you can disable upsert or update query syntax to match your RDBMS syntax: enableUpsert = true +# Default uses the row-alias form (`AS new`) added in MySQL 8.0.19 and is the +# preferred syntax there. For MariaDB (any version) and MySQL 5.7, override +# with the form below — MariaDB does not support row aliases in +# INSERT ... ON DUPLICATE KEY UPDATE: +# upsertQuery = """INSERT INTO `%s` (`dirhash`,`name`,`directory`,`meta`) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE `meta` = VALUES(`meta`)""" upsertQuery = """INSERT INTO `%s` (`dirhash`,`name`,`directory`,`meta`) VALUES (?,?,?,?) AS `new` ON DUPLICATE KEY UPDATE `meta` = `new`.`meta`""" [mysql2] # or memsql, tidb diff --git a/weed/filer/mysql/mysql_store.go b/weed/filer/mysql/mysql_store.go index a4f8a3ce2..cb96dea1d 100644 --- a/weed/filer/mysql/mysql_store.go +++ b/weed/filer/mysql/mysql_store.go @@ -11,15 +11,13 @@ import ( "time" "github.com/go-sql-driver/mysql" - _ "github.com/go-sql-driver/mysql" "github.com/seaweedfs/seaweedfs/weed/filer" "github.com/seaweedfs/seaweedfs/weed/filer/abstract_sql" "github.com/seaweedfs/seaweedfs/weed/util" ) const ( - CONNECTION_URL_PATTERN = "%s:%s@tcp(%s:%d)/%s?collation=utf8mb4_bin" - CONNECTION_TLS_URL_PATTERN = "%s:%s@tcp(%s:%d)/%s?collation=utf8mb4_bin&tls=mysql-tls" + CONNECTION_URL_PATTERN = "%s:%s@tcp(%s:%d)/%s?collation=utf8mb4_bin" ) func init() { @@ -52,11 +50,14 @@ func (store *MysqlStore) Initialize(configuration util.Configuration, prefix str configuration.GetString(prefix+"ca_crt"), configuration.GetString(prefix+"client_crt"), configuration.GetString(prefix+"client_key"), + configuration.GetBool(prefix+"tls_insecure_skip_verify"), + configuration.GetString(prefix+"tls_server_name"), ) } func (store *MysqlStore) initialize(dsn string, upsertQuery string, enableUpsert bool, user, password, hostname string, port int, database string, maxIdle, maxOpen, - maxLifetimeSeconds int, interpolateParams bool, enableTls bool, caCrtDir string, clientCrtDir string, clientKeyDir string) (err error) { + maxLifetimeSeconds int, interpolateParams bool, enableTls bool, caCrtDir string, clientCrtDir string, clientKeyDir string, + tlsInsecureSkipVerify bool, tlsServerName string) (err error) { store.SupportBucketTable = false if !enableUpsert { @@ -81,38 +82,8 @@ func (store *MysqlStore) initialize(dsn string, upsertQuery string, enableUpsert return false } - if enableTls { - rootCertPool := x509.NewCertPool() - pem, err := os.ReadFile(caCrtDir) - if err != nil { - return err - } - if ok := rootCertPool.AppendCertsFromPEM(pem); !ok { - return fmt.Errorf("failed to append root certificate") - } - - clientCert := make([]tls.Certificate, 0) - if cert, err := tls.LoadX509KeyPair(clientCrtDir, clientKeyDir); err == nil { - clientCert = append(clientCert, cert) - } - - tlsConfig := &tls.Config{ - RootCAs: rootCertPool, - Certificates: clientCert, - MinVersion: tls.VersionTLS12, - } - err = mysql.RegisterTLSConfig("mysql-tls", tlsConfig) - if err != nil { - return err - } - } - if dsn == "" { - pattern := CONNECTION_URL_PATTERN - if enableTls { - pattern = CONNECTION_TLS_URL_PATTERN - } - dsn = fmt.Sprintf(pattern, user, password, hostname, port, database) + dsn = fmt.Sprintf(CONNECTION_URL_PATTERN, user, password, hostname, port, database) if interpolateParams { dsn += "&interpolateParams=true" } @@ -122,21 +93,67 @@ func (store *MysqlStore) initialize(dsn string, upsertQuery string, enableUpsert return fmt.Errorf("can not parse DSN error:%w", err) } - var dbErr error - store.DB, dbErr = sql.Open("mysql", dsn) - if dbErr != nil { - store.DB.Close() - store.DB = nil - return fmt.Errorf("can not connect to %s error:%v", strings.ReplaceAll(dsn, cfg.Passwd, ""), err) + if enableTls { + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: tlsInsecureSkipVerify, + ServerName: tlsServerName, + } + + // When ca_crt is empty, leave RootCAs nil so Go falls back to the + // system trust store. This is the common case for managed databases + // (RDS, Aiven, ...) whose certs chain to a public CA already on the host. + if caCrtDir != "" { + rootCertPool := x509.NewCertPool() + pem, err := os.ReadFile(caCrtDir) + if err != nil { + return fmt.Errorf("read ca_crt %s: %w", caCrtDir, err) + } + if ok := rootCertPool.AppendCertsFromPEM(pem); !ok { + return fmt.Errorf("failed to append root certificate from %s", caCrtDir) + } + tlsConfig.RootCAs = rootCertPool + } + + // Only attempt to load a client keypair when at least one of the paths is + // set. If either is set, both must load successfully — silently skipping + // a typo'd path used to mask broken mTLS setups as confusing handshake + // failures. + if clientCrtDir != "" || clientKeyDir != "" { + cert, err := tls.LoadX509KeyPair(clientCrtDir, clientKeyDir) + if err != nil { + return fmt.Errorf("load mysql client keypair (crt=%s key=%s): %w", clientCrtDir, clientKeyDir, err) + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + + // Set TLS directly on the parsed Config rather than registering a global + // "mysql-tls" entry — the global registry is process-wide and would be + // overwritten if a second MysqlStore is initialized with different TLS + // settings. + cfg.TLS = tlsConfig } + connector, err := mysql.NewConnector(cfg) + if err != nil { + return fmt.Errorf("can not create mysql connector for %s error:%w", maskedDSN(cfg), err) + } + + store.DB = sql.OpenDB(connector) store.DB.SetMaxIdleConns(maxIdle) store.DB.SetMaxOpenConns(maxOpen) store.DB.SetConnMaxLifetime(time.Duration(maxLifetimeSeconds) * time.Second) if err = store.DB.Ping(); err != nil { - return fmt.Errorf("connect to %s error:%v", strings.ReplaceAll(dsn, cfg.Passwd, ""), err) + return fmt.Errorf("connect to %s error:%v", maskedDSN(cfg), err) } return nil } + +func maskedDSN(cfg *mysql.Config) string { + if cfg.Passwd == "" { + return cfg.FormatDSN() + } + return strings.ReplaceAll(cfg.FormatDSN(), cfg.Passwd, "") +}