// Package httpd implements REST API and Web interface for SFTPGo.
// The OpenAPI 3 schema for the exposed API can be found inside the source tree:
// https://github.com/drakkan/sftpgo/blob/main/openapi/openapi.yaml
package httpd

import (
	"crypto/sha256"
	"fmt"
	"net"
	"net/http"
	"path"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/jwtauth/v5"
	"github.com/lestrrat-go/jwx/jwa"

	"github.com/drakkan/sftpgo/v2/common"
	"github.com/drakkan/sftpgo/v2/dataprovider"
	"github.com/drakkan/sftpgo/v2/ftpd"
	"github.com/drakkan/sftpgo/v2/logger"
	"github.com/drakkan/sftpgo/v2/mfa"
	"github.com/drakkan/sftpgo/v2/sftpd"
	"github.com/drakkan/sftpgo/v2/util"
	"github.com/drakkan/sftpgo/v2/webdavd"
)

const (
	logSender                             = "httpd"
	tokenPath                             = "/api/v2/token"
	logoutPath                            = "/api/v2/logout"
	userTokenPath                         = "/api/v2/user/token"
	userLogoutPath                        = "/api/v2/user/logout"
	activeConnectionsPath                 = "/api/v2/connections"
	quotasBasePath                        = "/api/v2/quotas"
	quotaScanPath                         = "/api/v2/quota-scans"
	quotaScanVFolderPath                  = "/api/v2/folder-quota-scans"
	userPath                              = "/api/v2/users"
	versionPath                           = "/api/v2/version"
	folderPath                            = "/api/v2/folders"
	serverStatusPath                      = "/api/v2/status"
	dumpDataPath                          = "/api/v2/dumpdata"
	loadDataPath                          = "/api/v2/loaddata"
	updateUsedQuotaPath                   = "/api/v2/quota-update"
	updateFolderUsedQuotaPath             = "/api/v2/folder-quota-update"
	defenderHosts                         = "/api/v2/defender/hosts"
	defenderBanTime                       = "/api/v2/defender/bantime"
	defenderUnban                         = "/api/v2/defender/unban"
	defenderScore                         = "/api/v2/defender/score"
	adminPath                             = "/api/v2/admins"
	adminPwdPath                          = "/api/v2/admin/changepwd"
	adminPwdCompatPath                    = "/api/v2/changepwd/admin"
	adminProfilePath                      = "/api/v2/admin/profile"
	userPwdPath                           = "/api/v2/user/changepwd"
	userPublicKeysPath                    = "/api/v2/user/publickeys"
	userFolderPath                        = "/api/v2/user/folder"
	userDirsPath                          = "/api/v2/user/dirs"
	userFilePath                          = "/api/v2/user/file"
	userFilesPath                         = "/api/v2/user/files"
	userStreamZipPath                     = "/api/v2/user/streamzip"
	userUploadFilePath                    = "/api/v2/user/files/upload"
	userFilesDirsMetadataPath             = "/api/v2/user/files/metadata"
	apiKeysPath                           = "/api/v2/apikeys"
	adminTOTPConfigsPath                  = "/api/v2/admin/totp/configs"
	adminTOTPGeneratePath                 = "/api/v2/admin/totp/generate"
	adminTOTPValidatePath                 = "/api/v2/admin/totp/validate"
	adminTOTPSavePath                     = "/api/v2/admin/totp/save"
	admin2FARecoveryCodesPath             = "/api/v2/admin/2fa/recoverycodes"
	userTOTPConfigsPath                   = "/api/v2/user/totp/configs"
	userTOTPGeneratePath                  = "/api/v2/user/totp/generate"
	userTOTPValidatePath                  = "/api/v2/user/totp/validate"
	userTOTPSavePath                      = "/api/v2/user/totp/save"
	user2FARecoveryCodesPath              = "/api/v2/user/2fa/recoverycodes"
	userProfilePath                       = "/api/v2/user/profile"
	userSharesPath                        = "/api/v2/user/shares"
	retentionBasePath                     = "/api/v2/retention/users"
	retentionChecksPath                   = "/api/v2/retention/users/checks"
	metadataBasePath                      = "/api/v2/metadata/users"
	metadataChecksPath                    = "/api/v2/metadata/users/checks"
	fsEventsPath                          = "/api/v2/events/fs"
	providerEventsPath                    = "/api/v2/events/provider"
	sharesPath                            = "/api/v2/shares"
	healthzPath                           = "/healthz"
	webRootPathDefault                    = "/"
	webBasePathDefault                    = "/web"
	webBasePathAdminDefault               = "/web/admin"
	webBasePathClientDefault              = "/web/client"
	webAdminSetupPathDefault              = "/web/admin/setup"
	webLoginPathDefault                   = "/web/admin/login"
	webAdminTwoFactorPathDefault          = "/web/admin/twofactor"
	webAdminTwoFactorRecoveryPathDefault  = "/web/admin/twofactor-recovery"
	webLogoutPathDefault                  = "/web/admin/logout"
	webUsersPathDefault                   = "/web/admin/users"
	webUserPathDefault                    = "/web/admin/user"
	webConnectionsPathDefault             = "/web/admin/connections"
	webFoldersPathDefault                 = "/web/admin/folders"
	webFolderPathDefault                  = "/web/admin/folder"
	webStatusPathDefault                  = "/web/admin/status"
	webAdminsPathDefault                  = "/web/admin/managers"
	webAdminPathDefault                   = "/web/admin/manager"
	webMaintenancePathDefault             = "/web/admin/maintenance"
	webBackupPathDefault                  = "/web/admin/backup"
	webRestorePathDefault                 = "/web/admin/restore"
	webScanVFolderPathDefault             = "/web/admin/quotas/scanfolder"
	webQuotaScanPathDefault               = "/web/admin/quotas/scanuser"
	webChangeAdminPwdPathDefault          = "/web/admin/changepwd"
	webAdminForgotPwdPathDefault          = "/web/admin/forgot-password"
	webAdminResetPwdPathDefault           = "/web/admin/reset-password"
	webAdminProfilePathDefault            = "/web/admin/profile"
	webAdminMFAPathDefault                = "/web/admin/mfa"
	webAdminTOTPGeneratePathDefault       = "/web/admin/totp/generate"
	webAdminTOTPValidatePathDefault       = "/web/admin/totp/validate"
	webAdminTOTPSavePathDefault           = "/web/admin/totp/save"
	webAdminRecoveryCodesPathDefault      = "/web/admin/recoverycodes"
	webTemplateUserDefault                = "/web/admin/template/user"
	webTemplateFolderDefault              = "/web/admin/template/folder"
	webDefenderPathDefault                = "/web/admin/defender"
	webDefenderHostsPathDefault           = "/web/admin/defender/hosts"
	webClientLoginPathDefault             = "/web/client/login"
	webClientTwoFactorPathDefault         = "/web/client/twofactor"
	webClientTwoFactorRecoveryPathDefault = "/web/client/twofactor-recovery"
	webClientFilesPathDefault             = "/web/client/files"
	webClientFilePathDefault              = "/web/client/file"
	webClientSharesPathDefault            = "/web/client/shares"
	webClientSharePathDefault             = "/web/client/share"
	webClientEditFilePathDefault          = "/web/client/editfile"
	webClientDirsPathDefault              = "/web/client/dirs"
	webClientDownloadZipPathDefault       = "/web/client/downloadzip"
	webClientProfilePathDefault           = "/web/client/profile"
	webClientMFAPathDefault               = "/web/client/mfa"
	webClientTOTPGeneratePathDefault      = "/web/client/totp/generate"
	webClientTOTPValidatePathDefault      = "/web/client/totp/validate"
	webClientTOTPSavePathDefault          = "/web/client/totp/save"
	webClientRecoveryCodesPathDefault     = "/web/client/recoverycodes"
	webChangeClientPwdPathDefault         = "/web/client/changepwd"
	webClientLogoutPathDefault            = "/web/client/logout"
	webClientPubSharesPathDefault         = "/web/client/pubshares"
	webClientForgotPwdPathDefault         = "/web/client/forgot-password"
	webClientResetPwdPathDefault          = "/web/client/reset-password"
	webClientViewPDFPathDefault           = "/web/client/viewpdf"
	webStaticFilesPathDefault             = "/static"
	webOpenAPIPathDefault                 = "/openapi"
	// MaxRestoreSize defines the max size for the loaddata input file
	MaxRestoreSize       = 10485760 // 10 MB
	maxRequestSize       = 1048576  // 1MB
	maxLoginBodySize     = 262144   // 256 KB
	httpdMaxEditFileSize = 1048576  // 1 MB
	maxMultipartMem      = 10485760 // 10 MB
	osWindows            = "windows"
	otpHeaderCode        = "X-SFTPGO-OTP"
	mTimeHeader          = "X-SFTPGO-MTIME"
)

