fix: harden exclusive file creation and webdav writes
This commit is contained in:
+52
-163
@@ -478,13 +478,20 @@ func (s *Server) newtext(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f, e := s.Store.Create(p)
|
f, e := createExclusive(s.Store, p)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
|
if os.IsExist(e) {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
http.Error(w, e.Error(), 500)
|
http.Error(w, e.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
io.WriteString(f, r.FormValue("text"))
|
if _, e = io.WriteString(f, r.FormValue("text")); e != nil {
|
||||||
|
http.Error(w, e.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
s.logOp(r, fmt.Sprintf("create a text file:'%s'", p))
|
s.logOp(r, fmt.Sprintf("create a text file:'%s'", p))
|
||||||
writeCreated(w)
|
writeCreated(w)
|
||||||
}
|
}
|
||||||
@@ -573,19 +580,42 @@ func (s *Server) upload(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
writeCreated(w)
|
writeCreated(w)
|
||||||
}
|
}
|
||||||
|
func createExclusive(st storage.Storage, target string) (*os.File, error) {
|
||||||
|
if x, ok := st.(storage.ExclusiveCreator); ok {
|
||||||
|
return x.CreateExclusive(target)
|
||||||
|
}
|
||||||
|
if _, err := st.Stat(target); err == nil {
|
||||||
|
return nil, os.ErrExist
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return st.Create(target)
|
||||||
|
}
|
||||||
|
|
||||||
func savePart(st storage.Storage, target string, h *multipart.FileHeader) error {
|
func savePart(st storage.Storage, target string, h *multipart.FileHeader) error {
|
||||||
in, e := h.Open()
|
in, e := h.Open()
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
defer in.Close()
|
defer in.Close()
|
||||||
f, e := st.Create(target)
|
f, e := createExclusive(st, target)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
defer f.Close()
|
copyErr := func() error {
|
||||||
_, e = io.Copy(f, in)
|
_, err := io.Copy(f, in)
|
||||||
return e
|
return err
|
||||||
|
}()
|
||||||
|
closeErr := f.Close()
|
||||||
|
if copyErr != nil {
|
||||||
|
_ = st.Remove(target)
|
||||||
|
return copyErr
|
||||||
|
}
|
||||||
|
if closeErr != nil {
|
||||||
|
_ = st.Remove(target)
|
||||||
|
return closeErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) upload2(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) upload2(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -1079,17 +1109,32 @@ func (f *webdavStorageFS) Create(ctx context.Context, name string, body io.ReadC
|
|||||||
if statErr != nil && !os.IsNotExist(statErr) {
|
if statErr != nil && !os.IsNotExist(statErr) {
|
||||||
return nil, false, statErr
|
return nil, false, statErr
|
||||||
}
|
}
|
||||||
file, err := f.store.Create(p)
|
exclusive := opts != nil && opts.IfNoneMatch.IsWildcard()
|
||||||
|
var file *os.File
|
||||||
|
if exclusive {
|
||||||
|
file, err = createExclusive(f.store, p)
|
||||||
|
} else {
|
||||||
|
file, err = f.store.Create(p)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if exclusive && os.IsExist(err) {
|
||||||
|
return nil, false, webdav.NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("destination exists"))
|
||||||
|
}
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
_, copyErr := io.Copy(file, body)
|
_, copyErr := io.Copy(file, body)
|
||||||
closeErr := file.Close()
|
closeErr := file.Close()
|
||||||
body.Close()
|
body.Close()
|
||||||
if copyErr != nil {
|
if copyErr != nil {
|
||||||
|
if exclusive {
|
||||||
|
_ = f.store.Remove(p)
|
||||||
|
}
|
||||||
return nil, false, copyErr
|
return nil, false, copyErr
|
||||||
}
|
}
|
||||||
if closeErr != nil {
|
if closeErr != nil {
|
||||||
|
if exclusive {
|
||||||
|
_ = f.store.Remove(p)
|
||||||
|
}
|
||||||
return nil, false, closeErr
|
return nil, false, closeErr
|
||||||
}
|
}
|
||||||
fi, err := f.info(p)
|
fi, err := f.info(p)
|
||||||
@@ -1272,162 +1317,6 @@ func (f *webdavStorageFS) Move(ctx context.Context, name, dest string, opts *web
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type webdavMultiFS struct {
|
|
||||||
roots map[string]webdav.FileSystem
|
|
||||||
names []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newWebDAVMultiFS(m storage.Multi) *webdavMultiFS {
|
|
||||||
fs := &webdavMultiFS{roots: make(map[string]webdav.FileSystem), names: append([]string(nil), m.Names...)}
|
|
||||||
for i, name := range m.Names {
|
|
||||||
if i < len(m.Roots) {
|
|
||||||
fs.roots[name] = webdav.LocalFileSystem(m.Roots[i].Root)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fs
|
|
||||||
}
|
|
||||||
func splitDAVPath(name string) (string, string) {
|
|
||||||
p := strings.Trim(strings.ReplaceAll(name, "\\\\", "/"), "/")
|
|
||||||
p = strings.TrimPrefix(p, "webdav/")
|
|
||||||
p = strings.TrimPrefix(p, "webdav")
|
|
||||||
if p == "" {
|
|
||||||
return "", ""
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(p, "/", 2)
|
|
||||||
if len(parts) == 1 {
|
|
||||||
return parts[0], ""
|
|
||||||
}
|
|
||||||
return parts[0], parts[1]
|
|
||||||
}
|
|
||||||
func (f *webdavMultiFS) root(name string) (webdav.FileSystem, string, error) {
|
|
||||||
r, rel := splitDAVPath(name)
|
|
||||||
if r == "" {
|
|
||||||
return nil, "", nil
|
|
||||||
}
|
|
||||||
fs, ok := f.roots[r]
|
|
||||||
if !ok {
|
|
||||||
return nil, "", webdav.NewHTTPError(http.StatusNotFound, fmt.Errorf("root not found"))
|
|
||||||
}
|
|
||||||
return fs, rel, nil
|
|
||||||
}
|
|
||||||
func (f *webdavMultiFS) Open(ctx context.Context, name string) (io.ReadCloser, error) {
|
|
||||||
fs, rel, err := f.root(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if fs == nil {
|
|
||||||
return nil, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("virtual root is not a file"))
|
|
||||||
}
|
|
||||||
return fs.Open(ctx, "/"+rel)
|
|
||||||
}
|
|
||||||
func (f *webdavMultiFS) Stat(ctx context.Context, name string) (*webdav.FileInfo, error) {
|
|
||||||
if strings.Trim(strings.ReplaceAll(name, "\\\\", "/"), "/") == "" {
|
|
||||||
return &webdav.FileInfo{Path: "/", IsDir: true}, nil
|
|
||||||
}
|
|
||||||
fs, rel, err := f.root(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if rel == "" {
|
|
||||||
return &webdav.FileInfo{Path: "/" + strings.Trim(strings.SplitN(strings.Trim(name, "/"), "/", 2)[0], "/"), IsDir: true}, nil
|
|
||||||
}
|
|
||||||
fi, err := fs.Stat(ctx, "/"+rel)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
fi.Path = "/" + strings.Trim(name, "/")
|
|
||||||
return fi, nil
|
|
||||||
}
|
|
||||||
func (f *webdavMultiFS) ReadDir(ctx context.Context, name string, recursive bool) ([]webdav.FileInfo, error) {
|
|
||||||
p := strings.Trim(strings.ReplaceAll(name, "\\\\", "/"), "/")
|
|
||||||
if p == "" {
|
|
||||||
out := make([]webdav.FileInfo, 0, len(f.names))
|
|
||||||
for _, n := range f.names {
|
|
||||||
out = append(out, webdav.FileInfo{Path: "/" + n, IsDir: true})
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
fs, rel, err := f.root(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items, err := fs.ReadDir(ctx, "/"+rel, recursive)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
root, _ := splitDAVPath(p)
|
|
||||||
for i := range items {
|
|
||||||
itemPath := strings.TrimPrefix(items[i].Path, "/")
|
|
||||||
if itemPath == "" {
|
|
||||||
itemPath = filepath.Base(items[i].Path)
|
|
||||||
}
|
|
||||||
items[i].Path = "/" + path.Join(root, itemPath)
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
func (f *webdavMultiFS) Create(ctx context.Context, name string, body io.ReadCloser, opts *webdav.CreateOptions) (*webdav.FileInfo, bool, error) {
|
|
||||||
fs, rel, err := f.root(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
if fs == nil || rel == "" {
|
|
||||||
return nil, false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cannot create virtual root"))
|
|
||||||
}
|
|
||||||
fi, created, err := fs.Create(ctx, "/"+rel, body, opts)
|
|
||||||
if err == nil {
|
|
||||||
fi.Path = "/" + strings.Trim(name, "/")
|
|
||||||
}
|
|
||||||
return fi, created, err
|
|
||||||
}
|
|
||||||
func (f *webdavMultiFS) RemoveAll(ctx context.Context, name string, opts *webdav.RemoveAllOptions) error {
|
|
||||||
fs, rel, err := f.root(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if fs == nil || rel == "" {
|
|
||||||
return webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cannot remove virtual root"))
|
|
||||||
}
|
|
||||||
return fs.RemoveAll(ctx, "/"+rel, opts)
|
|
||||||
}
|
|
||||||
func (f *webdavMultiFS) Mkdir(ctx context.Context, name string) error {
|
|
||||||
fs, rel, err := f.root(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if fs == nil || rel == "" {
|
|
||||||
return webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cannot create virtual root"))
|
|
||||||
}
|
|
||||||
return fs.Mkdir(ctx, "/"+rel)
|
|
||||||
}
|
|
||||||
func (f *webdavMultiFS) Copy(ctx context.Context, name, dest string, opts *webdav.CopyOptions) (bool, error) {
|
|
||||||
fs, rel, err := f.root(name)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
dfs, drel, err := f.root(dest)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if fs == nil || dfs == nil || fs != dfs {
|
|
||||||
return false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cross-root copy is not supported"))
|
|
||||||
}
|
|
||||||
return fs.Copy(ctx, "/"+rel, "/"+drel, opts)
|
|
||||||
}
|
|
||||||
func (f *webdavMultiFS) Move(ctx context.Context, name, dest string, opts *webdav.MoveOptions) (bool, error) {
|
|
||||||
fs, rel, err := f.root(name)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
dfs, drel, err := f.root(dest)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if fs == nil || dfs == nil || fs != dfs {
|
|
||||||
return false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cross-root move is not supported"))
|
|
||||||
}
|
|
||||||
return fs.Move(ctx, "/"+rel, "/"+drel, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
type webdavAuthFS struct {
|
type webdavAuthFS struct {
|
||||||
inner webdav.FileSystem
|
inner webdav.FileSystem
|
||||||
account *auth.Account
|
account *auth.Account
|
||||||
|
|||||||
@@ -107,6 +107,14 @@ func (m Multi) Create(p string) (*os.File, error) {
|
|||||||
}
|
}
|
||||||
return r.Create(rel)
|
return r.Create(rel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Multi) CreateExclusive(p string) (*os.File, error) {
|
||||||
|
r, rel, ok := m.find(p)
|
||||||
|
if !ok {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
return r.CreateExclusive(rel)
|
||||||
|
}
|
||||||
func (m Multi) Remove(p string) error {
|
func (m Multi) Remove(p string) error {
|
||||||
r, rel, ok := m.find(p)
|
r, rel, ok := m.find(p)
|
||||||
if !ok || rel == "" {
|
if !ok || rel == "" {
|
||||||
|
|||||||
@@ -46,3 +46,44 @@ func TestMultiRemoveCannotRemoveVirtualRoot(t *testing.T) {
|
|||||||
t.Fatal("expected virtual root removal to fail")
|
t.Fatal("expected virtual root removal to fail")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLocalCreateExclusiveRejectsExistingFile(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
st := Local{Root: root}
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "x.txt"), []byte("old"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f, err := st.CreateExclusive("x.txt")
|
||||||
|
if !os.IsExist(err) || f != nil {
|
||||||
|
t.Fatalf("expected EEXIST, file=%v err=%v", f, err)
|
||||||
|
}
|
||||||
|
got, err := os.ReadFile(filepath.Join(root, "x.txt"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(got) != "old" {
|
||||||
|
t.Fatalf("existing file changed: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocalCreateExclusiveCreatesMissingFile(t *testing.T) {
|
||||||
|
st := Local{Root: t.TempDir()}
|
||||||
|
f, err := st.CreateExclusive("nested/x.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := f.WriteString("new"); err != nil {
|
||||||
|
f.Close()
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err := os.ReadFile(filepath.Join(st.Root, "nested", "x.txt"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(got) != "new" {
|
||||||
|
t.Fatalf("unexpected content: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ type Storage interface {
|
|||||||
List(path string) ([]os.FileInfo, error)
|
List(path string) ([]os.FileInfo, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExclusiveCreator interface {
|
||||||
|
CreateExclusive(path string) (*os.File, error)
|
||||||
|
}
|
||||||
|
|
||||||
type Local struct{ Root string }
|
type Local struct{ Root string }
|
||||||
|
|
||||||
func (s Local) real(path string) (string, error) {
|
func (s Local) real(path string) (string, error) {
|
||||||
@@ -95,6 +99,17 @@ func (s Local) Create(path string) (*os.File, error) {
|
|||||||
}
|
}
|
||||||
return os.Create(p)
|
return os.Create(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Local) CreateExclusive(path string) (*os.File, error) {
|
||||||
|
p, e := s.real(path)
|
||||||
|
if e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
if e = os.MkdirAll(filepath.Dir(p), 0755); e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
return os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
|
||||||
|
}
|
||||||
func (s Local) Remove(path string) error {
|
func (s Local) Remove(path string) error {
|
||||||
p, e := s.real(path)
|
p, e := s.real(path)
|
||||||
if e == nil {
|
if e == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user