浏览代码

WebClient shares: replace basic auth with a login form

basic auth will continue to work for REST API

Fixes #1166

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 2 年之前
父节点
当前提交
7e85356325

+ 1 - 1
docs/account.md

@@ -18,5 +18,5 @@ SFTPGo supports checking passwords stored with argon2id, bcrypt, pbkdf2, md5cryp
 
 
 If you want to use your existing accounts, you have these options:
 If you want to use your existing accounts, you have these options:
 
 
-- you can import your users inside SFTPGo. Take a look at [convert users](.../examples/convertusers) script, it can convert and import users from Linux system users and Pure-FTPd/ProFTPD virtual users
+- you can import your users inside SFTPGo. Take a look at [convert users](../examples/convertusers) script, it can convert and import users from Linux system users and Pure-FTPd/ProFTPD virtual users
 - you can use an external authentication program
 - you can use an external authentication program

+ 1 - 1
go.mod

@@ -35,7 +35,7 @@ require (
 	github.com/hashicorp/go-hclog v1.4.0
 	github.com/hashicorp/go-hclog v1.4.0
 	github.com/hashicorp/go-plugin v1.4.8
 	github.com/hashicorp/go-plugin v1.4.8
 	github.com/hashicorp/go-retryablehttp v0.7.2
 	github.com/hashicorp/go-retryablehttp v0.7.2
-	github.com/jackc/pgx/v5 v5.2.0
+	github.com/jackc/pgx/v5 v5.3.0
 	github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
 	github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
 	github.com/klauspost/compress v1.15.15
 	github.com/klauspost/compress v1.15.15
 	github.com/lestrrat-go/jwx/v2 v2.0.8
 	github.com/lestrrat-go/jwx/v2 v2.0.8

+ 2 - 2
go.sum

@@ -1350,8 +1350,8 @@ github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgS
 github.com/jackc/pgx/v4 v4.16.0/go.mod h1:N0A9sFdWzkw/Jy1lwoiB64F2+ugFZi987zRxcPez/wI=
 github.com/jackc/pgx/v4 v4.16.0/go.mod h1:N0A9sFdWzkw/Jy1lwoiB64F2+ugFZi987zRxcPez/wI=
 github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ=
 github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ=
 github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
 github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
-github.com/jackc/pgx/v5 v5.2.0 h1:NdPpngX0Y6z6XDFKqmFQaE+bCtkqzvQIOt1wvBlAqs8=
-github.com/jackc/pgx/v5 v5.2.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk=
+github.com/jackc/pgx/v5 v5.3.0 h1:/NQi8KHMpKWHInxXesC8yD4DhkXPrVhmnwYkjp9AmBA=
+github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
 github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=

+ 2 - 5
internal/dataprovider/share.go

@@ -273,14 +273,11 @@ func (s *Share) validate() error {
 }
 }
 
 
 // CheckCredentials verifies the share credentials if a password if set
 // CheckCredentials verifies the share credentials if a password if set