var (
	backupsPath                    string
	certMgr                        *common.CertManager
	cleanupTicker                  *time.Ticker
	cleanupDone                    chan bool
	invalidatedJWTTokens           sync.Map
	csrfTokenAuth                  *jwtauth.JWTAuth
	webRootPath                    string
	webBasePath                    string
	webBaseAdminPath               string
	webBaseClientPath              string
	webAdminSetupPath              string
	webLoginPath                   string
	webAdminTwoFactorPath          string
	webAdminTwoFactorRecoveryPath  string
	webLogoutPath                  string
	webUsersPath                   string
	webUserPath                    string
	webConnectionsPath             string
	webFoldersPath                 string
	webFolderPath                  string
	webStatusPath                  string
	webAdminsPath                  string
	webAdminPath                   string
	webMaintenancePath             string
	webBackupPath                  string
	webRestorePath                 string
	webScanVFolderPath             string
	webQuotaScanPath               string
	webAdminProfilePath            string
	webAdminMFAPath                string
	webAdminTOTPGeneratePath       string
	webAdminTOTPValidatePath       string
	webAdminTOTPSavePath           string
	webAdminRecoveryCodesPath      string
	webChangeAdminPwdPath          string
	webAdminForgotPwdPath          string
	webAdminResetPwdPath           string
	webTemplateUser                string
	webTemplateFolder              string
	webDefenderPath                string
	webDefenderHostsPath           string
	webClientLoginPath             string
	webClientTwoFactorPath         string
	webClientTwoFactorRecoveryPath string
	webClientFilesPath             string
	webClientFilePath              string
	webClientSharesPath            string
	webClientSharePath             string
	webClientEditFilePath          string
	webClientDirsPath              string
	webClientDownloadZipPath       string
	webClientProfilePath           string
	webChangeClientPwdPath         string
	webClientMFAPath               string
	webClientTOTPGeneratePath      string
	webClientTOTPValidatePath      string
	webClientTOTPSavePath          string
	webClientRecoveryCodesPath     string
	webClientPubSharesPath         string
	webClientLogoutPath            string
	webClientForgotPwdPath         string
	webClientResetPwdPath          string
	webClientViewPDFPath           string
	webStaticFilesPath             string
	webOpenAPIPath                 string
	// max upload size for http clients, 1GB by default
	maxUploadFileSize = int64(1048576000)
)

