Browse Source

WebClient share: add a download page

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 1 year ago
parent
commit
1a765c7ff7

+ 4 - 4
go.mod

@@ -10,10 +10,10 @@ require (
 	github.com/alexedwards/argon2id v1.0.0
 	github.com/alexedwards/argon2id v1.0.0
 	github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
 	github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
 	github.com/aws/aws-sdk-go-v2 v1.23.0
 	github.com/aws/aws-sdk-go-v2 v1.23.0
-	github.com/aws/aws-sdk-go-v2/config v1.25.1
+	github.com/aws/aws-sdk-go-v2/config v1.25.2
 	github.com/aws/aws-sdk-go-v2/credentials v1.16.1
 	github.com/aws/aws-sdk-go-v2/credentials v1.16.1
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4
-	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.13.8
+	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.13.9
 	github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.18.2
 	github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.18.2
 	github.com/aws/aws-sdk-go-v2/service/s3 v1.42.2
 	github.com/aws/aws-sdk-go-v2/service/s3 v1.42.2
 	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.23.2
 	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.23.2
@@ -74,7 +74,7 @@ require (
 	golang.org/x/sys v0.14.0
 	golang.org/x/sys v0.14.0
 	golang.org/x/term v0.14.0
 	golang.org/x/term v0.14.0
 	golang.org/x/time v0.4.0
 	golang.org/x/time v0.4.0
-	google.golang.org/api v0.150.0
+	google.golang.org/api v0.151.0
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 )
 )
 
 
