tokidoki/storage/filesystem.go
Conrad Hoffmann 1d50d6dad8 Harden mapping from request path to FS path
Put strict checks in place to avoid authenticated users accessing files
outside of their actual storage directory. These checks will need
updating if multiple address books are to be supported.
2022-03-10 16:46:56 +01:00

296 lines
6.7 KiB
Go

package storage
import (
"context"
"crypto/md5"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"github.com/emersion/go-vcard"
"github.com/emersion/go-webdav/carddav"
"git.sr.ht/~sircmpwn/tokidoki/auth"
)
type filesystemBackend struct {
path string
}
var nilBackend carddav.Backend = (*filesystemBackend)(nil)
func NewFilesystem(path string) (carddav.Backend, error) {
info, err := os.Stat(path)
if err != nil {
return nilBackend, fmt.Errorf("failed to create filesystem backend: %s", err.Error())
}
if !info.IsDir() {
return nilBackend, fmt.Errorf("base path for filesystem backend must be a directory")
}
return &filesystemBackend{
path: path,
}, nil
}
func (b *filesystemBackend) pathForContext(ctx context.Context) (string, error) {
raw := ctx.Value(auth.AuthCtxKey)
if raw == nil {
return "", fmt.Errorf("unauthenticated requests are not supported")
}
authCtx, ok := raw.(*auth.AuthContext)
if !ok {
panic("Invalid data in auth context!")
}
//TODO sanitize user name or at least check if valid dir name?
path := filepath.Join(b.path, authCtx.UserName)
_, err := os.Stat(path)
if os.IsNotExist(err) {
err = os.Mkdir(path, 0755)
if err != nil {
return "", fmt.Errorf("error creating '%s': %s", path, err.Error())
}
}
return path, nil
}
func (b *filesystemBackend) safePath(ctx context.Context, path string) (string, error) {
basePath, err := b.pathForContext(ctx)
if err != nil {
return "", err
}
// We are mapping to local filesystem path, so be conservative about what to accept
// TODO this changes once multiple addess books are supported
var valid = regexp.MustCompile(`^/[A-Za-z0-9_-]+(.[a-zA-Z]+)?$`)
if !valid.MatchString(path) {
return "", fmt.Errorf("invalid request path")
}
return filepath.Join(basePath, path), nil
}
func etagForFile(path string) (string, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
csum := md5.Sum(data)
return base64.StdEncoding.EncodeToString(csum[:]), nil
}
func vcardPropFilter(card *vcard.Card, props []string) *vcard.Card {
if card == nil {
return nil
}
if len(props) == 0 {
return card
}
result := make(vcard.Card)
result["VERSION"] = (*card)["VERSION"]
for _, prop := range props {
value, ok := (*card)[prop]
if ok {
result[prop] = value
}
}
return &result
}
func vcardFromFile(path string, propFilter []string) (*vcard.Card, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
dec := vcard.NewDecoder(f)
card, err := dec.Decode()
if err != nil {
return nil, err
}
return vcardPropFilter(&card, propFilter), nil
}
func createDefaultAddressBook(path string) error {
// TODO what should the default address book look like?
defaultAB := carddav.AddressBook{
Path: "/default",
Name: "My contacts",
Description: "Default address book",
MaxResourceSize: 1024,
SupportedAddressData: []carddav.AddressDataType{},
}
blob, err := json.MarshalIndent(defaultAB, "", " ")
if err != nil {
return fmt.Errorf("error creating default address book: %s", err.Error())
}
err = os.WriteFile(path, blob, 0644)
if err != nil {
return fmt.Errorf("error writing default address book: %s", err.Error())
}
return nil
}
func (b *filesystemBackend) AddressBook(ctx context.Context) (*carddav.AddressBook, error) {
path, err := b.pathForContext(ctx)
if err != nil {
return nil, err
}
path = filepath.Join(path, "default.json")
_, err = os.Stat(path)
if os.IsNotExist(err) {
err = createDefaultAddressBook(path)
if err != nil {
return nil, err
}
} else if err != nil {
return nil, fmt.Errorf("error opening address book: %s", err.Error())
}
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("error opening address book: %s", err.Error())
}
var addressBook carddav.AddressBook
err = json.Unmarshal(data, &addressBook)
if err != nil {
return nil, fmt.Errorf("error reading address book: %s", err.Error())
}
return &addressBook, nil
}
func (b *filesystemBackend) GetAddressObject(ctx context.Context, path string, req *carddav.AddressDataRequest) (*carddav.AddressObject, error) {
localPath, err := b.safePath(ctx, path)
if err != nil {
return nil, err
}
info, err := os.Stat(localPath)
if err != nil {
return nil, err
}
propFilter := []string{}
if req != nil && !req.AllProp {
propFilter = req.Props
}
card, err := vcardFromFile(localPath, propFilter)
if err != nil {
return nil, err
}
etag, err := etagForFile(localPath)
if err != nil {
return nil, err
}
obj := carddav.AddressObject{
Path: path,
ModTime: info.ModTime(),
ETag: etag,
Card: *card,
}
return &obj, nil
}
func (b *filesystemBackend) loadAll(ctx context.Context, propFilter []string) ([]carddav.AddressObject, error) {
result := []carddav.AddressObject{}
path, err := b.pathForContext(ctx)
if err != nil {
return result, err
}
err = filepath.Walk(path, func(filename string, info os.FileInfo, err error) error {
// Skip address book meta data files
if !info.Mode().IsRegular() || filepath.Ext(filename) == ".json" {
return nil
}
card, err := vcardFromFile(filename, propFilter)
if err != nil {
return err
}
etag, err := etagForFile(filename)
if err != nil {
return err
}
obj := carddav.AddressObject{
Path: "/" + filepath.Base(filename),
ModTime: info.ModTime(),
ETag: etag,
Card: *card,
}
result = append(result, obj)
return nil
})
return result, err
}
func (b *filesystemBackend) ListAddressObjects(ctx context.Context, req *carddav.AddressDataRequest) ([]carddav.AddressObject, error) {
propFilter := []string{}
if req != nil && !req.AllProp {
propFilter = req.Props
}
return b.loadAll(ctx, propFilter)
}
func (b *filesystemBackend) QueryAddressObjects(ctx context.Context, query *carddav.AddressBookQuery) ([]carddav.AddressObject, error) {
propFilter := []string{}
if query != nil && !query.DataRequest.AllProp {
propFilter = query.DataRequest.Props
}
result, err := b.loadAll(ctx, propFilter)
if err != nil {
return result, err
}
return carddav.Filter(query, result)
}
func (b *filesystemBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card) (loc string, err error) {
localPath, err := b.safePath(ctx, path)
if err != nil {
return "", err
}
f, err := os.OpenFile(localPath, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return "", err
}
defer f.Close()
enc := vcard.NewEncoder(f)
err = enc.Encode(card)
if err != nil {
return "", err
}
return path, nil
}
func (b *filesystemBackend) DeleteAddressObject(ctx context.Context, path string) error {
localPath, err := b.safePath(ctx, path)
if err != nil {
return err
}
err = os.Remove(localPath)
if err != nil {
return err
}
return nil
}