func init() {
	updateWebAdminURLs("")
	updateWebClientURLs("")
}

// WebClientIntegration defines the configuration for an external Web Client integration
type WebClientIntegration struct {
	// Files with these extensions can be sent to the configured URL
	FileExtensions []string `json:"file_extensions" mapstructure:"file_extensions"`
	// URL that will receive the files
	URL string `json:"url" mapstructure:"url"`
}

// Binding defines the configuration for a network listener
type Binding struct {
	// The address to listen on. A blank value means listen on all available network interfaces.
	Address string `json:"address" mapstructure:"address"`
	// The port used for serving requests
	Port int `json:"port" mapstructure:"port"`
	// Enable the built-in admin interface.
	// You have to define TemplatesPath and StaticFilesPath for this to work
	EnableWebAdmin bool `json:"enable_web_admin" mapstructure:"enable_web_admin"`
	// Enable the built-in client interface.
	// You have to define TemplatesPath and StaticFilesPath for this to work
	EnableWebClient bool `json:"enable_web_client" mapstructure:"enable_web_client"`
	// you also need to provide a certificate for enabling HTTPS
	EnableHTTPS bool `json:"enable_https" mapstructure:"enable_https"`
	// set to 1 to require client certificate authentication in addition to basic auth.
	// You need to define at least a certificate authority for this to work
	ClientAuthType int `json:"client_auth_type" mapstructure:"client_auth_type"`
	// 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"`
	// List of IP addresses and IP ranges allowed to set X-Forwarded-For, X-Real-IP,
	// X-Forwarded-Proto headers.
	ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"`
	// If both web admin and web client are enabled each login page will show a link
	// to the other one. This setting allows to hide this link:
	// - 0 login links are displayed on both admin and client login page. This is the default
	// - 1 the login link to the web client login page is hidden on admin login page
	// - 2 the login link to the web admin login page is hidden on client login page
	// The flags can be combined, for example 3 will disable both login links.
	HideLoginURL int `json:"hide_login_url" mapstructure:"hide_login_url"`
	// Enable the built-in OpenAPI renderer
	RenderOpenAPI bool `json:"render_openapi" mapstructure:"render_openapi"`
	// Enabling web client integrations you can render or modify the files with the specified
	// extensions using an external tool.
	WebClientIntegrations []WebClientIntegration `json:"web_client_integrations" mapstructure:"web_client_integrations"`
	allowHeadersFrom      []func(net.IP) bool
}

