// +------------------------------------------------------------------------- // | Copyright (C) 2016 Yunify, Inc. // +------------------------------------------------------------------------- // | Licensed under the Apache License, Version 2.0 (the "License"); // | you may not use this work except in compliance with the License. // | You may obtain a copy of the License in the LICENSE file, or at: // | // | http://www.apache.org/licenses/LICENSE-2.0 // | // | Unless required by applicable law or agreed to in writing, software // | distributed under the License is distributed on an "AS IS" BASIS, // | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // | See the License for the specific language governing permissions and // | limitations under the License. // +------------------------------------------------------------------------- package config import ( "errors" "io/ioutil" "net/http" "os" "strings" "time" "gopkg.in/yaml.v2" "github.com/yunify/qingstor-sdk-go/v3/logger" "github.com/yunify/qingstor-sdk-go/v3/utils" ) // A Config stores a configuration of this sdk. type Config struct { AccessKeyID string `yaml:"access_key_id"` SecretAccessKey string `yaml:"secret_access_key"` Host string `yaml:"host"` Port int `yaml:"port"` Protocol string `yaml:"protocol"` ConnectionRetries int `yaml:"connection_retries"` AdditionalUserAgent string `yaml:"additional_user_agent"` LogLevel string `yaml:"log_level"` HTTPSettings HTTPClientSettings Connection *http.Client } // HTTPClientSettings is the http client settings. type HTTPClientSettings struct { // ConnectTimeout affects making new socket connection ConnectTimeout time.Duration `yaml:"connect_timeout"` // ReadTimeout affect each call to HTTPResponse.Body.Read() ReadTimeout time.Duration `yaml:"read_timeout"` // WriteTimeout affect each write in io.Copy while sending HTTPRequest WriteTimeout time.Duration `yaml:"write_timeout" ` // TLSHandshakeTimeout affects https hand shake timeout TLSHandshakeTimeout time.Duration `yaml:"tls_timeout"` // IdleConnTimeout affects the time limit to re-use http connections IdleConnTimeout time.Duration `yaml:"idle_timeout"` TCPKeepAlive time.Duration `yaml:"tcp_keepalive_time"` DualStack bool `yaml:"dual_stack"` // MaxIdleConns affects the idle connections kept for re-use MaxIdleConns int `yaml:"max_idle_conns"` MaxIdleConnsPerHost int `yaml:"max_idle_conns_per_host"` ExpectContinueTimeout time.Duration `yaml:"expect_continue_timeout"` } // DefaultHTTPClientSettings is the default http client settings. var DefaultHTTPClientSettings = HTTPClientSettings{ ConnectTimeout: time.Second * 30, ReadTimeout: time.Second * 30, WriteTimeout: time.Second * 30, TLSHandshakeTimeout: time.Second * 10, IdleConnTimeout: time.Second * 20, TCPKeepAlive: 0, DualStack: false, MaxIdleConns: 100, MaxIdleConnsPerHost: 10, ExpectContinueTimeout: time.Second * 2, } // New create a Config with given AccessKeyID and SecretAccessKey. func New(accessKeyID, secretAccessKey string) (c *Config, err error) { c, err = NewDefault() if err != nil { c = nil return } c.AccessKeyID = accessKeyID c.SecretAccessKey = secretAccessKey return } // NewDefault create a Config with default configuration. func NewDefault() (c *Config, err error) { c = &Config{} err = c.LoadDefaultConfig() if err != nil { c = nil return } return } // Check checks the configuration. func (c *Config) Check() (err error) { if c.AccessKeyID == "" { err = errors.New("access key ID not specified") return } if c.SecretAccessKey == "" { err = errors.New("secret access key not specified") return } if c.Host == "" { err = errors.New("server host not specified") return } if c.Port <= 0 { err = errors.New("server port not specified") return } if c.Protocol == "" { err = errors.New("server protocol not specified") return } if c.AdditionalUserAgent != "" { for _, x := range c.AdditionalUserAgent { // Allow space(32) to ~(126) in ASCII Table, exclude "(34). if int(x) < 32 || int(x) > 126 || int(x) == 32 || int(x) == 34 { err = errors.New("additional User-Agent contains characters that not allowed") return } } } err = logger.CheckLevel(c.LogLevel) if err != nil { return } return } // LoadDefaultConfig loads the default configuration for Config. // It returns error if yaml decode failed. func (c *Config) LoadDefaultConfig() (err error) { c.HTTPSettings = DefaultHTTPClientSettings err = yaml.Unmarshal([]byte(DefaultConfigFileContent), c) if err != nil { logger.Errorf(nil, "Config parse error, %v.", err) return } logger.SetLevel(c.LogLevel) c.InitHTTPClient() return } // LoadUserConfig loads user configuration in ~/.qingstor/config.yaml for Config. // It returns error if file not found. func (c *Config) LoadUserConfig() (err error) { _, err = os.Stat(GetUserConfigFilePath()) if err != nil { logger.Warnf(nil, "Installing default config file to %s.", GetUserConfigFilePath()) InstallDefaultUserConfig() } return c.LoadConfigFromFilePath(GetUserConfigFilePath()) } // LoadConfigFromFilePath loads configuration from a specified local path. // It returns error if file not found or yaml decode failed. func (c *Config) LoadConfigFromFilePath(filePath string) (err error) { if strings.Index(filePath, "~/") == 0 { filePath = strings.Replace(filePath, "~/", getHome()+"/", 1) } yamlString, err := ioutil.ReadFile(filePath) if err != nil { logger.Errorf(nil, "File not found: %s.", filePath) return err } return c.LoadConfigFromContent(yamlString) } // LoadConfigFromContent loads configuration from a given byte slice. // It returns error if yaml decode failed. func (c *Config) LoadConfigFromContent(content []byte) (err error) { c.LoadDefaultConfig() err = yaml.Unmarshal(content, c) if err != nil { logger.Errorf(nil, "Config parse error, %v.", err) return } err = c.Check() if err != nil { return } logger.SetLevel(c.LogLevel) c.InitHTTPClient() return } // InitHTTPClient : After modifying Config.HTTPSettings, you should always call this to initialize the HTTP Client. func (c *Config) InitHTTPClient() { var emptySettings HTTPClientSettings if c.HTTPSettings == emptySettings { // User forgot to initialize the settings c.HTTPSettings = DefaultHTTPClientSettings } else { if c.HTTPSettings.ConnectTimeout == 0 { c.HTTPSettings.ConnectTimeout = DefaultHTTPClientSettings.ConnectTimeout } // If ReadTimeout and WriteTimeout is zero, means no read/write timeout if c.HTTPSettings.TLSHandshakeTimeout == 0 { c.HTTPSettings.TLSHandshakeTimeout = DefaultHTTPClientSettings.TLSHandshakeTimeout } if c.HTTPSettings.ExpectContinueTimeout == 0 { c.HTTPSettings.ExpectContinueTimeout = DefaultHTTPClientSettings.ExpectContinueTimeout } } dialer := utils.NewDialer( c.HTTPSettings.ConnectTimeout, c.HTTPSettings.ReadTimeout, c.HTTPSettings.WriteTimeout, ) dialer.KeepAlive = c.HTTPSettings.TCPKeepAlive // XXX: DualStack enables RFC 6555-compliant "Happy Eyeballs" dialing // when the network is "tcp" and the destination is a host name // with both IPv4 and IPv6 addresses. This allows a client to // tolerate networks where one address family is silently broken dialer.DualStack = c.HTTPSettings.DualStack c.Connection = &http.Client{ // We do not use the timeout in http client, // because this timeout is for the whole http body read/write, // it's unsuitable for various length of files and network condition. // We provide a wraper in utils/conn.go of net.Dialer to make io timeout to the http connection // for individual buffer I/O operation, Timeout: 0, Transport: &http.Transport{ DialContext: dialer.DialContext, MaxIdleConns: c.HTTPSettings.MaxIdleConns, MaxIdleConnsPerHost: c.HTTPSettings.MaxIdleConnsPerHost, IdleConnTimeout: c.HTTPSettings.IdleConnTimeout, TLSHandshakeTimeout: c.HTTPSettings.TLSHandshakeTimeout, ExpectContinueTimeout: c.HTTPSettings.ExpectContinueTimeout, }, } }