-func (s *Share) CheckCredentials(username, password string) (bool, error) {
+func (s *Share) CheckCredentials(password string) (bool, error) {
 	if s.Password == "" {
 	if s.Password == "" {
 		return true, nil
 		return true, nil
 	}
 	}
-	if username == "" || password == "" {
-		return false, ErrInvalidCredentials
-	}
-	if username != s.Username {
+	if password == "" {
 		return false, ErrInvalidCredentials
 		return false, ErrInvalidCredentials
 	}
 	}
 	if strings.HasPrefix(s.Password, bcryptPwdPrefix) {
 	if strings.HasPrefix(s.Password, bcryptPwdPrefix) {

+ 58 - 17
internal/httpd/api_shares.go

@@ -25,6 +25,7 @@ import (
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
+	"github.com/go-chi/jwtauth/v5"
 	"github.com/go-chi/render"
 	"github.com/go-chi/render"
 	"github.com/rs/xid"
 	"github.com/rs/xid"
 	"github.com/sftpgo/sdk"
 	"github.com/sftpgo/sdk"
@@ -179,7 +180,7 @@ func deleteShare(w http.ResponseWriter, r *http.Request) {
 func (s *httpdServer) readBrowsableShareContents(w http.ResponseWriter, r *http.Request) {
 func (s *httpdServer) readBrowsableShareContents(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
-	share, connection, err := s.checkPublicShare(w, r, validScopes, false)
+	share, connection, err := s.checkPublicShare(w, r, validScopes)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -210,7 +211,7 @@ func (s *httpdServer) readBrowsableShareContents(w http.ResponseWriter, r *http.
 func (s *httpdServer) downloadBrowsableSharedFile(w http.ResponseWriter, r *http.Request) {
 func (s *httpdServer) downloadBrowsableSharedFile(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
-	share, connection, err := s.checkPublicShare(w, r, validScopes, false)
+	share, connection, err := s.checkPublicShare(w, r, validScopes)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -260,7 +261,7 @@ func (s *httpdServer) downloadBrowsableSharedFile(w http.ResponseWriter, r *http
 func (s *httpdServer) downloadFromShare(w http.ResponseWriter, r *http.Request) {
 func (s *httpdServer) downloadFromShare(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
-	share, connection, err := s.checkPublicShare(w, r, validScopes, false)
+	share, connection, err := s.checkPublicShare(w, r, validScopes)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -323,7 +324,7 @@ func (s *httpdServer) uploadFileToShare(w http.ResponseWriter, r *http.Request)
 	}
 	}
 	name := getURLParam(r, "name")
 	name := getURLParam(r, "name")
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite}
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite}
-	share, connection, err := s.checkPublicShare(w, r, validScopes, false)
+	share, connection, err := s.checkPublicShare(w, r, validScopes)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -353,7 +354,7 @@ func (s *httpdServer) uploadFilesToShare(w http.ResponseWriter, r *http.Request)
 		r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
 		r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
 	}
 	}
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite}
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite}
-	share, connection, err := s.checkPublicShare(w, r, validScopes, false)
+	share, connection, err := s.checkPublicShare(w, r, validScopes)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -402,9 +403,43 @@ func (s *httpdServer) uploadFilesToShare(w http.ResponseWriter, r *http.Request)
 	}
 	}
 }
 }
 
 
+func (s *httpdServer) checkWebClientShareCredentials(w http.ResponseWriter, r *http.Request, share *dataprovider.Share) error {
+	doRedirect := func() {
+		redirectURL := path.Join(webClientPubSharesPath, share.ShareID, fmt.Sprintf("login?next=%s", url.QueryEscape(r.RequestURI)))
+		http.Redirect(w, r, redirectURL, http.StatusFound)
+	}
+
+	token, err := jwtauth.VerifyRequest(s.tokenAuth, r, jwtauth.TokenFromCookie)
+	if err != nil || token == nil {
+		doRedirect()
+		return errInvalidToken
+	}
+	if !util.Contains(token.Audience(), tokenAudienceWebShare) {
+		logger.Debug(logSender, "", "invalid token audience for share %q", share.ShareID)
+		doRedirect()
+		return errInvalidToken
+	}
+	if tokenValidationMode != tokenValidationNoIPMatch {
+		ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+		if !util.Contains(token.Audience(), ipAddr) {
+			logger.Debug(logSender, "", "token for share %q is not valid for the ip address %q", share.ShareID, ipAddr)
+			doRedirect()
+			return errInvalidToken
+		}
+	}
+	ctx := jwtauth.NewContext(r.Context(), token, nil)
+	claims, err := getTokenClaims(r.WithContext(ctx))
+	if err != nil || claims.Username != share.ShareID {
+		logger.Debug(logSender, "", "token not valid for share %q", share.ShareID)
+		doRedirect()
+		return errInvalidToken
+	}
+	return nil
+}
+
 func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, validScopes []dataprovider.ShareScope,
 func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, validScopes []dataprovider.ShareScope,
-	isWebClient bool,
 ) (dataprovider.Share, *Connection, error) {
 ) (dataprovider.Share, *Connection, error) {
+	isWebClient := isWebClientRequest(r)
 	renderError := func(err error, message string, statusCode int) {
 	renderError := func(err error, message string, statusCode int) {
 		if isWebClient {
 		if isWebClient {
 			s.renderClientMessagePage(w, r, "Unable to access the share", message, statusCode, err, "")
 			s.renderClientMessagePage(w, r, "Unable to access the share", message, statusCode, err, "")
@@ -434,17 +469,23 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, v
 		return share, nil, err
 		return share, nil, err
 	}
 	}
 	if share.Password != "" {
 	if share.Password != "" {
-		username, password, ok := r.BasicAuth()
-		if !ok {
-			w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
-			renderError(dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
-			return share, nil, dataprovider.ErrInvalidCredentials
-		}
-		match, err := share.CheckCredentials(username, password)
-		if !match || err != nil {
-			w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
-			renderError(dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
-			return share, nil, dataprovider.ErrInvalidCredentials
+		if isWebClient {
+			if err := s.checkWebClientShareCredentials(w, r, &share); err != nil {
+				return share, nil, dataprovider.ErrInvalidCredentials
+			}
+		} else {
+			_, password, ok := r.BasicAuth()
+			if !ok {
+				w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
+				renderError(dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+				return share, nil, dataprovider.ErrInvalidCredentials
+			}
+			match, err := share.CheckCredentials(password)
+			if !match || err != nil {
+				w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
+				renderError(dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+				return share, nil, dataprovider.ErrInvalidCredentials
+			}
 		}
 		}
 	}
 	}
 	user, err := getUserForShare(share)
 	user, err := getUserForShare(share)

+ 10 - 3
internal/httpd/auth_utils.go

@@ -34,6 +34,7 @@ type tokenAudience = string
 const (
 const (
 	tokenAudienceWebAdmin         tokenAudience = "WebAdmin"
 	tokenAudienceWebAdmin         tokenAudience = "WebAdmin"
 	tokenAudienceWebClient        tokenAudience = "WebClient"
 	tokenAudienceWebClient        tokenAudience = "WebClient"
+	tokenAudienceWebShare         tokenAudience = "WebShare"
 	tokenAudienceWebAdminPartial  tokenAudience = "WebAdminPartial"
 	tokenAudienceWebAdminPartial  tokenAudience = "WebAdminPartial"
 	tokenAudienceWebClientPartial tokenAudience = "WebClientPartial"
 	tokenAudienceWebClientPartial tokenAudience = "WebClientPartial"
 	tokenAudienceAPI              tokenAudience = "API"
 	tokenAudienceAPI              tokenAudience = "API"
@@ -63,7 +64,8 @@ const (
 )
 )
 
 
 var (
 var (
-	tokenDuration = 20 * time.Minute
+	tokenDuration      = 20 * time.Minute
+	shareTokenDuration = 12 * time.Hour
 	// csrf token duration is greater than normal token duration to reduce issues
 	// csrf token duration is greater than normal token duration to reduce issues
 	// with the login form
 	// with the login form
 	csrfTokenDuration     = 6 * time.Hour
 	csrfTokenDuration     = 6 * time.Hour
@@ -267,12 +269,16 @@ func (c *jwtTokenClaims) createAndSetCookie(w http.ResponseWriter, r *http.Reque
 	} else {
 	} else {
 		basePath = webBaseClientPath
 		basePath = webBaseClientPath
 	}
 	}
+	duration := tokenDuration
+	if audience == tokenAudienceWebShare {
+		duration = shareTokenDuration
+	}
 	http.SetCookie(w, &http.Cookie{
 	http.SetCookie(w, &http.Cookie{
 		Name:     jwtCookieKey,
 		Name:     jwtCookieKey,
 		Value:    resp["access_token"].(string),
 		Value:    resp["access_token"].(string),
 		Path:     basePath,
 		Path:     basePath,
-		Expires:  time.Now().Add(tokenDuration),
-		MaxAge:   int(tokenDuration / time.Second),
+		Expires:  time.Now().Add(duration),
+		MaxAge:   int(duration / time.Second),
 		HttpOnly: true,
 		HttpOnly: true,
 		Secure:   isTLS(r),
 		Secure:   isTLS(r),
 		SameSite: http.SameSiteStrictMode,
 		SameSite: http.SameSiteStrictMode,
@@ -403,6 +409,7 @@ func verifyCSRFToken(tokenString, ip string) error {
 
 
 	if tokenValidationMode != tokenValidationNoIPMatch {
 	if tokenValidationMode != tokenValidationNoIPMatch {
 		if !util.Contains(token.Audience(), ip) {
 		if !util.Contains(token.Audience(), ip) {
+			fmt.Printf("ip %v audience %+v\n\n", ip, token.Audience())
 			logger.Debug(logSender, "", "error validating CSRF token IP audience")
 			logger.Debug(logSender, "", "error validating CSRF token IP audience")
 			return errors.New("the form token is not valid")
 			return errors.New("the form token is not valid")
 		}
 		}

+ 154 - 8
internal/httpd/httpd_test.go

@@ -12471,6 +12471,13 @@ func TestShareUsage(t *testing.T) {
 	rr = executeRequest(req)
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusNotFound, rr)
 	checkResponseCode(t, http.StatusNotFound, rr)
 
 
+	req, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+"/"+objectID+"_mod", nil)
+	assert.NoError(t, err)
+	req.RequestURI = webClientPubSharesPath + "/" + objectID + "_mod"
+	req.SetBasicAuth(defaultUsername, defaultPassword)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusNotFound, rr)
+
 	share.ExpiresAt = 0
 	share.ExpiresAt = 0
 	jsonReq := make(map[string]any)
 	jsonReq := make(map[string]any)
 	jsonReq["name"] = share.Name
 	jsonReq["name"] = share.Name
@@ -12680,6 +12687,148 @@ func TestShareUsage(t *testing.T) {
 	executeRequest(req)
 	executeRequest(req)
 }
 }
 
 
+func TestWebClientShareCredentials(t *testing.T) {
+	user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
+	assert.NoError(t, err)
+	token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
+	assert.NoError(t, err)
+
+	shareRead := dataprovider.Share{
+		Name:     "test share read",
+		Scope:    dataprovider.ShareScopeRead,
+		Password: defaultPassword,
+		Paths:    []string{"/"},
+	}
+
+	shareWrite := dataprovider.Share{
+		Name:     "test share write",
+		Scope:    dataprovider.ShareScopeReadWrite,
+		Password: defaultPassword,
+		Paths:    []string{"/"},
+	}
+
+	asJSON, err := json.Marshal(shareRead)
+	assert.NoError(t, err)
+	req, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))
+	assert.NoError(t, err)
+	setBearerForReq(req, token)
+	rr := executeRequest(req)
+	checkResponseCode(t, http.StatusCreated, rr)
+	shareReadID := rr.Header().Get("X-Object-ID")
+	assert.NotEmpty(t, shareReadID)
+
+	asJSON, err = json.Marshal(shareWrite)
+	assert.NoError(t, err)
+	req, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))
+	assert.NoError(t, err)
+	setBearerForReq(req, token)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusCreated, rr)
+	shareWriteID := rr.Header().Get("X-Object-ID")
+	assert.NotEmpty(t, shareWriteID)
+
+	uri := path.Join(webClientPubSharesPath, shareReadID, "browse")
+	req, err = http.NewRequest(http.MethodGet, uri, nil)
+	assert.NoError(t, err)
+	req.RequestURI = uri
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusFound, rr)
+	location := rr.Header().Get("Location")
+	assert.Contains(t, location, url.QueryEscape(uri))
+	// get the login form
+	req, err = http.NewRequest(http.MethodGet, location, nil)
+	assert.NoError(t, err)
+	req.RequestURI = uri
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusOK, rr)
+	// now set the user token, it is not valid for the share
+	req, err = http.NewRequest(http.MethodGet, uri, nil)
+	assert.NoError(t, err)
+	req.RequestURI = uri
+	setJWTCookieForReq(req, token)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusFound, rr)
+	// get a share token
+	form := make(url.Values)
+	form.Set("share_password", defaultPassword)
+	loginURI := path.Join(webClientPubSharesPath, shareReadID, "login")
+	req, err = http.NewRequest(http.MethodPost, loginURI, bytes.NewBuffer([]byte(form.Encode())))
+	assert.NoError(t, err)
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusOK, rr)
+	assert.Contains(t, rr.Body.String(), "unable to verify form token")
+	// set the CSRF token
+	csrfToken, err := getCSRFToken(httpBaseURL + webClientLoginPath)
+	assert.NoError(t, err)
+	form.Set(csrfFormToken, csrfToken)
+	req, err = http.NewRequest(http.MethodPost, loginURI, bytes.NewBuffer([]byte(form.Encode())))
+	assert.NoError(t, err)
+	req.RemoteAddr = defaultRemoteAddr
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusOK, rr)
+	assert.Contains(t, rr.Body.String(), "Share login successful")
+	cookie := rr.Header().Get("Set-Cookie")
+	cookie = strings.TrimPrefix(cookie, "jwt=")
+	assert.NotEmpty(t, cookie)
+	req, err = http.NewRequest(http.MethodGet, uri, nil)
+	assert.NoError(t, err)
+	req.RequestURI = uri
+	setJWTCookieForReq(req, cookie)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusOK, rr)
+	// the same cookie will not work for the other share
+	req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, shareWriteID, "browse"), nil)
+	assert.NoError(t, err)
+	req.RequestURI = uri
+	setJWTCookieForReq(req, cookie)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusFound, rr)
+	// IP address does not match
+	req, err = http.NewRequest(http.MethodGet, uri, nil)
+	assert.NoError(t, err)
+	req.RequestURI = uri
+	setJWTCookieForReq(req, cookie)
+	req.RemoteAddr = "1.2.3.4:1234"
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusFound, rr)
+	// try to login with invalid credentials
+	form.Set("share_password", "")
+	req, err = http.NewRequest(http.MethodPost, loginURI, bytes.NewBuffer([]byte(form.Encode())))
+	assert.NoError(t, err)
+	req.RemoteAddr = defaultRemoteAddr
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusOK, rr)
+	assert.Contains(t, rr.Body.String(), dataprovider.ErrInvalidCredentials.Error())
+	// login with the next param set
+	form.Set("share_password", defaultPassword)
+	nextURI := path.Join(webClientPubSharesPath, shareReadID, "browse")
+	loginURI = path.Join(webClientPubSharesPath, shareReadID, fmt.Sprintf("login?next=%s", url.QueryEscape(nextURI)))
+	req, err = http.NewRequest(http.MethodPost, loginURI, bytes.NewBuffer([]byte(form.Encode())))
+	assert.NoError(t, err)
+	req.RemoteAddr = defaultRemoteAddr
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusFound, rr)
+	assert.Equal(t, nextURI, rr.Header().Get("Location"))
+	// try to login to a missing share
+	loginURI = path.Join(webClientPubSharesPath, "missing", "login")
+	req, err = http.NewRequest(http.MethodPost, loginURI, bytes.NewBuffer([]byte(form.Encode())))
+	assert.NoError(t, err)
+	req.RemoteAddr = defaultRemoteAddr
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusOK, rr)
+	assert.Contains(t, rr.Body.String(), dataprovider.ErrInvalidCredentials.Error())
+
+	_, err = httpdtest.RemoveUser(user, http.StatusOK)
+	assert.NoError(t, err)
+	err = os.RemoveAll(user.GetHomeDir())
+	assert.NoError(t, err)
+}
+
 func TestShareMaxSessions(t *testing.T) {
 func TestShareMaxSessions(t *testing.T) {
 	u := getTestUser()
 	u := getTestUser()
 	u.MaxSessions = 1
 	u.MaxSessions = 1
@@ -13716,13 +13865,10 @@ func TestUserAPIShares(t *testing.T) {
 
 
 	s, err := dataprovider.ShareExists(objectID, defaultUsername)
 	s, err := dataprovider.ShareExists(objectID, defaultUsername)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	match, err := s.CheckCredentials(defaultUsername, defaultPassword)
+	match, err := s.CheckCredentials(defaultPassword)
 	assert.True(t, match)
 	assert.True(t, match)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	match, err = s.CheckCredentials(defaultUsername, defaultPassword+"mod")
-	assert.False(t, match)
-	assert.Error(t, err)
-	match, err = s.CheckCredentials(altAdminUsername, defaultPassword)
+	match, err = s.CheckCredentials(defaultPassword + "mod")
 	assert.False(t, match)
 	assert.False(t, match)
 	assert.Error(t, err)
 	assert.Error(t, err)
 
 
@@ -13737,10 +13883,10 @@ func TestUserAPIShares(t *testing.T) {
 
 
 	s, err = dataprovider.ShareExists(objectID, defaultUsername)
 	s, err = dataprovider.ShareExists(objectID, defaultUsername)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	match, err = s.CheckCredentials(defaultUsername, defaultPassword)
+	match, err = s.CheckCredentials(defaultPassword)
 	assert.True(t, match)
 	assert.True(t, match)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	match, err = s.CheckCredentials(defaultUsername, defaultPassword+"mod")
+	match, err = s.CheckCredentials(defaultPassword + "mod")
 	assert.False(t, match)
 	assert.False(t, match)
 	assert.Error(t, err)
 	assert.Error(t, err)
 
 
@@ -16605,7 +16751,7 @@ func TestWebUserShare(t *testing.T) {
 	// check the password
 	// check the password
 	s, err := dataprovider.ShareExists(share.ShareID, user.Username)
 	s, err := dataprovider.ShareExists(share.ShareID, user.Username)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	match, err := s.CheckCredentials(user.Username, defaultPassword)
+	match, err := s.CheckCredentials(defaultPassword)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	assert.True(t, match)
 	assert.True(t, match)
 
 

+ 56 - 0
internal/httpd/internal_test.go

@@ -1009,6 +1009,56 @@ func TestCSRFToken(t *testing.T) {
 	csrfTokenAuth = jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil)
 	csrfTokenAuth = jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil)
 }
 }
 
 
+func TestCreateShareCookieError(t *testing.T) {
+	username := "share_user"
+	pwd := "pwd"
+	user := &dataprovider.User{
+		BaseUser: sdk.BaseUser{
+			Username: username,
+			Password: pwd,
+			HomeDir:  filepath.Join(os.TempDir(), username),
+			Status:   1,
+			Permissions: map[string][]string{
+				"/": {dataprovider.PermAny},
+			},
+		},
+	}
+	err := dataprovider.AddUser(user, "", "", "")
+	assert.NoError(t, err)
+	share := &dataprovider.Share{
+		Name:     "test share cookie error",
+		ShareID:  util.GenerateUniqueID(),
+		Scope:    dataprovider.ShareScopeRead,
+		Password: pwd,
+		Paths:    []string{"/"},
+		Username: username,
+	}
+	err = dataprovider.AddShare(share, "", "", "")
+	assert.NoError(t, err)
+
+	server := httpdServer{
+		tokenAuth: jwtauth.New("TS256", util.GenerateRandomBytes(32), nil),
+	}
+	form := make(url.Values)
+	form.Set("share_password", pwd)
+	form.Set(csrfFormToken, createCSRFToken("127.0.0.1"))
+	rctx := chi.NewRouteContext()
+	rctx.URLParams.Add("id", share.ShareID)
+	rr := httptest.NewRecorder()
+	req, err := http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, share.ShareID, "login"),
+		bytes.NewBuffer([]byte(form.Encode())))
+	assert.NoError(t, err)
+	req.RemoteAddr = "127.0.0.1:2345"
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
+	server.handleClientShareLoginPost(rr, req)
+	assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
+	assert.Contains(t, rr.Body.String(), common.ErrInternalFailure.Error())
+
+	err = dataprovider.DeleteUser(username, "", "", "")
+	assert.NoError(t, err)
+}
+
 func TestCreateTokenError(t *testing.T) {
 func TestCreateTokenError(t *testing.T) {
 	server := httpdServer{
 	server := httpdServer{
 		tokenAuth: jwtauth.New("PS256", util.GenerateRandomBytes(32), nil),
 		tokenAuth: jwtauth.New("PS256", util.GenerateRandomBytes(32), nil),
@@ -1087,6 +1137,12 @@ func TestCreateTokenError(t *testing.T) {
 	_, err = getIPListEntryFromPostFields(req, dataprovider.IPListTypeAllowList)
 	_, err = getIPListEntryFromPostFields(req, dataprovider.IPListTypeAllowList)
 	assert.Error(t, err)
 	assert.Error(t, err)
 
 
+	req, _ = http.NewRequest(http.MethodPost, path.Join(webClientSharePath, "shareID", "login?a=a%C3%AO%GG"), bytes.NewBuffer([]byte(form.Encode())))
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	rr = httptest.NewRecorder()
+	server.handleClientShareLoginPost(rr, req)
+	assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
+
 	req, _ = http.NewRequest(http.MethodPost, webClientLoginPath+"?a=a%C3%AO%GG", bytes.NewBuffer([]byte(form.Encode())))
 	req, _ = http.NewRequest(http.MethodPost, webClientLoginPath+"?a=a%C3%AO%GG", bytes.NewBuffer([]byte(form.Encode())))
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()

+ 2 - 2
internal/httpd/middleware.go

@@ -86,14 +86,14 @@ func validateJWTToken(w http.ResponseWriter, r *http.Request, audience tokenAudi
 		return err
 		return err
 	}
 	}
 	if !util.Contains(token.Audience(), audience) {
 	if !util.Contains(token.Audience(), audience) {
-		logger.Debug(logSender, "", "the token is not valid for audience %#v", audience)
+		logger.Debug(logSender, "", "the token is not valid for audience %q", audience)
 		doRedirect("Your token audience is not valid", nil)
 		doRedirect("Your token audience is not valid", nil)
 		return errInvalidToken
 		return errInvalidToken
 	}
 	}
 	if tokenValidationMode != tokenValidationNoIPMatch {
 	if tokenValidationMode != tokenValidationNoIPMatch {
 		ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 		ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 		if !util.Contains(token.Audience(), ipAddr) {
 		if !util.Contains(token.Audience(), ipAddr) {
-			logger.Debug(logSender, "", "the token with id %#v is not valid for the ip address %#v", token.JwtID(), ipAddr)
+			logger.Debug(logSender, "", "the token with id %q is not valid for the ip address %q", token.JwtID(), ipAddr)
 			doRedirect("Your token is not valid", nil)
 			doRedirect("Your token is not valid", nil)
 			return errInvalidToken
 			return errInvalidToken
 		}
 		}

+ 2 - 0
internal/httpd/server.go

@@ -1447,6 +1447,8 @@ func (s *httpdServer) setupWebClientRoutes() {
 				Post(webClientTwoFactorRecoveryPath, s.handleWebClientTwoFactorRecoveryPost)
 				Post(webClientTwoFactorRecoveryPath, s.handleWebClientTwoFactorRecoveryPost)
 		}
 		}
 		// share routes available to external users
 		// share routes available to external users
+		s.router.Get(webClientPubSharesPath+"/{id}/login", s.handleClientShareLoginGet)
+		s.router.Post(webClientPubSharesPath+"/{id}/login", s.handleClientShareLoginPost)
 		s.router.Get(webClientPubSharesPath+"/{id}", s.downloadFromShare)
 		s.router.Get(webClientPubSharesPath+"/{id}", s.downloadFromShare)
 		s.router.Get(webClientPubSharesPath+"/{id}/partial", s.handleClientSharePartialDownload)
 		s.router.Get(webClientPubSharesPath+"/{id}/partial", s.handleClientSharePartialDownload)
 		s.router.Get(webClientPubSharesPath+"/{id}/browse", s.handleShareGetFiles)
 		s.router.Get(webClientPubSharesPath+"/{id}/browse", s.handleShareGetFiles)

+ 76 - 4
internal/httpd/webclient.go

@@ -60,6 +60,7 @@ const (
 	templateClientShare             = "share.html"
 	templateClientShare             = "share.html"
 	templateClientShares            = "shares.html"
 	templateClientShares            = "shares.html"
 	templateClientViewPDF           = "viewpdf.html"
 	templateClientViewPDF           = "viewpdf.html"
+	templateShareLogin              = "sharelogin.html"
 	templateShareFiles              = "sharefiles.html"
 	templateShareFiles              = "sharefiles.html"
 	templateUploadToShare           = "shareupload.html"
 	templateUploadToShare           = "shareupload.html"
 	pageClientFilesTitle            = "My Files"
 	pageClientFilesTitle            = "My Files"
@@ -156,6 +157,15 @@ type filesPage struct {
 	HasIntegrations bool
 	HasIntegrations bool
 }
 }
 
 
+type shareLoginPage struct {
+	CurrentURL string
+	Version    string
+	Error      string
+	CSRFToken  string
+	StaticURL  string
+	Branding   UIBranding
+}
+
 type shareFilesPage struct {
 type shareFilesPage struct {
 	baseClientPage
 	baseClientPage
 	CurrentDir    string
 	CurrentDir    string
@@ -298,6 +308,11 @@ func loadClientTemplates(templatesPath string) {
 		filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
 		filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
 		filepath.Join(templatesPath, templateClientDir, templateClientViewPDF),
 		filepath.Join(templatesPath, templateClientDir, templateClientViewPDF),
 	}
 	}
+	shareLoginPath := []string{
+		filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
+		filepath.Join(templatesPath, templateClientDir, templateClientBaseLogin),
+		filepath.Join(templatesPath, templateClientDir, templateShareLogin),
+	}
 	shareFilesPath := []string{
 	shareFilesPath := []string{
 		filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
 		filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
 		filepath.Join(templatesPath, templateClientDir, templateClientBase),
 		filepath.Join(templatesPath, templateClientDir, templateClientBase),
@@ -318,6 +333,7 @@ func loadClientTemplates(templatesPath string) {
 	twoFactorTmpl := util.LoadTemplate(nil, twoFactorPath...)
 	twoFactorTmpl := util.LoadTemplate(nil, twoFactorPath...)
 	twoFactorRecoveryTmpl := util.LoadTemplate(nil, twoFactorRecoveryPath...)
 	twoFactorRecoveryTmpl := util.LoadTemplate(nil, twoFactorRecoveryPath...)
 	editFileTmpl := util.LoadTemplate(nil, editFilePath...)
 	editFileTmpl := util.LoadTemplate(nil, editFilePath...)
+	shareLoginTmpl := util.LoadTemplate(nil, shareLoginPath...)
 	sharesTmpl := util.LoadTemplate(nil, sharesPaths...)
 	sharesTmpl := util.LoadTemplate(nil, sharesPaths...)
 	shareTmpl := util.LoadTemplate(nil, sharePaths...)
 	shareTmpl := util.LoadTemplate(nil, sharePaths...)
 	forgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...)
 	forgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...)
@@ -340,6 +356,7 @@ func loadClientTemplates(templatesPath string) {
 	clientTemplates[templateForgotPassword] = forgotPwdTmpl
 	clientTemplates[templateForgotPassword] = forgotPwdTmpl
 	clientTemplates[templateResetPassword] = resetPwdTmpl
 	clientTemplates[templateResetPassword] = resetPwdTmpl
 	clientTemplates[templateClientViewPDF] = viewPDFTmpl
 	clientTemplates[templateClientViewPDF] = viewPDFTmpl
+	clientTemplates[templateShareLogin] = shareLoginTmpl
 	clientTemplates[templateShareFiles] = shareFilesTmpl
 	clientTemplates[templateShareFiles] = shareFilesTmpl
 	clientTemplates[templateUploadToShare] = shareUploadTmpl
 	clientTemplates[templateUploadToShare] = shareUploadTmpl
 }
 }
@@ -397,6 +414,18 @@ func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, error, ip
 	renderClientTemplate(w, templateResetPassword, data)
 	renderClientTemplate(w, templateResetPassword, data)
 }
 }
 
 
+func (s *httpdServer) renderShareLoginPage(w http.ResponseWriter, currentURL, error, ip string) {
+	data := shareLoginPage{
+		CurrentURL: currentURL,
+		Version:    version.Get().Version,
+		Error:      error,
+		CSRFToken:  createCSRFToken(ip),
+		StaticURL:  webStaticFilesPath,
+		Branding:   s.binding.Branding.WebClient,
+	}
+	renderClientTemplate(w, templateShareLogin, data)
+}
+
 func renderClientTemplate(w http.ResponseWriter, tmplName string, data any) {
 func renderClientTemplate(w http.ResponseWriter, tmplName string, data any) {
 	err := clientTemplates[tmplName].ExecuteTemplate(w, tmplName, data)
 	err := clientTemplates[tmplName].ExecuteTemplate(w, tmplName, data)
 	if err != nil {
 	if err != nil {
@@ -663,7 +692,7 @@ func (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http.
 func (s *httpdServer) handleClientSharePartialDownload(w http.ResponseWriter, r *http.Request) {
 func (s *httpdServer) handleClientSharePartialDownload(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
-	share, connection, err := s.checkPublicShare(w, r, validScopes, true)
+	share, connection, err := s.checkPublicShare(w, r, validScopes)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -706,7 +735,7 @@ func (s *httpdServer) handleClientSharePartialDownload(w http.ResponseWriter, r
 func (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.Request) {
 func (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
-	share, connection, err := s.checkPublicShare(w, r, validScopes, true)
+	share, connection, err := s.checkPublicShare(w, r, validScopes)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -757,7 +786,7 @@ func (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.R
 func (s *httpdServer) handleClientUploadToShare(w http.ResponseWriter, r *http.Request) {
 func (s *httpdServer) handleClientUploadToShare(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite}
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite}
-	share, _, err := s.checkPublicShare(w, r, validScopes, true)
+	share, _, err := s.checkPublicShare(w, r, validScopes)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -771,7 +800,7 @@ func (s *httpdServer) handleClientUploadToShare(w http.ResponseWriter, r *http.R
 func (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request) {
 func (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
 	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
-	share, connection, err := s.checkPublicShare(w, r, validScopes, true)
+	share, connection, err := s.checkPublicShare(w, r, validScopes)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -1427,3 +1456,46 @@ func (s *httpdServer) handleClientGetPDF(w http.ResponseWriter, r *http.Request)
 	}
 	}
 	downloadFile(w, r, connection, name, info, true, nil) //nolint:errcheck
 	downloadFile(w, r, connection, name, info, true, nil) //nolint:errcheck
 }
 }
+
+func (s *httpdServer) handleClientShareLoginGet(w http.ResponseWriter, r *http.Request) {
+	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
+	s.renderShareLoginPage(w, r.RequestURI, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
+}
+
+func (s *httpdServer) handleClientShareLoginPost(w http.ResponseWriter, r *http.Request) {
+	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
+	if err := r.ParseForm(); err != nil {
+		s.renderShareLoginPage(w, r.RequestURI, err.Error(), ipAddr)
+		return
+	}
+	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
+		s.renderShareLoginPage(w, r.RequestURI, err.Error(), ipAddr)
+		return
+	}
+	shareID := getURLParam(r, "id")
+	share, err := dataprovider.ShareExists(shareID, "")
+	if err != nil {
+		s.renderShareLoginPage(w, r.RequestURI, dataprovider.ErrInvalidCredentials.Error(), ipAddr)
+		return
+	}
+	match, err := share.CheckCredentials(r.Form.Get("share_password"))
+	if !match || err != nil {
+		s.renderShareLoginPage(w, r.RequestURI, dataprovider.ErrInvalidCredentials.Error(), ipAddr)
+		return
+	}
+	c := jwtTokenClaims{
+		Username: shareID,
+	}
+	err = c.createAndSetCookie(w, r, s.tokenAuth, tokenAudienceWebShare, ipAddr)
+	if err != nil {
+		s.renderShareLoginPage(w, r.RequestURI, common.ErrInternalFailure.Error(), ipAddr)
+		return
+	}
+	next := r.URL.Query().Get("next")
+	if strings.HasPrefix(next, path.Join(webClientPubSharesPath, share.ShareID)) {
+		http.Redirect(w, r, next, http.StatusFound)
+	}
+	s.renderClientMessagePage(w, r, "Share Login OK", "Share login successful, you can now use your link",
+		http.StatusOK, nil, "")
+}

+ 5 - 2
templates/webclient/login.html

@@ -19,8 +19,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 
 
 {{define "content"}}
 {{define "content"}}
                                     {{if .Error}}
                                     {{if .Error}}
-                                    <div class="card mb-4 border-left-warning">
-                                        <div class="card-body text-form-error">{{.Error}}</div>
+                                    <div class="alert alert-warning alert-dismissible fade show" role="alert">
+                                        {{.Error}}
+                                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+                                            <span aria-hidden="true">&times;</span>
+                                        </button>
                                     </div>
                                     </div>
                                     {{end}}
                                     {{end}}
                                     <form id="login_form" action="{{.CurrentURL}}" method="POST"
                                     <form id="login_form" action="{{.CurrentURL}}" method="POST"

+ 40 - 0
templates/webclient/sharelogin.html

@@ -0,0 +1,40 @@
+<!--
+Copyright (C) 2019-2023 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 <https://www.gnu.org/licenses/>.
+-->
+{{template "baselogin" .}}
+
+{{define "title"}}Share login{{end}}
+
+{{define "content"}}
+                                    {{if .Error}}
+                                    <div class="alert alert-warning alert-dismissible fade show" role="alert">
+                                        {{.Error}}
+                                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+                                            <span aria-hidden="true">&times;</span>
+                                        </button>
+                                    </div>
+                                    {{end}}
+                                    <form id="login_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"
+                                        class="user-custom">
+                                        <div class="form-group">
+                                            <input type="password" class="form-control form-control-user-custom"
+                                                id="inputSharePassword" name="share_password" placeholder="Password" spellcheck="false" required>
+                                        </div>
+                                        <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
+                                        <button type="submit" class="btn btn-primary btn-user-custom btn-block">
+                                            Login
+                                        </button>
+                                    </form>
+{{end}}

+ 0 - 17
templates/webclient/shares.html

@@ -112,19 +112,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
                 </div>
                 </div>
                 <div id="writeShare">
                 <div id="writeShare">
                     <p>You can upload one or more files to the shared directory using this <a id="writePageLink" href="#" target="_blank">page</a></p>
                     <p>You can upload one or more files to the shared directory using this <a id="writePageLink" href="#" target="_blank">page</a></p>
-                    <p>
-                        <a data-toggle="collapse" href="#collapseWriteShareAdvanced" aria-expanded="false" aria-controls="collapseWriteShareAdvanced">
-                            Advanced options
-                        </a>
-                    </p>
-                    <div class="collapse" id="collapseWriteShareAdvanced">
-                        <div class="card card-body">
-                            <p>You can upload one or more files to the shared directory by sending a multipart/form-data request to this <a id="writeLink" href="#" target="_blank">link</a>. The form field name for the file(s) is <b><code>filenames</code></b>.</p>
-                            <p>Example: <code>curl -F [email protected] -F [email protected] "share link"</code></p>
-                            <p>Or you can upload files one by one by adding the path encoded file name to the share <a id="writeLinkSingle" href="#" target="_blank">link</a> and sending the file as POST body. The optional <b><code>X-SFTPGO-MTIME</code></b> header allows to set the file modification time as milliseconds since epoch.</p>
-                            <p>Example: <code>curl --data-binary @file.txt -H "Content-Type: application/octet-stream" -H "X-SFTPGO-MTIME: 1638882991234" "share link/file.txt"</code></p>
-                        </div>
-                    </div>
                 </div>
                 </div>
                 <div id="expiredShare">
                 <div id="expiredShare">
                     This share is no longer accessible because it has expired
                     This share is no longer accessible because it has expired
@@ -249,10 +236,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
                         $('#readShare').hide();
                         $('#readShare').hide();
                         $('#writePageLink').attr("href", shareURL+"/upload");
                         $('#writePageLink').attr("href", shareURL+"/upload");
                         $('#writePageLink').attr("title", shareURL+"/upload");
                         $('#writePageLink').attr("title", shareURL+"/upload");
-                        $('#writeLink').attr("href", shareURL);
-                        $('#writeLink').attr("title", shareURL);
-                        $('#writeLinkSingle').attr("href", shareURL);
-                        $('#writeLinkSingle').attr("title", shareURL);
                     }
                     }
                 }
                 }
                 $('#linkModal').modal('show');
                 $('#linkModal').modal('show');

+ 2 - 2
templates/webclient/shareupload.html

@@ -22,8 +22,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
     <div class="col-xl-5 col-lg-6 col-md-8">
     <div class="col-xl-5 col-lg-6 col-md-8">
         <div class="card shadow-lg my-5">
         <div class="card shadow-lg my-5">
             <div class="card-header py-3">
             <div class="card-header py-3">
-                <h6 id="default_title" class="m-0 font-weight-bold text-primary">Upload one or more files to share "{{.Share.Name}}", user "{{.Share.Username}}"</h6>
-                <h6 id="success_title" class="m-0 font-weight-bold text-primary" style="display: none;">Upload completed to share "{{.Share.Name}}", user "{{.Share.Username}}"</h6>
+                <h6 id="default_title" class="m-0 font-weight-bold text-primary">Upload one or more files to share "{{.Share.Name}}"</h6>
+                <h6 id="success_title" class="m-0 font-weight-bold text-primary" style="display: none;">Upload completed to share "{{.Share.Name}}"</h6>
             </div>
             </div>
             <div class="card-body">
             <div class="card-body">
                 <div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
                 <div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">