From 6c482a248d002bacccaec33e358693ee1a32016d Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Thu, 10 Aug 2023 19:02:55 +0200 Subject: [PATCH] portable mode: add WebClient Signed-off-by: Nicola Murino --- docs/portable-mode.md | 6 + go.mod | 2 +- go.sum | 4 +- internal/cmd/portable.go | 33 +++++- internal/httpd/webadmin.go | 2 +- internal/service/service_portable.go | 162 +++++++++++++++++++-------- openapi/openapi.yaml | 1 + 7 files changed, 153 insertions(+), 57 deletions(-) diff --git a/docs/portable-mode.md b/docs/portable-mode.md index 92a95c04..9f1cda47 100644 --- a/docs/portable-mode.md +++ b/docs/portable-mode.md @@ -75,6 +75,12 @@ Flags: interrupt signal. -h, --help help for portable + --httpd-cert string Path to the certificate file for WebClient + over HTTPS + --httpd-key string Path to the key file for WebClient over + HTTPS + --httpd-port int 0 means a random unprivileged port, + < 0 disabled (default -1) -l, --log-file-path string Leave empty to disable logging --log-level string Set the log level. Supported values: diff --git a/go.mod b/go.mod index 98e7a2f6..bae49666 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/jackc/pgx/v5 v5.4.3 github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126 github.com/klauspost/compress v1.16.7 - github.com/lestrrat-go/jwx/v2 v2.0.11 + github.com/lestrrat-go/jwx/v2 v2.0.12 github.com/lithammer/shortuuid/v3 v3.0.7 github.com/mattn/go-sqlite3 v1.14.17 github.com/mhale/smtpd v0.8.0 diff --git a/go.sum b/go.sum index 50c807e4..849caad1 100644 --- a/go.sum +++ b/go.sum @@ -338,8 +338,8 @@ github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJG github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.0.11 h1:ViHMnaMeaO0qV16RZWBHM7GTrAnX2aFLVKofc7FuKLQ= -github.com/lestrrat-go/jwx/v2 v2.0.11/go.mod h1:ZtPtMFlrfDrH2Y0iwfa3dRFn8VzwBrB+cyrm3IBWdDg= +github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA= +github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= diff --git a/internal/cmd/portable.go b/internal/cmd/portable.go index 2c876e88..93034295 100644 --- a/internal/cmd/portable.go +++ b/internal/cmd/portable.go @@ -75,6 +75,9 @@ var ( portableWebDAVPort int portableWebDAVCert string portableWebDAVKey string + portableHTTPPort int + portableHTTPSCert string + portableHTTPSKey string portableAzContainer string portableAzAccountName string portableAzAccountKey string @@ -152,7 +155,7 @@ Please take a look at the usage below to customize the serving parameters`, os.Exit(1) } } - if portableWebDAVPort > 0 && portableWebDAVCert != "" && portableWebDAVKey != "" { + if portableWebDAVPort >= 0 && portableWebDAVCert != "" && portableWebDAVKey != "" { keyPairs := []common.TLSKeyPair{ { Cert: portableWebDAVCert, @@ -168,6 +171,22 @@ Please take a look at the usage below to customize the serving parameters`, os.Exit(1) } } + if portableHTTPPort >= 0 && portableHTTPSCert != "" && portableHTTPSKey != "" { + keyPairs := []common.TLSKeyPair{ + { + Cert: portableHTTPSCert, + Key: portableHTTPSKey, + ID: common.DefaultTLSKeyPaidID, + }, + } + _, err := common.NewCertManager(keyPairs, filepath.Clean(defaultConfigDir), + "HTTP portable") + if err != nil { + fmt.Printf("Unable to load HTTPS key pair, cert file %q key file %q error: %v\n", + portableHTTPSCert, portableHTTPSKey, err) + os.Exit(1) + } + } pwd := portablePassword if portablePasswordFile != "" { content, err := os.ReadFile(portablePasswordFile) @@ -266,9 +285,9 @@ Please take a look at the usage below to customize the serving parameters`, }, }, } - err := service.StartPortableMode(portableSFTPDPort, portableFTPDPort, portableWebDAVPort, portableSSHCommands, - portableFTPSCert, portableFTPSKey, portableWebDAVCert, - portableWebDAVKey) + err := service.StartPortableMode(portableSFTPDPort, portableFTPDPort, portableWebDAVPort, portableHTTPPort, + portableSSHCommands, portableFTPSCert, portableFTPSKey, portableWebDAVCert, portableWebDAVKey, + portableHTTPSCert, portableHTTPSKey) if err == nil { service.Wait() if service.Error == nil { @@ -295,6 +314,8 @@ path`) portableCmd.Flags().IntVar(&portableFTPDPort, "ftpd-port", -1, `0 means a random unprivileged port, < 0 disabled`) portableCmd.Flags().IntVar(&portableWebDAVPort, "webdav-port", -1, `0 means a random unprivileged port, +< 0 disabled`) + portableCmd.Flags().IntVar(&portableHTTPPort, "httpd-port", -1, `0 means a random unprivileged port, < 0 disabled`) portableCmd.Flags().StringSliceVarP(&portableSSHCommands, "ssh-commands", "c", sftpd.GetDefaultSSHCommands(), `SSH commands to enable. @@ -366,6 +387,10 @@ a JSON credentials file, 1 automatic portableCmd.Flags().StringVar(&portableWebDAVCert, "webdav-cert", "", `Path to the certificate file for WebDAV over HTTPS`) portableCmd.Flags().StringVar(&portableWebDAVKey, "webdav-key", "", `Path to the key file for WebDAV over +HTTPS`) + portableCmd.Flags().StringVar(&portableHTTPSCert, "httpd-cert", "", `Path to the certificate file for WebClient +over HTTPS`) + portableCmd.Flags().StringVar(&portableHTTPSKey, "httpd-key", "", `Path to the key file for WebClient over HTTPS`) portableCmd.Flags().StringVar(&portableAzContainer, "az-container", "", "") portableCmd.Flags().StringVar(&portableAzAccountName, "az-account-name", "", "") diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go index 57aa370f..59f4d708 100644 --- a/internal/httpd/webadmin.go +++ b/internal/httpd/webadmin.go @@ -4174,7 +4174,7 @@ func (s *httpdServer) handleOAuth2TokenRedirect(w http.ResponseWriter, r *http.R errTxt := "the OAuth2 provider returned an empty token. " + "Some providers only return the token when the user first authorizes. " + "If you have already registered SFTPGo with this user in the past, revoke access and try again. " + - "This way you will invalidate the previous token." + "This way you will invalidate the previous token" s.renderMessagePage(w, r, errorTitle, "Unable to get token:", http.StatusBadRequest, errors.New(errTxt), "") return } diff --git a/internal/service/service_portable.go b/internal/service/service_portable.go index d49114ba..1c3dafcf 100644 --- a/internal/service/service_portable.go +++ b/internal/service/service_portable.go @@ -27,6 +27,7 @@ import ( "github.com/drakkan/sftpgo/v2/internal/config" "github.com/drakkan/sftpgo/v2/internal/dataprovider" "github.com/drakkan/sftpgo/v2/internal/ftpd" + "github.com/drakkan/sftpgo/v2/internal/httpd" "github.com/drakkan/sftpgo/v2/internal/kms" "github.com/drakkan/sftpgo/v2/internal/logger" "github.com/drakkan/sftpgo/v2/internal/sftpd" @@ -36,8 +37,8 @@ import ( ) // StartPortableMode starts the service in portable mode -func (s *Service) StartPortableMode(sftpdPort, ftpPort, webdavPort int, enabledSSHCommands []string, - ftpsCert, ftpsKey, webDavCert, webDavKey string) error { +func (s *Service) StartPortableMode(sftpdPort, ftpPort, webdavPort, httpPort int, enabledSSHCommands []string, + ftpsCert, ftpsKey, webDavCert, webDavKey, httpsCert, httpsKey string) error { if s.PortableMode != 1 { return fmt.Errorf("service is not configured for portable mode") } @@ -56,71 +57,50 @@ func (s *Service) StartPortableMode(sftpdPort, ftpPort, webdavPort int, enabledS dataProviderConf.Name = "" config.SetProviderConf(dataProviderConf) httpdConf := config.GetHTTPDConfig() - httpdConf.Bindings = nil + for idx := range httpdConf.Bindings { + httpdConf.Bindings[idx].Port = 0 + } config.SetHTTPDConfig(httpdConf) telemetryConf := config.GetTelemetryConfig() telemetryConf.BindPort = 0 config.SetTelemetryConfig(telemetryConf) - sftpdConf := config.GetSFTPDConfig() - sftpdConf.MaxAuthTries = 12 - sftpdConf.Bindings = []sftpd.Binding{ - { - Port: sftpdPort, - }, - } + if sftpdPort >= 0 { - if sftpdPort > 0 { - sftpdConf.Bindings[0].Port = sftpdPort - } else { - // dynamic ports starts from 49152 - sftpdConf.Bindings[0].Port = 49152 + rand.Intn(15000) - } - if util.Contains(enabledSSHCommands, "*") { - sftpdConf.EnabledSSHCommands = sftpd.GetSupportedSSHCommands() - } else { - sftpdConf.EnabledSSHCommands = enabledSSHCommands - } + configurePortableSFTPService(sftpdPort, enabledSSHCommands) } - config.SetSFTPDConfig(sftpdConf) if ftpPort >= 0 { - ftpConf := config.GetFTPDConfig() - binding := ftpd.Binding{} - if ftpPort > 0 { - binding.Port = ftpPort - } else { - binding.Port = 49152 + rand.Intn(15000) - } - ftpConf.Bindings = []ftpd.Binding{binding} - ftpConf.Banner = fmt.Sprintf("SFTPGo portable %v ready", version.Get().Version) - ftpConf.CertificateFile = ftpsCert - ftpConf.CertificateKeyFile = ftpsKey - config.SetFTPDConfig(ftpConf) + configurePortableFTPService(ftpPort, ftpsCert, ftpsKey) } if webdavPort >= 0 { - webDavConf := config.GetWebDAVDConfig() - binding := webdavd.Binding{} - if webdavPort > 0 { - binding.Port = webdavPort - } else { - binding.Port = 49152 + rand.Intn(15000) - } - webDavConf.Bindings = []webdavd.Binding{binding} - webDavConf.CertificateFile = webDavCert - webDavConf.CertificateKeyFile = webDavKey - config.SetWebDAVDConfig(webDavConf) + configurePortableWebDAVService(webdavPort, webDavCert, webDavKey) + } + + if httpPort >= 0 { + configurePortableHTTPService(httpPort, httpsCert, httpsKey) } err = s.Start(true) if err != nil { return err } + if httpPort >= 0 { + admin := &dataprovider.Admin{ + Username: util.GenerateUniqueID(), + Password: util.GenerateUniqueID(), + Status: 0, + Permissions: []string{dataprovider.PermAdminAny}, + } + if err := dataprovider.AddAdmin(admin, dataprovider.ActionExecutorSystem, "", ""); err != nil { + return err + } + } logger.InfoToConsole("Portable mode ready, user: %q, password: %q, public keys: %v, directory: %q, "+ - "permissions: %+v, enabled ssh commands: %v file patterns filters: %+v %v", s.PortableUser.Username, + "permissions: %+v, file patterns filters: %+v %v", s.PortableUser.Username, printablePassword, s.PortableUser.PublicKeys, s.getPortableDirToServe(), s.PortableUser.Permissions, - sftpdConf.EnabledSSHCommands, s.PortableUser.Filters.FilePatterns, s.getServiceOptionalInfoString()) + s.PortableUser.Filters.FilePatterns, s.getServiceOptionalInfoString()) return nil } @@ -137,7 +117,14 @@ func (s *Service) getServiceOptionalInfoString() string { if config.GetWebDAVDConfig().CertificateFile != "" && config.GetWebDAVDConfig().CertificateKeyFile != "" { scheme = "https" } - info.WriteString(fmt.Sprintf("WebDAV URL: %v://:%v/", scheme, config.GetWebDAVDConfig().Bindings[0].Port)) + info.WriteString(fmt.Sprintf("WebDAV URL: %v://:%v/ ", scheme, config.GetWebDAVDConfig().Bindings[0].Port)) + } + if config.GetHTTPDConfig().Bindings[0].IsValid() { + scheme := "http" + if config.GetHTTPDConfig().CertificateFile != "" && config.GetHTTPDConfig().CertificateKeyFile != "" { + scheme = "https" + } + info.WriteString(fmt.Sprintf("WebClient URL: %v://:%v/ ", scheme, config.GetHTTPDConfig().Bindings[0].Port)) } return info.String() } @@ -170,12 +157,16 @@ func (s *Service) configurePortableUser() string { } if len(s.PortableUser.PublicKeys) == 0 && s.PortableUser.Password == "" { var b strings.Builder - for i := 0; i < 8; i++ { + for i := 0; i < 16; i++ { b.WriteRune(chars[rand.Intn(len(chars))]) } s.PortableUser.Password = b.String() printablePassword = s.PortableUser.Password } + s.PortableUser.Filters.WebClient = []string{sdk.WebClientSharesDisabled, sdk.WebClientInfoChangeDisabled, + sdk.WebClientPubKeyChangeDisabled, sdk.WebClientPasswordChangeDisabled, sdk.WebClientAPIKeyAuthChangeDisabled, + sdk.WebClientMFADisabled, + } s.configurePortableSecrets() return printablePassword } @@ -218,3 +209,76 @@ func getSecretFromString(payload string) *kms.Secret { } return kms.NewEmptySecret() } + +func configurePortableSFTPService(port int, enabledSSHCommands []string) { + sftpdConf := config.GetSFTPDConfig() + if len(sftpdConf.Bindings) == 0 { + sftpdConf.Bindings = append(sftpdConf.Bindings, sftpd.Binding{}) + } + if port > 0 { + sftpdConf.Bindings[0].Port = port + } else { + // dynamic ports starts from 49152 + sftpdConf.Bindings[0].Port = 49152 + rand.Intn(15000) + } + if util.Contains(enabledSSHCommands, "*") { + sftpdConf.EnabledSSHCommands = sftpd.GetSupportedSSHCommands() + } else { + sftpdConf.EnabledSSHCommands = enabledSSHCommands + } + config.SetSFTPDConfig(sftpdConf) +} + +func configurePortableFTPService(port int, cert, key string) { + ftpConf := config.GetFTPDConfig() + if len(ftpConf.Bindings) == 0 { + ftpConf.Bindings = append(ftpConf.Bindings, ftpd.Binding{}) + } + if port > 0 { + ftpConf.Bindings[0].Port = port + } else { + ftpConf.Bindings[0].Port = 49152 + rand.Intn(15000) + } + if ftpConf.Banner == "" { + ftpConf.Banner = fmt.Sprintf("SFTPGo portable %v ready", version.Get().Version) + } + ftpConf.Bindings[0].CertificateFile = cert + ftpConf.Bindings[0].CertificateKeyFile = key + config.SetFTPDConfig(ftpConf) +} + +func configurePortableWebDAVService(port int, cert, key string) { + webDavConf := config.GetWebDAVDConfig() + if len(webDavConf.Bindings) == 0 { + webDavConf.Bindings = append(webDavConf.Bindings, webdavd.Binding{}) + } + if port > 0 { + webDavConf.Bindings[0].Port = port + } else { + webDavConf.Bindings[0].Port = 49152 + rand.Intn(15000) + } + webDavConf.Bindings[0].CertificateFile = cert + webDavConf.Bindings[0].CertificateKeyFile = key + webDavConf.Bindings[0].EnableHTTPS = true + config.SetWebDAVDConfig(webDavConf) +} + +func configurePortableHTTPService(port int, cert, key string) { + httpdConf := config.GetHTTPDConfig() + if len(httpdConf.Bindings) == 0 { + httpdConf.Bindings = append(httpdConf.Bindings, httpd.Binding{}) + } + if port > 0 { + httpdConf.Bindings[0].Port = port + } else { + httpdConf.Bindings[0].Port = 49152 + rand.Intn(15000) + } + httpdConf.Bindings[0].CertificateFile = cert + httpdConf.Bindings[0].CertificateKeyFile = key + httpdConf.Bindings[0].EnableHTTPS = true + httpdConf.Bindings[0].EnableWebAdmin = false + httpdConf.Bindings[0].EnableWebClient = true + httpdConf.Bindings[0].EnableRESTAPI = false + httpdConf.Bindings[0].RenderOpenAPI = false + config.SetHTTPDConfig(httpdConf) +} diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 7cccfd44..ad4c39c3 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -7031,6 +7031,7 @@ components: - GET - POST - PUT + - DELETE query_parameters: type: array items: