diff --git a/cmd/genman.go b/cmd/genman.go index 3b8cde29..e9d94593 100644 --- a/cmd/genman.go +++ b/cmd/genman.go @@ -1,7 +1,9 @@ package cmd import ( + "errors" "fmt" + "io/fs" "os" "github.com/rs/zerolog" @@ -25,7 +27,7 @@ current directory. Run: func(cmd *cobra.Command, args []string) { logger.DisableLogger() logger.EnableConsoleLogger(zerolog.DebugLevel) - if _, err := os.Stat(manDir); os.IsNotExist(err) { + if _, err := os.Stat(manDir); errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(manDir, os.ModePerm) if err != nil { logger.WarnToConsole("Unable to generate man page files: %v", err) diff --git a/dataprovider/share.go b/dataprovider/share.go index b2bb9d48..e4ec1193 100644 --- a/dataprovider/share.go +++ b/dataprovider/share.go @@ -21,6 +21,7 @@ type ShareScope int const ( ShareScopeRead ShareScope = iota + 1 ShareScopeWrite + ShareScopeReadWrite ) const ( @@ -64,10 +65,12 @@ type Share struct { // Used in web pages func (s *Share) GetScopeAsString() string { switch s.Scope { - case ShareScopeRead: - return "Read" - default: + case ShareScopeWrite: return "Write" + case ShareScopeReadWrite: + return "Read/Write" + default: + return "Read" } } @@ -194,7 +197,7 @@ func (s *Share) validatePaths() error { s.Paths[idx] = util.CleanPath(s.Paths[idx]) } s.Paths = util.RemoveDuplicates(s.Paths) - if s.Scope == ShareScopeWrite && len(s.Paths) != 1 { + if s.Scope >= ShareScopeWrite && len(s.Paths) != 1 { return util.NewValidationError("the write share scope requires exactly one path") } // check nested paths @@ -220,7 +223,7 @@ func (s *Share) validate() error { if s.Name == "" { return util.NewValidationError("name is mandatory") } - if s.Scope != ShareScopeRead && s.Scope != ShareScopeWrite { + if s.Scope < ShareScopeRead || s.Scope > ShareScopeReadWrite { return util.NewValidationError(fmt.Sprintf("invalid scope: %v", s.Scope)) } if err := s.validatePaths(); err != nil { diff --git a/ftpd/ftpd_test.go b/ftpd/ftpd_test.go index d9d5874d..35b4542c 100644 --- a/ftpd/ftpd_test.go +++ b/ftpd/ftpd_test.go @@ -6,8 +6,10 @@ import ( "crypto/tls" "encoding/hex" "encoding/json" + "errors" "fmt" "io" + "io/fs" "net" "net/http" "os" @@ -3463,7 +3465,7 @@ func getExitCodeScriptContent(exitCode int) []byte { func createTestFile(path string, size int64) error { baseDir := filepath.Dir(path) - if _, err := os.Stat(baseDir); os.IsNotExist(err) { + if _, err := os.Stat(baseDir); errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(baseDir, os.ModePerm) if err != nil { return err diff --git a/ftpd/internal_test.go b/ftpd/internal_test.go index 2af3313f..f454a1dd 100644 --- a/ftpd/internal_test.go +++ b/ftpd/internal_test.go @@ -3,7 +3,9 @@ package ftpd import ( "crypto/tls" "crypto/x509" + "errors" "fmt" + "io/fs" "net" "os" "path/filepath" @@ -735,7 +737,7 @@ func TestAVBLErrors(t *testing.T) { assert.NoError(t, err) _, err = connection.GetAvailableSpace("/missing-path") assert.Error(t, err) - assert.True(t, os.IsNotExist(err)) + assert.True(t, errors.Is(err, fs.ErrNotExist)) } func TestUploadOverwriteErrors(t *testing.T) { diff --git a/httpd/api_http_user.go b/httpd/api_http_user.go index 09dacff7..a9d3cb02 100644 --- a/httpd/api_http_user.go +++ b/httpd/api_http_user.go @@ -290,7 +290,7 @@ func doUploadFiles(w http.ResponseWriter, r *http.Request, connection *Connectio } defer file.Close() - filePath := path.Join(parentDir, f.Filename) + filePath := path.Join(parentDir, path.Base(util.CleanPath(f.Filename))) writer, err := connection.getFileWriter(filePath) if err != nil { sendAPIResponse(w, r, err, fmt.Sprintf("Unable to write file %#v", f.Filename), getMappedStatusCode(err)) diff --git a/httpd/api_shares.go b/httpd/api_shares.go index 44d9e39a..8f648354 100644 --- a/httpd/api_shares.go +++ b/httpd/api_shares.go @@ -153,7 +153,8 @@ func deleteShare(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) readBrowsableShareContents(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, false) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite} + share, connection, err := s.checkPublicShare(w, r, validScopes, false) if err != nil { return } @@ -183,7 +184,8 @@ func (s *httpdServer) readBrowsableShareContents(w http.ResponseWriter, r *http. func (s *httpdServer) downloadBrowsableSharedFile(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, false) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite} + share, connection, err := s.checkPublicShare(w, r, validScopes, false) if err != nil { return } @@ -232,7 +234,8 @@ func (s *httpdServer) downloadBrowsableSharedFile(w http.ResponseWriter, r *http func (s *httpdServer) downloadFromShare(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, false) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite} + share, connection, err := s.checkPublicShare(w, r, validScopes, false) if err != nil { return } @@ -264,7 +267,7 @@ func (s *httpdServer) downloadFromShare(w http.ResponseWriter, r *http.Request) connection.Log(logger.LevelInfo, "denying share read due to quota limits") sendAPIResponse(w, r, err, "", getMappedStatusCode(err)) } - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"share-%v.zip\"", share.ShareID)) + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"share-%v.zip\"", share.Name)) renderCompressedFiles(w, connection, "/", share.Paths, &share) return } @@ -287,12 +290,17 @@ func (s *httpdServer) uploadFileToShare(w http.ResponseWriter, r *http.Request) r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize) } name := getURLParam(r, "name") - share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeWrite, false) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite} + share, connection, err := s.checkPublicShare(w, r, validScopes, false) if err != nil { return } - filePath := path.Join(share.Paths[0], name) - if path.Dir(filePath) != share.Paths[0] { + filePath := util.CleanPath(path.Join(share.Paths[0], name)) + expectedPrefix := share.Paths[0] + if !strings.HasSuffix(expectedPrefix, "/") { + expectedPrefix += "/" + } + if !strings.HasPrefix(filePath, expectedPrefix) { sendAPIResponse(w, r, err, "Uploading outside the share is not allowed", http.StatusForbidden) return } @@ -312,7 +320,8 @@ func (s *httpdServer) uploadFilesToShare(w http.ResponseWriter, r *http.Request) if maxUploadFileSize > 0 { r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize) } - share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeWrite, false) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite} + share, connection, err := s.checkPublicShare(w, r, validScopes, false) if err != nil { return } @@ -361,7 +370,7 @@ func (s *httpdServer) uploadFilesToShare(w http.ResponseWriter, r *http.Request) } } -func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, shareShope dataprovider.ShareScope, +func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, validScopes []dataprovider.ShareScope, isWebClient bool, ) (dataprovider.Share, *Connection, error) { renderError := func(err error, message string, statusCode int) { @@ -382,7 +391,7 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, s renderError(err, "", statusCode) return share, nil, err } - if share.Scope != shareShope { + if !util.Contains(validScopes, share.Scope) { renderError(nil, "Invalid share scope", http.StatusForbidden) return share, nil, errors.New("invalid share scope") } @@ -406,16 +415,11 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, s return share, nil, dataprovider.ErrInvalidCredentials } } - user, err := dataprovider.GetUserWithGroupSettings(share.Username) + user, err := getUserForShare(share) if err != nil { renderError(err, "", getRespStatus(err)) return share, nil, err } - if user.MustSetSecondFactorForProtocol(common.ProtocolHTTP) { - err := util.NewMethodDisabledError("two-factor authentication requirements not met") - renderError(err, "", getRespStatus(err)) - return share, nil, err - } connID := xid.New().String() connection := &Connection{ BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTPShare, util.GetHTTPLocalAddress(r), @@ -426,6 +430,23 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, s return share, connection, nil } +func getUserForShare(share dataprovider.Share) (dataprovider.User, error) { + user, err := dataprovider.GetUserWithGroupSettings(share.Username) + if err != nil { + return user, err + } + if !user.CanManageShares() { + return user, util.NewRecordNotFoundError("this share does not exist") + } + if share.Password == "" && util.Contains(user.Filters.WebClient, sdk.WebClientShareNoPasswordDisabled) { + return user, fmt.Errorf("sharing without a password was disabled: %w", os.ErrPermission) + } + if user.MustSetSecondFactorForProtocol(common.ProtocolHTTP) { + return user, util.NewMethodDisabledError("two-factor authentication requirements not met") + } + return user, nil +} + func validateBrowsableShare(share dataprovider.Share, connection *Connection) error { if len(share.Paths) != 1 { return util.NewValidationError("a share with multiple paths is not browsable") diff --git a/httpd/api_utils.go b/httpd/api_utils.go index ef97a952..4e716056 100644 --- a/httpd/api_utils.go +++ b/httpd/api_utils.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "io/fs" "mime" "net/http" "net/url" @@ -79,10 +80,10 @@ func getRespStatus(err error) int { if _, ok := err.(*util.RecordNotFoundError); ok { return http.StatusNotFound } - if os.IsNotExist(err) { + if errors.Is(err, fs.ErrNotExist) { return http.StatusBadRequest } - if os.IsPermission(err) || errors.Is(err, dataprovider.ErrLoginNotAllowedFromIP) { + if errors.Is(err, fs.ErrPermission) || errors.Is(err, dataprovider.ErrLoginNotAllowedFromIP) { return http.StatusForbidden } if errors.Is(err, plugin.ErrNoSearcher) || errors.Is(err, dataprovider.ErrNotImplemented) { @@ -241,7 +242,11 @@ func addZipEntry(wr *zip.Writer, conn *Connection, entryPath, baseDir string) er return err } if info.IsDir() { - _, err := wr.Create(getZipEntryName(entryPath, baseDir) + "/") + _, err := wr.CreateHeader(&zip.FileHeader{ + Name: getZipEntryName(entryPath, baseDir) + "/", + Method: zip.Deflate, + Modified: info.ModTime(), + }) if err != nil { conn.Log(logger.LevelDebug, "unable to create zip entry %#v: %v", entryPath, err) return err @@ -271,7 +276,11 @@ func addZipEntry(wr *zip.Writer, conn *Connection, entryPath, baseDir string) er } defer reader.Close() - f, err := wr.Create(getZipEntryName(entryPath, baseDir)) + f, err := wr.CreateHeader(&zip.FileHeader{ + Name: getZipEntryName(entryPath, baseDir), + Method: zip.Deflate, + Modified: info.ModTime(), + }) if err != nil { conn.Log(logger.LevelDebug, "unable to create zip entry %#v: %v", entryPath, err) return err diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index 429a8b8b..885c4fef 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "io/fs" "math" "mime/multipart" "net" @@ -8561,7 +8562,7 @@ func TestStartQuotaScanMock(t *testing.T) { waitForUsersQuotaScan(t, token) _, err = os.Stat(user.HomeDir) - if err != nil && os.IsNotExist(err) { + if err != nil && errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(user.HomeDir, os.ModePerm) assert.NoError(t, err) } @@ -8725,7 +8726,7 @@ func TestStartFolderQuotaScanMock(t *testing.T) { assert.True(t, common.QuotaScans.RemoveVFolderQuotaScan(folderName)) // and now a real quota scan _, err = os.Stat(mappedPath) - if err != nil && os.IsNotExist(err) { + if err != nil && errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(mappedPath, os.ModePerm) assert.NoError(t, err) } @@ -10137,6 +10138,10 @@ func TestShareUsage(t *testing.T) { checkResponseCode(t, http.StatusForbidden, rr) assert.Contains(t, rr.Body.String(), "permission denied") + user.Permissions["/"] = []string{dataprovider.PermAny} + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + body = new(bytes.Buffer) writer = multipart.NewWriter(body) part, err := writer.CreateFormFile("filename", "file1.txt") @@ -10155,7 +10160,37 @@ func TestShareUsage(t *testing.T) { checkResponseCode(t, http.StatusBadRequest, rr) assert.Contains(t, rr.Body.String(), "No files uploaded!") - share.Scope = dataprovider.ShareScopeRead + user.Filters.WebClient = []string{sdk.WebClientSharesDisabled} + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + + req, err = http.NewRequest(http.MethodPost, sharesPath+"/"+objectID, reader) + assert.NoError(t, err) + req.Header.Add("Content-Type", writer.FormDataContentType()) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusNotFound, rr) + + user.Filters.WebClient = []string{sdk.WebClientShareNoPasswordDisabled} + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + share.Password = "" + err = dataprovider.UpdateShare(&share, user.Username, "") + assert.NoError(t, err) + + req, err = http.NewRequest(http.MethodPost, sharesPath+"/"+objectID, reader) + assert.NoError(t, err) + req.Header.Add("Content-Type", writer.FormDataContentType()) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusForbidden, rr) + assert.Contains(t, rr.Body.String(), "sharing without a password was disabled") + + user.Filters.WebClient = []string{sdk.WebClientInfoChangeDisabled} + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + + share.Scope = dataprovider.ShareScopeReadWrite share.Paths = []string{"/missing"} err = dataprovider.UpdateShare(&share, user.Username, "") assert.NoError(t, err) @@ -10347,12 +10382,6 @@ func TestShareUploadSingle(t *testing.T) { if assert.NoError(t, err) { assert.InDelta(t, util.GetTimeAsMsSinceEpoch(time.Now()), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(3000)) } - // we don't allow to create the file in subdirectories - req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, "%2Fdir%2Ffile1.txt"), bytes.NewBuffer(content)) - assert.NoError(t, err) - req.SetBasicAuth(defaultUsername, defaultPassword) - rr = executeRequest(req) - checkResponseCode(t, http.StatusForbidden, rr) req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, "dir", "file.dat"), bytes.NewBuffer(content)) assert.NoError(t, err) @@ -10391,6 +10420,76 @@ func TestShareUploadSingle(t *testing.T) { checkResponseCode(t, http.StatusNotFound, rr) } +func TestShareReadWrite(t *testing.T) { + u := getTestUser() + u.Filters.StartDirectory = path.Join("/start", "dir") + user, _, err := httpdtest.AddUser(u, http.StatusCreated) + assert.NoError(t, err) + token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword) + assert.NoError(t, err) + testFileName := "test.txt" + + share := dataprovider.Share{ + Name: "test share rw", + Scope: dataprovider.ShareScopeReadWrite, + Paths: []string{user.Filters.StartDirectory}, + Password: defaultPassword, + MaxTokens: 0, + } + asJSON, err := json.Marshal(share) + 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) + objectID := rr.Header().Get("X-Object-ID") + assert.NotEmpty(t, objectID) + + content := []byte("shared rw content") + req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, testFileName), bytes.NewBuffer(content)) + assert.NoError(t, err) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusCreated, rr) + assert.FileExists(t, filepath.Join(user.GetHomeDir(), user.Filters.StartDirectory, testFileName)) + + req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "browse?path=%2F"), nil) + assert.NoError(t, err) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "browse?path="+testFileName), nil) + assert.NoError(t, err) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + contentDisposition := rr.Header().Get("Content-Disposition") + assert.NotEmpty(t, contentDisposition) + + req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID)+"/"+url.PathEscape("../"+testFileName), + bytes.NewBuffer(content)) + assert.NoError(t, err) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusForbidden, rr) + assert.Contains(t, rr.Body.String(), "Uploading outside the share is not allowed") + + req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID)+"/"+url.PathEscape("/../../"+testFileName), + bytes.NewBuffer(content)) + assert.NoError(t, err) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusForbidden, rr) + assert.Contains(t, rr.Body.String(), "Uploading outside the share is not allowed") + + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(user.GetHomeDir()) + assert.NoError(t, err) +} + func TestShareUncompressed(t *testing.T) { user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) @@ -10799,7 +10898,7 @@ func TestBrowseShares(t *testing.T) { req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "dirs"), nil) assert.NoError(t, err) rr = executeRequest(req) - checkResponseCode(t, http.StatusInternalServerError, rr) + checkResponseCode(t, http.StatusBadRequest, rr) assert.Contains(t, rr.Body.String(), "unable to check the share directory") // share multiple paths share = dataprovider.Share{ @@ -10868,6 +10967,32 @@ func TestBrowseShares(t *testing.T) { rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) assert.Contains(t, rr.Body.String(), "two-factor authentication requirements not met") + user.Filters.TwoFactorAuthProtocols = []string{common.ProtocolSSH} + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + // share read/write + share.Scope = dataprovider.ShareScopeReadWrite + asJSON, err = json.Marshal(share) + 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) + objectID = rr.Header().Get("X-Object-ID") + assert.NotEmpty(t, objectID) + req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "browse?path=%2F"), nil) + assert.NoError(t, err) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + // on upload we should be redirected + req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "upload"), nil) + assert.NoError(t, err) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusFound, rr) + location := rr.Header().Get("Location") + assert.Equal(t, path.Join(webClientPubSharesPath, objectID, "browse"), location) _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) @@ -18853,7 +18978,7 @@ func checkResponseCode(t *testing.T, expected int, rr *httptest.ResponseRecorder func createTestFile(path string, size int64) error { baseDir := filepath.Dir(path) - if _, err := os.Stat(baseDir); os.IsNotExist(err) { + if _, err := os.Stat(baseDir); errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(baseDir, os.ModePerm) if err != nil { return err diff --git a/httpd/internal_test.go b/httpd/internal_test.go index e3c30ce3..d7e8b988 100644 --- a/httpd/internal_test.go +++ b/httpd/internal_test.go @@ -2291,7 +2291,13 @@ func TestMetadataAPI(t *testing.T) { func TestBrowsableSharePaths(t *testing.T) { share := dataprovider.Share{ - Paths: []string{"/"}, + Paths: []string{"/"}, + Username: defaultAdminUsername, + } + _, err := getUserForShare(share) + if assert.Error(t, err) { + _, ok := err.(*util.RecordNotFoundError) + assert.True(t, ok) } req, err := http.NewRequest(http.MethodGet, "/share", nil) require.NoError(t, err) diff --git a/httpd/webclient.go b/httpd/webclient.go index 2068314b..7e1835a2 100644 --- a/httpd/webclient.go +++ b/httpd/webclient.go @@ -143,12 +143,14 @@ type filesPage struct { type shareFilesPage struct { baseClientPage - CurrentDir string - DirsURL string - FilesURL string - DownloadURL string - Error string - Paths []dirMapping + CurrentDir string + DirsURL string + FilesURL string + DownloadURL string + UploadBaseURL string + Error string + Paths []dirMapping + Scope dataprovider.ShareScope } type shareUploadPage struct { @@ -512,8 +514,10 @@ func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Reque DirsURL: path.Join(webClientPubSharesPath, share.ShareID, "dirs"), FilesURL: currentURL, DownloadURL: path.Join(webClientPubSharesPath, share.ShareID), + UploadBaseURL: path.Join(webClientPubSharesPath, share.ShareID, url.PathEscape(dirName)), Error: error, Paths: getDirMapping(dirName, currentURL), + Scope: share.Scope, } renderClientTemplate(w, templateShareFiles, data) } @@ -625,7 +629,8 @@ func (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http. func (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, true) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite} + share, connection, err := s.checkPublicShare(w, r, validScopes, true) if err != nil { return } @@ -674,16 +679,22 @@ func (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.R func (s *httpdServer) handleClientUploadToShare(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - share, _, err := s.checkPublicShare(w, r, dataprovider.ShareScopeWrite, true) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite} + share, _, err := s.checkPublicShare(w, r, validScopes, true) if err != nil { return } + if share.Scope == dataprovider.ShareScopeReadWrite { + http.Redirect(w, r, path.Join(webClientPubSharesPath, share.ShareID, "browse"), http.StatusFound) + return + } s.renderUploadToSharePage(w, r, share) } func (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, true) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite} + share, connection, err := s.checkPublicShare(w, r, validScopes, true) if err != nil { return } diff --git a/logger/logger.go b/logger/logger.go index 38aa9d55..ac72d0cd 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -11,6 +11,7 @@ package logger import ( "errors" "fmt" + "io/fs" "os" "path/filepath" "time" @@ -57,7 +58,7 @@ func InitLogger(logFilePath string, logMaxSize int, logMaxBackups int, logMaxAge SetLogTime(logUTCTime) if isLogFilePathValid(logFilePath) { logDir := filepath.Dir(logFilePath) - if _, err := os.Stat(logDir); os.IsNotExist(err) { + if _, err := os.Stat(logDir); errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(logDir, os.ModePerm) if err != nil { fmt.Printf("unable to create log dir %#v: %v", logDir, err) diff --git a/sftpd/server.go b/sftpd/server.go index d0ad805d..a93e8fe3 100644 --- a/sftpd/server.go +++ b/sftpd/server.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "io/fs" "net" "os" "path" @@ -755,7 +756,7 @@ func (c *Configuration) generateDefaultHostKeys(configDir string) error { defaultHostKeys := []string{defaultPrivateRSAKeyName, defaultPrivateECDSAKeyName, defaultPrivateEd25519KeyName} for _, k := range defaultHostKeys { autoFile := filepath.Join(configDir, k) - if _, err = os.Stat(autoFile); os.IsNotExist(err) { + if _, err = os.Stat(autoFile); errors.Is(err, fs.ErrNotExist) { logger.Info(logSender, "", "No host keys configured and %#v does not exist; try to create a new host key", autoFile) logger.InfoToConsole("No host keys configured and %#v does not exist; try to create a new host key", autoFile) if k == defaultPrivateRSAKeyName { @@ -780,7 +781,7 @@ func (c *Configuration) generateDefaultHostKeys(configDir string) error { func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error { for _, k := range c.HostKeys { if filepath.IsAbs(k) { - if _, err := os.Stat(k); os.IsNotExist(err) { + if _, err := os.Stat(k); errors.Is(err, fs.ErrNotExist) { keyName := filepath.Base(k) switch keyName { case defaultPrivateRSAKeyName: diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index 1050a1ce..f3d28346 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -10,9 +10,11 @@ import ( "encoding/base64" "encoding/binary" "encoding/json" + "errors" "fmt" "hash" "io" + "io/fs" "math" "net" "net/http" @@ -1463,11 +1465,11 @@ func TestStat(t *testing.T) { assert.NoError(t, err) _, err = client.Stat(testFileName) assert.NoError(t, err) - // stat a missing path we should get an os.IsNotExist error + // stat a missing path we should get an fs.ErrNotExist error _, err = client.Stat("missing path") - assert.True(t, os.IsNotExist(err)) + assert.True(t, errors.Is(err, fs.ErrNotExist)) _, err = client.Lstat("missing path") - assert.True(t, os.IsNotExist(err)) + assert.True(t, errors.Is(err, fs.ErrNotExist)) // mode 0666 and 0444 works on Windows too newPerm := os.FileMode(0666) err = client.Chmod(testFileName, newPerm) @@ -6924,7 +6926,7 @@ func TestOpenError(t *testing.T) { err = os.Chmod(filepath.Join(user.GetHomeDir(), testDir), 0000) assert.NoError(t, err) err = client.Rename(testFileName, path.Join(testDir, testFileName)) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = os.Chmod(filepath.Join(user.GetHomeDir(), testDir), os.ModePerm) assert.NoError(t, err) err = os.Remove(localDownloadPath) @@ -7253,7 +7255,7 @@ func TestPermRename(t *testing.T) { err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) assert.NoError(t, err) err = client.Rename(testFileName, testFileName+".rename") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) _, err = client.Stat(testFileName) assert.NoError(t, err) err = os.Remove(testFilePath) @@ -7289,7 +7291,7 @@ func TestPermRenameOverwrite(t *testing.T) { err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) assert.NoError(t, err) err = client.Rename(testFileName, testFileName+".rename") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Remove(testFileName) assert.NoError(t, err) err = os.Remove(testFilePath) @@ -7474,13 +7476,13 @@ func TestSubDirsUploads(t *testing.T) { err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) assert.NoError(t, err) err = sftpUploadFile(testFilePath, testFileNameSub, testFileSize, client) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Symlink(testFileName, testFileNameSub+".link") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Symlink(testFileName, testFileName+".link") assert.NoError(t, err) err = client.Rename(testFileName, testFileNameSub+".rename") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Rename(testFileName, testFileName+".rename") assert.NoError(t, err) err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) @@ -7500,7 +7502,7 @@ func TestSubDirsUploads(t *testing.T) { err = client.Remove(testDir) assert.NoError(t, err) err = client.Remove(path.Join("/subdir", "file.dat")) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Remove(testFileName + ".rename") assert.NoError(t, err) err = os.Remove(testFilePath) @@ -7532,7 +7534,7 @@ func TestSubDirsOverwrite(t *testing.T) { err = createTestFile(testFileSFTPPath, 16384) assert.NoError(t, err) err = sftpUploadFile(testFilePath, testFileName+".new", testFileSize, client) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) assert.NoError(t, err) err = os.Remove(testFilePath) @@ -7566,17 +7568,17 @@ func TestSubDirsDownloads(t *testing.T) { assert.NoError(t, err) localDownloadPath := filepath.Join(homeBasePath, testDLFileName) err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Chtimes(testFileName, time.Now(), time.Now()) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Rename(testFileName, testFileName+".rename") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Symlink(testFileName, testFileName+".link") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Remove(testFileName) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = os.Remove(localDownloadPath) assert.NoError(t, err) err = os.Remove(testFilePath) @@ -7611,9 +7613,9 @@ func TestPermsSubDirsSetstat(t *testing.T) { err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) assert.NoError(t, err) err = client.Chtimes("/subdir/", time.Now(), time.Now()) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Chtimes("subdir/", time.Now(), time.Now()) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Chtimes(testFileName, time.Now(), time.Now()) assert.NoError(t, err) err = os.Remove(testFilePath) @@ -7674,19 +7676,19 @@ func TestPermsSubDirsCommands(t *testing.T) { _, err = client.ReadDir("/") assert.NoError(t, err) _, err = client.ReadDir("/subdir") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.RemoveDirectory("/subdir/dir") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Mkdir("/subdir/otherdir/dir") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Mkdir("/otherdir") assert.NoError(t, err) err = client.Mkdir("/subdir/otherdir") assert.NoError(t, err) err = client.Rename("/otherdir", "/subdir/otherdir/adir") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Symlink("/otherdir", "/subdir/otherdir") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Symlink("/otherdir", "/otherdir_link") assert.NoError(t, err) err = client.Rename("/otherdir", "/otherdir1") @@ -7718,11 +7720,11 @@ func TestRootDirCommands(t *testing.T) { defer conn.Close() defer client.Close() err = client.Rename("/", "rootdir") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Symlink("/", "rootdir") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.RemoveDirectory("/") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) } if user.Username == defaultUsername { err = os.RemoveAll(user.GetHomeDir()) @@ -8200,7 +8202,7 @@ func TestStatVFS(t *testing.T) { _, err = client.StatVFS("missing-path") assert.Error(t, err) - assert.True(t, os.IsNotExist(err)) + assert.True(t, errors.Is(err, fs.ErrNotExist)) } user.QuotaFiles = 100 user.Filters.DisableFsChecks = true @@ -10524,7 +10526,7 @@ func getCustomAuthSftpClient(user dataprovider.User, authMethods []ssh.AuthMetho func createTestFile(path string, size int64) error { baseDir := filepath.Dir(path) - if _, err := os.Stat(baseDir); os.IsNotExist(err) { + if _, err := os.Stat(baseDir); errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(baseDir, os.ModePerm) if err != nil { return err diff --git a/templates/webadmin/mfa.html b/templates/webadmin/mfa.html index b0106ed5..ebc0444e 100644 --- a/templates/webadmin/mfa.html +++ b/templates/webadmin/mfa.html @@ -2,6 +2,10 @@ {{define "title"}}{{.Title}}{{end}} +{{define "extra_css"}} + +{{end}} + {{define "page_body"}}