func (b *Binding) checkWebClientIntegrations() {
	var integrations []WebClientIntegration
	for _, integration := range b.WebClientIntegrations {
		if integration.URL != "" && len(integration.FileExtensions) > 0 {
			integrations = append(integrations, integration)
		}
	}
	b.WebClientIntegrations = integrations
}

func (b *Binding) parseAllowedProxy() error {
	allowedFuncs, err := util.ParseAllowedIPAndRanges(b.ProxyAllowed)
	if err != nil {
		return err
	}
	b.allowHeadersFrom = allowedFuncs
	return nil
}

// GetAddress returns the binding address
func (b *Binding) GetAddress() string {
	return fmt.Sprintf("%s:%d", b.Address, b.Port)
}

// IsValid returns true if the binding is valid
func (b *Binding) IsValid() bool {
	if b.Port > 0 {
		return true
	}
	if filepath.IsAbs(b.Address) && runtime.GOOS != osWindows {
		return true
	}
	return false
}

func (b *Binding) showAdminLoginURL() bool {
	if !b.EnableWebAdmin {
		return false
	}
	if b.HideLoginURL&2 != 0 {
		return false
	}
	return true
}

func (b *Binding) showClientLoginURL() bool {
	if !b.EnableWebClient {
		return false
	}
	if b.HideLoginURL&1 != 0 {
		return false
	}
	return true
}

type defenderStatus struct {
	IsActive bool `json:"is_active"`
}

// ServicesStatus keep the state of the running services
type ServicesStatus struct {
	SSH          sftpd.ServiceStatus         `json:"ssh"`
	FTP          ftpd.ServiceStatus          `json:"ftp"`
	WebDAV       webdavd.ServiceStatus       `json:"webdav"`
	DataProvider dataprovider.ProviderStatus `json:"data_provider"`
	Defender     defenderStatus              `json:"defender"`
	MFA          mfa.ServiceStatus           `json:"mfa"`
}

// CorsConfig defines the CORS configuration
type CorsConfig struct {
	AllowedOrigins   []string `json:"allowed_origins" mapstructure:"allowed_origins"`
	AllowedMethods   []string `json:"allowed_methods" mapstructure:"allowed_methods"`
	AllowedHeaders   []string `json:"allowed_headers" mapstructure:"allowed_headers"`
	ExposedHeaders   []string `json:"exposed_headers" mapstructure:"exposed_headers"`
	AllowCredentials bool     `json:"allow_credentials" mapstructure:"allow_credentials"`
	Enabled          bool     `json:"enabled" mapstructure:"enabled"`
	MaxAge           int      `json:"max_age" mapstructure:"max_age"`
}