@@ -88,7 +88,7 @@ require (
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1 // indirect
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/ini v1.7.0 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.3 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.3 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.3 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.3 // indirect

+ 8 - 8
go.sum

@@ -75,20 +75,20 @@ github.com/aws/aws-sdk-go-v2 v1.23.0 h1:PiHAzmiQQr6JULBUdvR8fKlA+UPKLT/8KbiqpFBW
 github.com/aws/aws-sdk-go-v2 v1.23.0/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA=
 github.com/aws/aws-sdk-go-v2 v1.23.0/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1 h1:ZY3108YtBNq96jNZTICHxN1gSBSbnvIdYwwqnvCV4Mc=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1 h1:ZY3108YtBNq96jNZTICHxN1gSBSbnvIdYwwqnvCV4Mc=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1/go.mod h1:t8PYl/6LzdAqsU4/9tz28V/kU+asFePvpOMkdul0gEQ=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1/go.mod h1:t8PYl/6LzdAqsU4/9tz28V/kU+asFePvpOMkdul0gEQ=
-github.com/aws/aws-sdk-go-v2/config v1.25.1 h1:YsjngBOl2mx4l3egkVWndr6/6TqtkdsWJFZIsQ924Ek=
-github.com/aws/aws-sdk-go-v2/config v1.25.1/go.mod h1:yV6h7TRVzhdIFmUk9WWDRpWwYGg1woEzKr0k1IYz2Tk=
+github.com/aws/aws-sdk-go-v2/config v1.25.2 h1:+Gy7Xe372Tw/PiUw3We94Le9IwU1tmJqCD6cvI4oBJM=
+github.com/aws/aws-sdk-go-v2/config v1.25.2/go.mod h1:6hFlwWQiVOUG0Ej2ql0tG4zPlpDH++HD0WT1MA6l5Q4=
 github.com/aws/aws-sdk-go-v2/credentials v1.16.1 h1:WessyrdgyFN5TB+eLQdrFSlN/3oMnqukIFhDxK6z8h0=
 github.com/aws/aws-sdk-go-v2/credentials v1.16.1 h1:WessyrdgyFN5TB+eLQdrFSlN/3oMnqukIFhDxK6z8h0=
 github.com/aws/aws-sdk-go-v2/credentials v1.16.1/go.mod h1:RQJyPxKcr+m4ArlIG1LUhMOrjposVfzbX6H8oR6oCgE=
 github.com/aws/aws-sdk-go-v2/credentials v1.16.1/go.mod h1:RQJyPxKcr+m4ArlIG1LUhMOrjposVfzbX6H8oR6oCgE=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4 h1:9wKDWEjwSnXZre0/O3+ZwbBl1SmlgWYBbrTV10X/H1s=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4 h1:9wKDWEjwSnXZre0/O3+ZwbBl1SmlgWYBbrTV10X/H1s=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4/go.mod h1:t4i+yGHMCcUNIX1x7YVYa6bH/Do7civ5I6cG/6PMfyA=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4/go.mod h1:t4i+yGHMCcUNIX1x7YVYa6bH/Do7civ5I6cG/6PMfyA=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.13.8 h1:wuOjvalpd2CnXffks74Vq6n3yv9vunKCoy4R1sjStGk=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.13.8/go.mod h1:vywwjy6VnrR48Izg136JoSUXC4mH9QeUi3g0EH9DSrA=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.13.9 h1:yG01Big4R5CDxftieMlgZPcHKZbwkRygur4DMGTqSzg=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.13.9/go.mod h1:RV5gmgYb4psddWMPaf4giuGdsK1l0KwlXNFAbzWAIIo=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3 h1:DUwbD79T8gyQ23qVXFUthjzVMTviSHi3y4z58KvghhM=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3 h1:DUwbD79T8gyQ23qVXFUthjzVMTviSHi3y4z58KvghhM=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3 h1:AplLJCtIaUZDCbr6+gLYdsYNxne4iuaboJhVt9d+WXI=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3 h1:AplLJCtIaUZDCbr6+gLYdsYNxne4iuaboJhVt9d+WXI=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3/go.mod h1:ify42Rb7nKeDDPkFjKn7q1bPscVPu/+gmHH8d2c+anU=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3/go.mod h1:ify42Rb7nKeDDPkFjKn7q1bPscVPu/+gmHH8d2c+anU=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.7.0 h1:usgqiJtamuGIBj+OvYmMq89+Z1hIKkMJToz1WpoeNUY=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.7.0/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 h1:uR9lXYjdPX0xY+NhvaJ4dD8rpSRz5VY81ccIIoNG+lw=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
 github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.3 h1:lMwCXiWJlrtZot0NJTjbC8G9zl+V3i68gBTBBvDeEXA=
 github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.3 h1:lMwCXiWJlrtZot0NJTjbC8G9zl+V3i68gBTBBvDeEXA=
 github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.3/go.mod h1:5yzAuE9i2RkVAttBl8yxZgQr5OCq4D5yDnG7j9x2L0U=
 github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.3/go.mod h1:5yzAuE9i2RkVAttBl8yxZgQr5OCq4D5yDnG7j9x2L0U=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1 h1:rpkF4n0CyFcrJUG/rNNohoTmhtWlFTRI4BsZOh9PvLs=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1 h1:rpkF4n0CyFcrJUG/rNNohoTmhtWlFTRI4BsZOh9PvLs=
@@ -773,8 +773,8 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513
 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
 google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
 google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
 google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
 google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/api v0.150.0 h1:Z9k22qD289SZ8gCJrk4DrWXkNjtfvKAUo/l1ma8eBYE=
-google.golang.org/api v0.150.0/go.mod h1:ccy+MJ6nrYFgE3WgRx/AMXOxOmU8Q4hSa+jjibzhxcg=
+google.golang.org/api v0.151.0 h1:FhfXLO/NFdJIzQtCqjpysWwqKk8AzGWBUhMIx67cVDU=
+google.golang.org/api v0.151.0/go.mod h1:ccy+MJ6nrYFgE3WgRx/AMXOxOmU8Q4hSa+jjibzhxcg=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=

+ 14 - 0
internal/httpd/httpd_test.go

@@ -13745,6 +13745,20 @@ func TestWebClientShareCredentials(t *testing.T) {
 	setJWTCookieForReq(req, cookie)
 	setJWTCookieForReq(req, cookie)
 	rr = executeRequest(req)
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusOK, rr)
 	checkResponseCode(t, http.StatusOK, rr)
+	// get the download page
+	req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, shareReadID, "download?a=b"), nil)
+	assert.NoError(t, err)
+	req.RequestURI = uri
+	setJWTCookieForReq(req, cookie)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusOK, rr)
+	// get the download page for a missing share
+	req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, "invalidshareid", "download"), nil)
+	assert.NoError(t, err)
+	req.RequestURI = uri
+	setJWTCookieForReq(req, cookie)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusNotFound, rr)
 	// the same cookie will not work for the other share
 	// the same cookie will not work for the other share
 	req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, shareWriteID, "browse"), nil)
 	req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, shareWriteID, "browse"), nil)
 	assert.NoError(t, err)
 	assert.NoError(t, err)

+ 35 - 0
internal/httpd/internal_test.go

@@ -3476,6 +3476,41 @@ func TestUserQuotaUsage(t *testing.T) {
 	assert.True(t, usage.IsTransferQuotaLow())
 	assert.True(t, usage.IsTransferQuotaLow())
 }
 }
 
 
+func TestShareRedirectURL(t *testing.T) {
+	shareID := util.GenerateUniqueID()
+	base := path.Join(webClientPubSharesPath, shareID)
+	next := path.Join(webClientPubSharesPath, shareID, "browse")
+	ok, res := checkShareRedirectURL(next, base)
+	assert.True(t, ok)
+	assert.Equal(t, next, res)
+	next = path.Join(webClientPubSharesPath, shareID, "browse") + "?a=b"
+	ok, res = checkShareRedirectURL(next, base)
+	assert.True(t, ok)
+	assert.Equal(t, next, res)
+	next = path.Join(webClientPubSharesPath, shareID)
+	ok, res = checkShareRedirectURL(next, base)
+	assert.True(t, ok)
+	assert.Equal(t, path.Join(base, "download"), res)
+	next = path.Join(webClientEditFilePath, shareID)
+	ok, res = checkShareRedirectURL(next, base)
+	assert.False(t, ok)
+	assert.Empty(t, res)
+	next = path.Join(webClientPubSharesPath, shareID) + "?compress=false&a=b"
+	ok, res = checkShareRedirectURL(next, base)
+	assert.True(t, ok)
+	assert.Equal(t, path.Join(base, "download?compress=false&a=b"), res)
+	next = path.Join(webClientPubSharesPath, shareID) + "?compress=true&b=c"
+	ok, res = checkShareRedirectURL(next, base)
+	assert.True(t, ok)
+	assert.Equal(t, path.Join(base, "download?compress=true&b=c"), res)
+	ok, res = checkShareRedirectURL("http://foo\x7f.com/ab", "http://foo\x7f.com/")
+	assert.False(t, ok)
+	assert.Empty(t, res)
+	ok, res = checkShareRedirectURL("http://foo.com/?foo\nbar", "http://foo.com")
+	assert.False(t, ok)
+	assert.Empty(t, res)
+}
+
 func isSharedProviderSupported() bool {
 func isSharedProviderSupported() bool {
 	// SQLite shares the implementation with other SQL-based provider but it makes no sense
 	// SQLite shares the implementation with other SQL-based provider but it makes no sense
 	// to use it outside test cases
 	// to use it outside test cases

+ 1 - 0
internal/httpd/server.go

@@ -1524,6 +1524,7 @@ func (s *httpdServer) setupWebClientRoutes() {
 		s.router.Get(webClientPubSharesPath+"/{id}", s.downloadFromShare)
 		s.router.Get(webClientPubSharesPath+"/{id}", s.downloadFromShare)
 		s.router.Post(webClientPubSharesPath+"/{id}/partial", s.handleClientSharePartialDownload)
 		s.router.Post(webClientPubSharesPath+"/{id}/partial", s.handleClientSharePartialDownload)
 		s.router.Get(webClientPubSharesPath+"/{id}/browse", s.handleShareGetFiles)
 		s.router.Get(webClientPubSharesPath+"/{id}/browse", s.handleShareGetFiles)
+		s.router.Get(webClientPubSharesPath+"/{id}/download", s.handleClientSharedFile)
 		s.router.Get(webClientPubSharesPath+"/{id}/upload", s.handleClientUploadToShare)
 		s.router.Get(webClientPubSharesPath+"/{id}/upload", s.handleClientUploadToShare)
 		s.router.With(compressor.Handler).Get(webClientPubSharesPath+"/{id}/dirs", s.handleShareGetDirContents)
 		s.router.With(compressor.Handler).Get(webClientPubSharesPath+"/{id}/dirs", s.handleShareGetDirContents)
 		s.router.Post(webClientPubSharesPath+"/{id}", s.uploadFilesToShare)
 		s.router.Post(webClientPubSharesPath+"/{id}", s.uploadFilesToShare)

+ 62 - 2
internal/httpd/webclient.go

@@ -63,6 +63,7 @@ const (
 	templateClientShares            = "shares.html"
 	templateClientShares            = "shares.html"
 	templateClientViewPDF           = "viewpdf.html"
 	templateClientViewPDF           = "viewpdf.html"
 	templateShareLogin              = "sharelogin.html"
 	templateShareLogin              = "sharelogin.html"
+	templateShareDownload           = "sharedownload.html"
 	templateUploadToShare           = "shareupload.html"
 	templateUploadToShare           = "shareupload.html"
 	pageClientFilesTitle            = "Files"
 	pageClientFilesTitle            = "Files"
 	pageClientSharesTitle           = "Shares"
 	pageClientSharesTitle           = "Shares"
@@ -74,6 +75,7 @@ const (
 	pageClientResetPwdTitle         = "SFTPGo WebClient - Reset password"
 	pageClientResetPwdTitle         = "SFTPGo WebClient - Reset password"
 	pageExtShareTitle               = "Shared files"
 	pageExtShareTitle               = "Shared files"
 	pageUploadToShareTitle          = "Upload to share"
 	pageUploadToShareTitle          = "Upload to share"
+	pageDownloadFromShareTitle      = "Download shared file"
 )
 )
 
 
 // condResult is the result of an HTTP request precondition check.
 // condResult is the result of an HTTP request precondition check.
@@ -174,6 +176,11 @@ type shareLoginPage struct {
 	Branding   UIBranding
 	Branding   UIBranding
 }
 }
 
 
+type shareDownloadPage struct {
+	baseClientPage
+	DownloadLink string
+}
+
 type shareUploadPage struct {
 type shareUploadPage struct {
 	baseClientPage
 	baseClientPage
 	Share          *dataprovider.Share
 	Share          *dataprovider.Share
@@ -495,6 +502,11 @@ func loadClientTemplates(templatesPath string) {
 		filepath.Join(templatesPath, templateClientDir, templateClientBase),
 		filepath.Join(templatesPath, templateClientDir, templateClientBase),
 		filepath.Join(templatesPath, templateClientDir, templateUploadToShare),
 		filepath.Join(templatesPath, templateClientDir, templateUploadToShare),
 	}
 	}
+	shareDownloadPath := []string{
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
+		filepath.Join(templatesPath, templateClientDir, templateClientBase),
+		filepath.Join(templatesPath, templateClientDir, templateShareDownload),
+	}
 
 
 	filesTmpl := util.LoadTemplate(nil, filesPaths...)
 	filesTmpl := util.LoadTemplate(nil, filesPaths...)
 	profileTmpl := util.LoadTemplate(nil, profilePaths...)
 	profileTmpl := util.LoadTemplate(nil, profilePaths...)
@@ -512,6 +524,7 @@ func loadClientTemplates(templatesPath string) {
 	resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)
 	resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)
 	viewPDFTmpl := util.LoadTemplate(nil, viewPDFPaths...)
 	viewPDFTmpl := util.LoadTemplate(nil, viewPDFPaths...)
 	shareUploadTmpl := util.LoadTemplate(nil, shareUploadPath...)
 	shareUploadTmpl := util.LoadTemplate(nil, shareUploadPath...)
+	shareDownloadTmpl := util.LoadTemplate(nil, shareDownloadPath...)
 
 
 	clientTemplates[templateClientFiles] = filesTmpl
 	clientTemplates[templateClientFiles] = filesTmpl
 	clientTemplates[templateClientProfile] = profileTmpl
 	clientTemplates[templateClientProfile] = profileTmpl
@@ -529,6 +542,7 @@ func loadClientTemplates(templatesPath string) {
 	clientTemplates[templateClientViewPDF] = viewPDFTmpl
 	clientTemplates[templateClientViewPDF] = viewPDFTmpl
 	clientTemplates[templateShareLogin] = shareLoginTmpl
 	clientTemplates[templateShareLogin] = shareLoginTmpl
 	clientTemplates[templateUploadToShare] = shareUploadTmpl
 	clientTemplates[templateUploadToShare] = shareUploadTmpl
+	clientTemplates[templateShareDownload] = shareDownloadTmpl
 }
 }
 
 
 func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage {
 func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage {
@@ -780,6 +794,14 @@ func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Reque
 	renderClientTemplate(w, templateClientFiles, data)
 	renderClientTemplate(w, templateClientFiles, data)
 }
 }
 
 
+func (s *httpdServer) renderShareDownloadPage(w http.ResponseWriter, r *http.Request, downloadLink string) {
+	data := shareDownloadPage{
+		baseClientPage: s.getBaseClientPageData(pageDownloadFromShareTitle, "", r),
+		DownloadLink:   downloadLink,
+	}
+	renderClientTemplate(w, templateShareDownload, data)
+}
+
 func (s *httpdServer) renderUploadToSharePage(w http.ResponseWriter, r *http.Request, share dataprovider.Share) {
 func (s *httpdServer) renderUploadToSharePage(w http.ResponseWriter, r *http.Request, share dataprovider.Share) {
 	currentURL := path.Join(webClientPubSharesPath, share.ShareID, "upload")
 	currentURL := path.Join(webClientPubSharesPath, share.ShareID, "upload")
 	data := shareUploadPage{
 	data := shareUploadPage{
@@ -1799,15 +1821,53 @@ func (s *httpdServer) handleClientShareLoginPost(w http.ResponseWriter, r *http.
 		return
 		return
 	}
 	}
 	next := path.Clean(r.URL.Query().Get("next"))
 	next := path.Clean(r.URL.Query().Get("next"))
-	if strings.HasPrefix(next, path.Join(webClientPubSharesPath, share.ShareID)) {
-		http.Redirect(w, r, next, http.StatusFound)
+	baseShareURL := path.Join(webClientPubSharesPath, share.ShareID)
+	isRedirect, redirectTo := checkShareRedirectURL(next, baseShareURL)
+	if isRedirect {
+		http.Redirect(w, r, redirectTo, http.StatusFound)
 		return
 		return
 	}
 	}
 	s.renderClientMessagePage(w, r, "Share Login OK", "Share login successful, you can now use your link",
 	s.renderClientMessagePage(w, r, "Share Login OK", "Share login successful, you can now use your link",
 		http.StatusOK, nil, "")
 		http.StatusOK, nil, "")
 }
 }
 
 
+func (s *httpdServer) handleClientSharedFile(w http.ResponseWriter, r *http.Request) {
+	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+	validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead}
+	share, _, err := s.checkPublicShare(w, r, validScopes)
+	if err != nil {
+		return
+	}
+	query := ""
+	if r.URL.RawQuery != "" {
+		query = "?" + r.URL.RawQuery
+	}
+	s.renderShareDownloadPage(w, r, path.Join(webClientPubSharesPath, share.ShareID)+query)
+}
+
 func (s *httpdServer) handleClientPing(w http.ResponseWriter, r *http.Request) {
 func (s *httpdServer) handleClientPing(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	render.PlainText(w, r, "PONG")
 	render.PlainText(w, r, "PONG")
 }
 }
+
+func checkShareRedirectURL(next, base string) (bool, string) {
+	if !strings.HasPrefix(next, base) {
+		return false, ""
+	}
+	if next == base {
+		return true, path.Join(next, "download")
+	}
+	baseURL, err := url.Parse(base)
+	if err != nil {
+		return false, ""
+	}
+	nextURL, err := url.Parse(next)
+	if err != nil {
+		return false, ""
+	}
+	if nextURL.Path == baseURL.Path {
+		redirectURL := nextURL.JoinPath("download")
+		return true, redirectURL.String()
+	}
+	return true, next
+}

+ 0 - 1
templates/webclient/baselogin.html

@@ -53,7 +53,6 @@ explicit grant from the SFTPGo Team ([email protected]).
 					let submitButton = document.querySelector('#sign_in_submit');
 					let submitButton = document.querySelector('#sign_in_submit');
 					submitButton.setAttribute('data-kt-indicator', 'on');
 					submitButton.setAttribute('data-kt-indicator', 'on');
 					submitButton.disabled = true;
 					submitButton.disabled = true;
-					return true;
 				});
 				});
 			});
 			});
 		</script>
 		</script>

