WebClient share: add a download page

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2023-11-17 19:10:03 +01:00
parent 61e6cc6985
commit 1a765c7ff7
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
11 changed files with 167 additions and 21 deletions

8
go.mod
View file

@ -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

16
go.sum
View file

@ -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=

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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}}

View file

@ -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 {