// Conf httpd daemon configuration
type Conf struct {
	// Addresses and ports to bind to
	Bindings []Binding `json:"bindings" mapstructure:"bindings"`
	// Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
	TemplatesPath string `json:"templates_path" mapstructure:"templates_path"`
	// Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir.
	// If both TemplatesPath and StaticFilesPath are empty the built-in web interface will be disabled
	StaticFilesPath string `json:"static_files_path" mapstructure:"static_files_path"`
	// Path to the backup directory. This can be an absolute path or a path relative to the config dir
	BackupsPath string `json:"backups_path" mapstructure:"backups_path"`
	// Path to the directory that contains the OpenAPI schema and the default renderer.
	// This can be an absolute path or a path relative to the config dir
	OpenAPIPath string `json:"openapi_path" mapstructure:"openapi_path"`
	// Defines a base URL for the web admin and client interfaces. If empty web admin and client resources will
	// be available at the root ("/") URI. If defined it must be an absolute URI or it will be ignored.
	WebRoot string `json:"web_root" mapstructure:"web_root"`
	// 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"`
	// CACertificates defines the set of root certificate authorities to be used to verify client certificates.
	CACertificates []string `json:"ca_certificates" mapstructure:"ca_certificates"`
	// CARevocationLists defines a set a revocation lists, one for each root CA, to be used to check
	// if a client certificate has been revoked
	CARevocationLists []string `json:"ca_revocation_lists" mapstructure:"ca_revocation_lists"`
	// SigningPassphrase defines the passphrase to use to derive the signing key for JWT and CSRF tokens.
	// If empty a random signing key will be generated each time SFTPGo starts. If you set a
	// signing passphrase you should consider rotating it periodically for added security
	SigningPassphrase string `json:"signing_passphrase" mapstructure:"signing_passphrase"`
	// MaxUploadFileSize Defines the maximum request body size, in bytes, for Web Client/API HTTP upload requests.
	// 0 means no limit
	MaxUploadFileSize int64 `json:"max_upload_file_size" mapstructure:"max_upload_file_size"`
	// CORS configuration
	Cors CorsConfig `json:"cors" mapstructure:"cors"`
}

type apiResponse struct {
	Error   string `json:"error,omitempty"`
	Message string `json:"message"`
}

// ShouldBind returns true if there is at least a valid binding
func (c *Conf) ShouldBind() bool {
	for _, binding := range c.Bindings {
		if binding.IsValid() {
			return true
		}
	}

	return false
}

func (c *Conf) isWebAdminEnabled() bool {
	for _, binding := range c.Bindings {
		if binding.EnableWebAdmin {
			return true
		}
	}
	return false
}

func (c *Conf) isWebClientEnabled() bool {
	for _, binding := range c.Bindings {
		if binding.EnableWebClient {
			return true
		}
	}
	return false
}

func (c *Conf) checkRequiredDirs(staticFilesPath, templatesPath string) error {
	if (c.isWebAdminEnabled() || c.isWebClientEnabled()) && (staticFilesPath == "" || templatesPath == "") {
		return fmt.Errorf("required directory is invalid, static file path: %#v template path: %#v",
			staticFilesPath, templatesPath)
	}
	return nil
}

func (c *Conf) getRedacted() Conf {
	conf := *c
	conf.SigningPassphrase = "[redacted]"
	return conf
}

// Initialize configures and starts the HTTP server
func (c *Conf) Initialize(configDir string) error {
	logger.Debug(logSender, "", "initializing HTTP server with config %+v", c.getRedacted())
	backupsPath = getConfigPath(c.BackupsPath, configDir)
	staticFilesPath := getConfigPath(c.StaticFilesPath, configDir)
	templatesPath := getConfigPath(c.TemplatesPath, configDir)
	openAPIPath := getConfigPath(c.OpenAPIPath, configDir)
	if backupsPath == "" {
		return fmt.Errorf("required directory is invalid, backup path %#v", backupsPath)
	}
	if err := c.checkRequiredDirs(staticFilesPath, templatesPath); err != nil {
		return err
	}
	certificateFile := getConfigPath(c.CertificateFile, configDir)
	certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
	if c.isWebAdminEnabled() {
		updateWebAdminURLs(c.WebRoot)
		loadAdminTemplates(templatesPath)
	} else {
		logger.Info(logSender, "", "built-in web admin interface disabled")
	}
	if c.isWebClientEnabled() {
		updateWebClientURLs(c.WebRoot)
		loadClientTemplates(templatesPath)
	} else {
		logger.Info(logSender, "", "built-in web client interface disabled")
	}
	if certificateFile != "" && certificateKeyFile != "" {
		mgr, err := common.NewCertManager(certificateFile, certificateKeyFile, configDir, logSender)
		if err != nil {
			return err
		}
		mgr.SetCACertificates(c.CACertificates)
		if err := mgr.LoadRootCAs(); err != nil {
			return err
		}
		mgr.SetCARevocationLists(c.CARevocationLists)
		if err := mgr.LoadCRLs(); err != nil {
			return err
		}
		certMgr = mgr
	}

	csrfTokenAuth = jwtauth.New(jwa.HS256.String(), getSigningKey(c.SigningPassphrase), nil)

	exitChannel := make(chan error, 1)

	for _, binding := range c.Bindings {
		if !binding.IsValid() {
			continue
		}
		if err := binding.parseAllowedProxy(); err != nil {
			return err
		}
		binding.checkWebClientIntegrations()

		go func(b Binding) {
			server := newHttpdServer(b, staticFilesPath, c.SigningPassphrase, c.Cors, openAPIPath)

			exitChannel <- server.listenAndServe()
		}(binding)
	}

	maxUploadFileSize = c.MaxUploadFileSize
	startCleanupTicker(tokenDuration / 2)
	return <-exitChannel
}

