2022-02-22 11:14:19 +00:00
|
|
|
package storage
|
|
|
|
|
|
|
|
import (
|
2022-03-16 13:47:47 +00:00
|
|
|
"crypto/sha1"
|
2022-02-24 11:54:30 +00:00
|
|
|
"encoding/base64"
|
2022-02-22 17:24:17 +00:00
|
|
|
"fmt"
|
2022-03-16 13:47:47 +00:00
|
|
|
"io"
|
2022-02-22 17:24:17 +00:00
|
|
|
"os"
|
2022-03-23 09:38:14 +00:00
|
|
|
"path"
|
2022-02-22 17:24:17 +00:00
|
|
|
"path/filepath"
|
2022-03-10 15:46:56 +00:00
|
|
|
"regexp"
|
2022-05-03 16:00:07 +01:00
|
|
|
"strings"
|
2022-02-22 17:24:17 +00:00
|
|
|
|
2022-12-01 11:00:42 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
|
2022-03-23 09:38:14 +00:00
|
|
|
"github.com/emersion/go-webdav"
|
|
|
|
"github.com/emersion/go-webdav/caldav"
|
2022-02-22 11:14:19 +00:00
|
|
|
"github.com/emersion/go-webdav/carddav"
|
|
|
|
)
|
|
|
|
|
|
|
|
type filesystemBackend struct {
|
2022-05-24 11:39:27 +01:00
|
|
|
webdav.UserPrincipalBackend
|
|
|
|
path string
|
|
|
|
caldavPrefix string
|
|
|
|
carddavPrefix string
|
2022-02-22 11:14:19 +00:00
|
|
|
}
|
|
|
|
|
2022-03-16 13:37:21 +00:00
|
|
|
var (
|
2022-12-01 10:06:15 +00:00
|
|
|
validFilenameRegex = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]+(.[a-zA-Z]+)?$`)
|
|
|
|
defaultResourceName = "default"
|
2022-03-16 13:37:21 +00:00
|
|
|
)
|
2022-02-22 11:14:19 +00:00
|
|
|
|
2022-03-23 09:38:14 +00:00
|
|
|
func NewFilesystem(fsPath, caldavPrefix, carddavPrefix string, userPrincipalBackend webdav.UserPrincipalBackend) (caldav.Backend, carddav.Backend, error) {
|
|
|
|
info, err := os.Stat(fsPath)
|
2022-02-22 17:24:17 +00:00
|
|
|
if err != nil {
|
2022-03-23 09:38:14 +00:00
|
|
|
return nil, nil, fmt.Errorf("failed to create filesystem backend: %s", err.Error())
|
2022-02-22 17:24:17 +00:00
|
|
|
}
|
|
|
|
if !info.IsDir() {
|
2022-03-23 09:38:14 +00:00
|
|
|
return nil, nil, fmt.Errorf("base path for filesystem backend must be a directory")
|
2022-02-22 17:24:17 +00:00
|
|
|
}
|
2022-03-23 09:38:14 +00:00
|
|
|
backend := &filesystemBackend{
|
2022-05-24 11:39:27 +01:00
|
|
|
UserPrincipalBackend: userPrincipalBackend,
|
2022-03-23 09:38:14 +00:00
|
|
|
path: fsPath,
|
|
|
|
caldavPrefix: caldavPrefix,
|
|
|
|
carddavPrefix: carddavPrefix,
|
|
|
|
}
|
|
|
|
return backend, backend, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func ensureLocalDir(path string) error {
|
|
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
|
|
err = os.MkdirAll(path, 0755)
|
2022-02-22 17:24:17 +00:00
|
|
|
if err != nil {
|
2022-03-23 09:38:14 +00:00
|
|
|
return fmt.Errorf("error creating '%s': %s", path, err.Error())
|
2022-02-22 17:24:17 +00:00
|
|
|
}
|
|
|
|
}
|
2022-03-23 09:38:14 +00:00
|
|
|
return nil
|
2022-02-22 17:24:17 +00:00
|
|
|
}
|
|
|
|
|
2024-02-02 21:19:36 +00:00
|
|
|
func (b *filesystemBackend) localDir(homeSetPath string, components ...string) (string, error) {
|
|
|
|
c := append([]string{b.path}, homeSetPath)
|
|
|
|
c = append(c, components...)
|
|
|
|
localPath := filepath.Join(c...)
|
|
|
|
if err := ensureLocalDir(localPath); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return localPath, nil
|
|
|
|
}
|
|
|
|
|
2022-03-23 09:38:14 +00:00
|
|
|
// don't use this directly, use localCalDAVPath or localCardDAVPath instead.
|
2024-02-02 21:19:36 +00:00
|
|
|
// note that homesetpath is expected to end in /
|
2022-03-23 09:38:14 +00:00
|
|
|
func (b *filesystemBackend) safeLocalPath(homeSetPath string, urlPath string) (string, error) {
|
2024-02-02 21:19:36 +00:00
|
|
|
localPath := filepath.Join(b.path, homeSetPath)
|
2022-03-23 09:38:14 +00:00
|
|
|
if err := ensureLocalDir(localPath); err != nil {
|
2022-03-10 15:46:56 +00:00
|
|
|
return "", err
|
|
|
|
}
|
2022-03-23 09:38:14 +00:00
|
|
|
|
|
|
|
if urlPath == "" {
|
|
|
|
return localPath, nil
|
|
|
|
}
|
|
|
|
|
2022-03-10 15:46:56 +00:00
|
|
|
// We are mapping to local filesystem path, so be conservative about what to accept
|
2024-02-02 21:19:36 +00:00
|
|
|
if strings.HasSuffix(urlPath, "/") {
|
|
|
|
urlPath = path.Clean(urlPath) + "/"
|
|
|
|
} else {
|
|
|
|
urlPath = path.Clean(urlPath)
|
|
|
|
}
|
|
|
|
if !strings.HasPrefix(urlPath, homeSetPath) {
|
|
|
|
err := fmt.Errorf("access to resource outside of home set: %s", urlPath)
|
|
|
|
return "", webdav.NewHTTPError(403, err)
|
2022-03-23 09:38:14 +00:00
|
|
|
}
|
2024-02-02 21:19:36 +00:00
|
|
|
urlPath = strings.TrimPrefix(urlPath, homeSetPath)
|
|
|
|
|
2022-03-23 09:38:14 +00:00
|
|
|
// only accept simple file names for now
|
2024-02-02 21:19:36 +00:00
|
|
|
dir, file := path.Split(urlPath)
|
|
|
|
if file != "" && !validFilenameRegex.MatchString(file) {
|
2022-12-01 11:00:42 +00:00
|
|
|
log.Debug().Str("file", file).Msg("file name does not match regex")
|
2022-05-03 16:00:07 +01:00
|
|
|
err := fmt.Errorf("invalid file name: %s", file)
|
|
|
|
return "", webdav.NewHTTPError(400, err)
|
2022-03-23 09:38:14 +00:00
|
|
|
}
|
|
|
|
|
2024-02-02 21:19:36 +00:00
|
|
|
return filepath.Join(localPath, dir, file), nil
|
2022-03-23 09:38:14 +00:00
|
|
|
}
|
|
|
|
|
2022-02-24 11:54:30 +00:00
|
|
|
func etagForFile(path string) (string, error) {
|
2022-03-16 13:47:47 +00:00
|
|
|
f, err := os.Open(path)
|
2022-02-24 11:54:30 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2022-03-16 13:47:47 +00:00
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
h := sha1.New()
|
|
|
|
if _, err := io.Copy(h, f); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
csum := h.Sum(nil)
|
|
|
|
|
2022-02-24 11:54:30 +00:00
|
|
|
return base64.StdEncoding.EncodeToString(csum[:]), nil
|
|
|
|
}
|