From 1a765c7ff797680c0eefe5b251f7065b5b9516fe Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Fri, 17 Nov 2023 19:10:03 +0100 Subject: [PATCH] WebClient share: add a download page Signed-off-by: Nicola Murino --- go.mod | 8 ++-- go.sum | 16 +++---- internal/httpd/httpd_test.go | 14 ++++++ internal/httpd/internal_test.go | 35 ++++++++++++++ internal/httpd/server.go | 1 + internal/httpd/webclient.go | 64 +++++++++++++++++++++++++- templates/webclient/baselogin.html | 1 - templates/webclient/profile.html | 1 - templates/webclient/share.html | 1 - templates/webclient/sharedownload.html | 39 ++++++++++++++++ templates/webclient/shares.html | 8 ++-- 11 files changed, 167 insertions(+), 21 deletions(-) create mode 100644 templates/webclient/sharedownload.html diff --git a/go.mod b/go.mod index f499bdb3..584a8992 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 659bdcb7..e8232143 100644 --- a/go.sum +++ b/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= diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go index 59582081..4cf03607 100644 --- a/internal/httpd/httpd_test.go +++ b/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) diff --git a/internal/httpd/internal_test.go b/internal/httpd/internal_test.go index 3164909a..31d0093f 100644 --- a/internal/httpd/internal_test.go +++ b/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 diff --git a/internal/httpd/server.go b/internal/httpd/server.go index b9d1f38b..e107eb67 100644 --- a/internal/httpd/server.go +++ b/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) diff --git a/internal/httpd/webclient.go b/internal/httpd/webclient.go index 7269b55b..29032f7f 100644 --- a/internal/httpd/webclient.go +++ b/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 +} diff --git a/templates/webclient/baselogin.html b/templates/webclient/baselogin.html index 548bef6a..1b91e5b6 100644 --- a/templates/webclient/baselogin.html +++ b/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; }); }); diff --git a/templates/webclient/profile.html b/templates/webclient/profile.html index c2e00688..ddc53766 100644 --- a/templates/webclient/profile.html +++ b/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; }); }); diff --git a/templates/webclient/share.html b/templates/webclient/share.html index a471ab16..08151434 100644 --- a/templates/webclient/share.html +++ b/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; }); }); diff --git a/templates/webclient/sharedownload.html b/templates/webclient/sharedownload.html new file mode 100644 index 00000000..8ca17a2e --- /dev/null +++ b/templates/webclient/sharedownload.html @@ -0,0 +1,39 @@ + +{{template "base" .}} +{{- define "title"}}{{.Title}}{{- end}} +{{- define "page_body"}} +
+
+ + Logo + + + {{.Branding.ShortName}} + +
+
+
+

Your download is ready

+
+
+
+ Download +
+
+
+
+{{- end}} diff --git a/templates/webclient/shares.html b/templates/webclient/shares.html index 5c1fbbfa..e91f8736 100644 --- a/templates/webclient/shares.html +++ b/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 {