func isWebRequest(r *http.Request) bool {
	return strings.HasPrefix(r.RequestURI, webBasePath+"/")
}

func isWebClientRequest(r *http.Request) bool {
	return strings.HasPrefix(r.RequestURI, webBaseClientPath+"/")
}

// 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
}

func getServicesStatus() ServicesStatus {
	status := ServicesStatus{
		SSH:          sftpd.GetStatus(),
		FTP:          ftpd.GetStatus(),
		WebDAV:       webdavd.GetStatus(),
		DataProvider: dataprovider.GetProviderStatus(),
		Defender: defenderStatus{
			IsActive: common.Config.DefenderConfig.Enabled,
		},
		MFA: mfa.GetStatus(),
	}
	return status
}

func fileServer(r chi.Router, path string, root http.FileSystem) {
	if path != "/" && path[len(path)-1] != '/' {
		r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
		path += "/"
	}
	path += "*"

	r.Get(path, func(w http.ResponseWriter, r *http.Request) {
		rctx := chi.RouteContext(r.Context())
		pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
		fs := http.StripPrefix(pathPrefix, http.FileServer(root))
		fs.ServeHTTP(w, r)
	})
}

func updateWebClientURLs(baseURL string) {
	if !path.IsAbs(baseURL) {
		baseURL = "/"
	}
	webRootPath = path.Join(baseURL, webRootPathDefault)
	webBasePath = path.Join(baseURL, webBasePathDefault)
	webBaseClientPath = path.Join(baseURL, webBasePathClientDefault)
	webClientLoginPath = path.Join(baseURL, webClientLoginPathDefault)
	webClientTwoFactorPath = path.Join(baseURL, webClientTwoFactorPathDefault)
	webClientTwoFactorRecoveryPath = path.Join(baseURL, webClientTwoFactorRecoveryPathDefault)
	webClientFilesPath = path.Join(baseURL, webClientFilesPathDefault)
	webClientFilePath = path.Join(baseURL, webClientFilePathDefault)
	webClientSharesPath = path.Join(baseURL, webClientSharesPathDefault)
	webClientPubSharesPath = path.Join(baseURL, webClientPubSharesPathDefault)
	webClientSharePath = path.Join(baseURL, webClientSharePathDefault)
	webClientEditFilePath = path.Join(baseURL, webClientEditFilePathDefault)
	webClientDirsPath = path.Join(baseURL, webClientDirsPathDefault)
	webClientDownloadZipPath = path.Join(baseURL, webClientDownloadZipPathDefault)
	webClientProfilePath = path.Join(baseURL, webClientProfilePathDefault)
	webChangeClientPwdPath = path.Join(baseURL, webChangeClientPwdPathDefault)
	webClientLogoutPath = path.Join(baseURL, webClientLogoutPathDefault)
	webClientMFAPath = path.Join(baseURL, webClientMFAPathDefault)
	webClientTOTPGeneratePath = path.Join(baseURL, webClientTOTPGeneratePathDefault)
	webClientTOTPValidatePath = path.Join(baseURL, webClientTOTPValidatePathDefault)
	webClientTOTPSavePath = path.Join(baseURL, webClientTOTPSavePathDefault)
	webClientRecoveryCodesPath = path.Join(baseURL, webClientRecoveryCodesPathDefault)
	webClientForgotPwdPath = path.Join(baseURL, webClientForgotPwdPathDefault)
	webClientResetPwdPath = path.Join(baseURL, webClientResetPwdPathDefault)
	webClientViewPDFPath = path.Join(baseURL, webClientViewPDFPathDefault)
}

