Browse Source

portable mode: add WebClient

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 2 years ago
parent
commit
6c482a248d
7 changed files with 153 additions and 57 deletions
  1. 6 0
      docs/portable-mode.md
  2. 1 1
      go.mod
  3. 2 2
      go.sum
  4. 29 4
      internal/cmd/portable.go
  5. 1 1
      internal/httpd/webadmin.go
  6. 113 49
      internal/service/service_portable.go
  7. 1 0
      openapi/openapi.yaml

+ 6 - 0
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:

+ 1 - 1
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

+ 2 - 2
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=

+ 29 - 4
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", "", "")

+ 1 - 1
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
 	}

+ 113 - 49
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://<your IP>:%v/", scheme, config.GetWebDAVDConfig().Bindings[0].Port))
+		info.WriteString(fmt.Sprintf("WebDAV URL: %v://<your IP>:%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://<your IP>:%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)
+}

+ 1 - 0
openapi/openapi.yaml

@@ -7031,6 +7031,7 @@ components:
             - GET
             - POST
             - PUT
+            - DELETE
         query_parameters:
           type: array
           items: