瀏覽代碼

WebClient share: add a download page

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino 1 年之前
父節點
當前提交
1a765c7ff7

+ 4 - 4
go.mod

@@ -10,10 +10,10 @@ require (
 	github.com/alexedwards/argon2id v1.0.0
 	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/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/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/s3 v1.42.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/term v0.14.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
 )
 
@@ -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/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/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/service/internal/accept-encoding v1.10.1 // 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/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/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/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/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/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/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/go.mod h1:5yzAuE9i2RkVAttBl8yxZgQr5OCq4D5yDnG7j9x2L0U=
 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.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
 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.4.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)
 	rr = executeRequest(req)
 	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
 	req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, shareWriteID, "browse"), nil)
 	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())
 }
 
+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 {
 	// SQLite shares the implementation with other SQL-based provider but it makes no sense
 	// 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.Post(webClientPubSharesPath+"/{id}/partial", s.handleClientSharePartialDownload)
 		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.With(compressor.Handler).Get(webClientPubSharesPath+"/{id}/dirs", s.handleShareGetDirContents)
 		s.router.Post(webClientPubSharesPath+"/{id}", s.uploadFilesToShare)

+ 62 - 2
internal/httpd/webclient.go

@@ -63,6 +63,7 @@ const (
 	templateClientShares            = "shares.html"
 	templateClientViewPDF           = "viewpdf.html"
 	templateShareLogin              = "sharelogin.html"
+	templateShareDownload           = "sharedownload.html"
 	templateUploadToShare           = "shareupload.html"
 	pageClientFilesTitle            = "Files"
 	pageClientSharesTitle           = "Shares"
@@ -74,6 +75,7 @@ const (
 	pageClientResetPwdTitle         = "SFTPGo WebClient - Reset password"
 	pageExtShareTitle               = "Shared files"
 	pageUploadToShareTitle          = "Upload to share"
+	pageDownloadFromShareTitle      = "Download shared file"
 )
 
 // condResult is the result of an HTTP request precondition check.
@@ -174,6 +176,11 @@ type shareLoginPage struct {
 	Branding   UIBranding
 }
 
+type shareDownloadPage struct {
+	baseClientPage
+	DownloadLink string
+}
+
 type shareUploadPage struct {
 	baseClientPage
 	Share          *dataprovider.Share
@@ -495,6 +502,11 @@ func loadClientTemplates(templatesPath string) {
 		filepath.Join(templatesPath, templateClientDir, templateClientBase),
 		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...)
 	profileTmpl := util.LoadTemplate(nil, profilePaths...)
@@ -512,6 +524,7 @@ func loadClientTemplates(templatesPath string) {
 	resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)
 	viewPDFTmpl := util.LoadTemplate(nil, viewPDFPaths...)
 	shareUploadTmpl := util.LoadTemplate(nil, shareUploadPath...)
+	shareDownloadTmpl := util.LoadTemplate(nil, shareDownloadPath...)
 
 	clientTemplates[templateClientFiles] = filesTmpl
 	clientTemplates[templateClientProfile] = profileTmpl
@@ -529,6 +542,7 @@ func loadClientTemplates(templatesPath string) {
 	clientTemplates[templateClientViewPDF] = viewPDFTmpl
 	clientTemplates[templateShareLogin] = shareLoginTmpl
 	clientTemplates[templateUploadToShare] = shareUploadTmpl
+	clientTemplates[templateShareDownload] = shareDownloadTmpl
 }
 
 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)
 }
 
+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) {
 	currentURL := path.Join(webClientPubSharesPath, share.ShareID, "upload")
 	data := shareUploadPage{
@@ -1799,15 +1821,53 @@ func (s *httpdServer) handleClientShareLoginPost(w http.ResponseWriter, r *http.
 		return
 	}
 	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
 	}
 	s.renderClientMessagePage(w, r, "Share Login OK", "Share login successful, you can now use your link",
 		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) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	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 (support@sftpgo.com).
 					let submitButton = document.querySelector('#sign_in_submit');
 					submitButton.setAttribute('data-kt-indicator', 'on');
 					submitButton.disabled = true;
-					return true;
 				});
 			});
 		</script>

+ 0 - 1
templates/webclient/profile.html

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

+ 0 - 1
templates/webclient/share.html

@@ -220,7 +220,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
                 let submitButton = document.querySelector('#form_submit');
                 submitButton.setAttribute('data-kt-indicator', 'on');
                 submitButton.disabled = true;
-                return true;
             });
         });
 </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 (support@sftpgo.com).
+-->
+{{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 (support@sftpgo.com).
                 $('#expiredShare').hide();
                 $('#writeShare').hide();
                 $('#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("title", shareURL + "/browse");
             } else {