func updateWebAdminURLs(baseURL string) {
	if !path.IsAbs(baseURL) {
		baseURL = "/"
	}
	webRootPath = path.Join(baseURL, webRootPathDefault)
	webBasePath = path.Join(baseURL, webBasePathDefault)
	webBaseAdminPath = path.Join(baseURL, webBasePathAdminDefault)
	webAdminSetupPath = path.Join(baseURL, webAdminSetupPathDefault)
	webLoginPath = path.Join(baseURL, webLoginPathDefault)
	webAdminTwoFactorPath = path.Join(baseURL, webAdminTwoFactorPathDefault)
	webAdminTwoFactorRecoveryPath = path.Join(baseURL, webAdminTwoFactorRecoveryPathDefault)
	webLogoutPath = path.Join(baseURL, webLogoutPathDefault)
	webUsersPath = path.Join(baseURL, webUsersPathDefault)
	webUserPath = path.Join(baseURL, webUserPathDefault)
	webConnectionsPath = path.Join(baseURL, webConnectionsPathDefault)
	webFoldersPath = path.Join(baseURL, webFoldersPathDefault)
	webFolderPath = path.Join(baseURL, webFolderPathDefault)
	webStatusPath = path.Join(baseURL, webStatusPathDefault)
	webAdminsPath = path.Join(baseURL, webAdminsPathDefault)
	webAdminPath = path.Join(baseURL, webAdminPathDefault)
	webMaintenancePath = path.Join(baseURL, webMaintenancePathDefault)
	webBackupPath = path.Join(baseURL, webBackupPathDefault)
	webRestorePath = path.Join(baseURL, webRestorePathDefault)
	webScanVFolderPath = path.Join(baseURL, webScanVFolderPathDefault)
	webQuotaScanPath = path.Join(baseURL, webQuotaScanPathDefault)
	webChangeAdminPwdPath = path.Join(baseURL, webChangeAdminPwdPathDefault)
	webAdminForgotPwdPath = path.Join(baseURL, webAdminForgotPwdPathDefault)
	webAdminResetPwdPath = path.Join(baseURL, webAdminResetPwdPathDefault)
	webAdminProfilePath = path.Join(baseURL, webAdminProfilePathDefault)
	webAdminMFAPath = path.Join(baseURL, webAdminMFAPathDefault)
	webAdminTOTPGeneratePath = path.Join(baseURL, webAdminTOTPGeneratePathDefault)
	webAdminTOTPValidatePath = path.Join(baseURL, webAdminTOTPValidatePathDefault)
	webAdminTOTPSavePath = path.Join(baseURL, webAdminTOTPSavePathDefault)
	webAdminRecoveryCodesPath = path.Join(baseURL, webAdminRecoveryCodesPathDefault)
	webTemplateUser = path.Join(baseURL, webTemplateUserDefault)
	webTemplateFolder = path.Join(baseURL, webTemplateFolderDefault)
	webDefenderHostsPath = path.Join(baseURL, webDefenderHostsPathDefault)
	webDefenderPath = path.Join(baseURL, webDefenderPathDefault)
	webStaticFilesPath = path.Join(baseURL, webStaticFilesPathDefault)
	webOpenAPIPath = path.Join(baseURL, webOpenAPIPathDefault)
}

// GetHTTPRouter returns an HTTP handler suitable to use for test cases
func GetHTTPRouter(b Binding) http.Handler {
	server := newHttpdServer(b, "../static", "", CorsConfig{}, "../openapi")
	server.initializeRouter()
	return server.router
}

// the ticker cannot be started/stopped from multiple goroutines
func startCleanupTicker(duration time.Duration) {
	stopCleanupTicker()
	cleanupTicker = time.NewTicker(duration)
	cleanupDone = make(chan bool)

	go func() {
		for {
			select {
			case <-cleanupDone:
				return
			case <-cleanupTicker.C:
				cleanupExpiredJWTTokens()
				cleanupExpiredResetCodes()
			}
		}
	}()
}

func stopCleanupTicker() {
	if cleanupTicker != nil {
		cleanupTicker.Stop()
		cleanupDone <- true
		cleanupTicker = nil
	}
}

func cleanupExpiredJWTTokens() {
	invalidatedJWTTokens.Range(func(key, value interface{}) bool {
		exp, ok := value.(time.Time)
		if !ok || exp.Before(time.Now().UTC()) {
			invalidatedJWTTokens.Delete(key)
		}
		return true
	})
}

func getSigningKey(signingPassphrase string) []byte {
	if signingPassphrase != "" {
		sk := sha256.Sum256([]byte(signingPassphrase))
		return sk[:]
	}
	return util.GenerateRandomBytes(32)
}