+ 0 - 1
templates/webclient/profile.html

@@ -148,7 +148,6 @@ explicit grant from the SFTPGo Team ([email protected]).
             let submitButton = document.querySelector('#form_submit');
             let submitButton = document.querySelector('#form_submit');
             submitButton.setAttribute('data-kt-indicator', 'on');
             submitButton.setAttribute('data-kt-indicator', 'on');
             submitButton.disabled = true;
             submitButton.disabled = true;
-            return true;
         });
         });
     });
     });
 </script>
 </script>

+ 0 - 1
templates/webclient/share.html

@@ -220,7 +220,6 @@ explicit grant from the SFTPGo Team ([email protected]).
                 let submitButton = document.querySelector('#form_submit');
                 let submitButton = document.querySelector('#form_submit');
                 submitButton.setAttribute('data-kt-indicator', 'on');
                 submitButton.setAttribute('data-kt-indicator', 'on');
                 submitButton.disabled = true;
                 submitButton.disabled = true;
-                return true;
             });
             });
         });
         });
 </script>
 </script>

+ 39 - 0
templates/webclient/sharedownload.html

@@ -0,0 +1,39 @@
+<!--
+Copyright (C) 2023 Nicola Murino
+
+This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
+
+https://keenthemes.com/products/templates-mega-bundle
+
+KeenThemes HTML/CSS/JS components are allowed for use only within the
+SFTPGo product and restricted to be used in a resealable HTML template
+that can compete with KeenThemes products anyhow.
+
+This WebUI is allowed for use only within the SFTPGo product and
+therefore cannot be used in derivative works/products without an
+explicit grant from the SFTPGo Team ([email protected]).
+-->
+{{template "base" .}}
+{{- define "title"}}{{.Title}}{{- end}}
+{{- define "page_body"}}
+<div class="d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20">
+    <div class="mb-12">
+        <span>
+            <img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px h-md-90px h-lg-100px" />
+        </span>
+        <span class="text-gray-900 fs-1 fw-bold ms-3 ps-5">
+		    {{.Branding.ShortName}}
+        </span>
+    </div>
+    <div class="card shadow-sm w-lg-600px">
+        <div class="card-header bg-light">
+            <h3 class="card-title text-primary">Your download is ready</h3>
+        </div>
+        <div class="card-body">
+            <div>
+                <a href="{{.DownloadLink}}" class="btn btn-primary btn-user-custom btn-block">Download</a>
+            </div>
+        </div>
+    </div>
+</div>
+{{- end}}

+ 4 - 4
templates/webclient/shares.html

@@ -171,10 +171,10 @@ explicit grant from the SFTPGo Team ([email protected]).
                 $('#expiredShare').hide();
                 $('#expiredShare').hide();
                 $('#writeShare').hide();
                 $('#writeShare').hide();
                 $('#readShare').show();
                 $('#readShare').show();
-                $('#readLink').attr("href", shareURL);
-                $('#readLink').attr("title", shareURL);
-                $('#readUncompressedLink').attr("href", shareURL + "?compress=false");
-                $('#readUncompressedLink').attr("title", shareURL + "?compress=false");
+                $('#readLink').attr("href", shareURL + "/download");
+                $('#readLink').attr("title", shareURL + "/download");
+                $('#readUncompressedLink').attr("href", shareURL + "/download?compress=false");
+                $('#readUncompressedLink').attr("title", shareURL + "/download?compress=false");
                 $('#readBrowseLink').attr("href", shareURL + "/browse");
                 $('#readBrowseLink').attr("href", shareURL + "/browse");
                 $('#readBrowseLink').attr("title", shareURL + "/browse");
                 $('#readBrowseLink').attr("title", shareURL + "/browse");
             } else {
             } else {