// Copyright (C) 2019-2022 Nicola Murino // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, version 3. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // Package telemetry provides telemetry information for SFTPGo, such as: // - health information (for health checks) // - metrics // - profiling information package telemetry import ( "crypto/tls" "log" "net/http" "path/filepath" "runtime" "time" "github.com/go-chi/chi/v5" "github.com/drakkan/sftpgo/v2/internal/common" "github.com/drakkan/sftpgo/v2/internal/logger" "github.com/drakkan/sftpgo/v2/internal/util" ) const ( logSender = "telemetry" metricsPath = "/metrics" pprofBasePath = "/debug" ) var ( router *chi.Mux httpAuth common.HTTPAuthProvider certMgr *common.CertManager ) // Conf telemetry server configuration. type Conf struct { // The port used for serving HTTP requests. 0 disable the HTTP server. Default: 0 BindPort int `json:"bind_port" mapstructure:"bind_port"` // The address to listen on. A blank value means listen on all available network interfaces. Default: "127.0.0.1" BindAddress string `json:"bind_address" mapstructure:"bind_address"` // Enable the built-in profiler. // The profiler will be accessible via HTTP/HTTPS using the base URL "/debug/pprof/" EnableProfiler bool `json:"enable_profiler" mapstructure:"enable_profiler"` // Path to a file used to store usernames and password for basic authentication. // This can be an absolute path or a path relative to the config dir. // We support HTTP basic authentication and the file format must conform to the one generated using the Apache // htpasswd tool. The supported password formats are bcrypt ($2y$ prefix) and md5 crypt ($apr1$ prefix). // If empty HTTP authentication is disabled AuthUserFile string `json:"auth_user_file" mapstructure:"auth_user_file"` // If files containing a certificate and matching private key for the server are provided the server will expect // HTTPS connections. // Certificate and key files can be reloaded on demand sending a "SIGHUP" signal on Unix based systems and a // "paramchange" request to the running service on Windows. CertificateFile string `json:"certificate_file" mapstructure:"certificate_file"` CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"` // TLSCipherSuites is a list of supported cipher suites for TLS version 1.2. // If CipherSuites is nil/empty, a default list of secure cipher suites // is used, with a preference order based on hardware performance. // Note that TLS 1.3 ciphersuites are not configurable. // The supported ciphersuites names are defined here: // // https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52 // // any invalid name will be silently ignored. // The order matters, the ciphers listed first will be the preferred ones. TLSCipherSuites []string `json:"tls_cipher_suites" mapstructure:"tls_cipher_suites"` // Defines the minimum TLS version. 13 means TLS 1.3, default is TLS 1.2 MinTLSVersion int `json:"min_tls_version" mapstructure:"min_tls_version"` } // ShouldBind returns true if there service must be started func (c Conf) ShouldBind() bool { if c.BindPort > 0 { return true } if filepath.IsAbs(c.BindAddress) && runtime.GOOS != "windows" { return true } return false } // Initialize configures and starts the telemetry server. func (c Conf) Initialize(configDir string) error { var err error logger.Info(logSender, "", "initializing telemetry server with config %+v", c) authUserFile := getConfigPath(c.AuthUserFile, configDir) httpAuth, err = common.NewBasicAuthProvider(authUserFile) if err != nil { return err } certificateFile := getConfigPath(c.CertificateFile, configDir) certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir) initializeRouter(c.EnableProfiler) httpServer := &http.Server{ Handler: router, ReadHeaderTimeout: 30 * time.Second, ReadTimeout: 60 * time.Second, WriteTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 14, // 16KB ErrorLog: log.New(&logger.StdLoggerWrapper{Sender: logSender}, "", 0), } if certificateFile != "" && certificateKeyFile != "" { keyPairs := []common.TLSKeyPair{ { Cert: certificateFile, Key: certificateKeyFile, ID: common.DefaultTLSKeyPaidID, }, } certMgr, err = common.NewCertManager(keyPairs, configDir, logSender) if err != nil { return err } config := &tls.Config{ GetCertificate: certMgr.GetCertificateFunc(common.DefaultTLSKeyPaidID), MinVersion: util.GetTLSVersion(c.MinTLSVersion), NextProtos: []string{"http/1.1", "h2"}, CipherSuites: util.GetTLSCiphersFromNames(c.TLSCipherSuites), PreferServerCipherSuites: true, } logger.Debug(logSender, "", "configured TLS cipher suites: %v", config.CipherSuites) httpServer.TLSConfig = config return util.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, true, logSender) } return util.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, false, logSender) } // ReloadCertificateMgr reloads the certificate manager func ReloadCertificateMgr() error { if certMgr != nil { return certMgr.Reload() } return nil } func getConfigPath(name, configDir string) string { if !util.IsFileInputValid(name) { return "" } if name != "" && !filepath.IsAbs(name) { return filepath.Join(configDir, name) } return name }