319 lines
8.0 KiB
Go
319 lines
8.0 KiB
Go
package backend
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/emersion/go-ical"
|
|
"github.com/emersion/go-webdav"
|
|
"github.com/emersion/go-webdav/caldav"
|
|
"nxcaldav/internal/config"
|
|
)
|
|
|
|
type MemBackend struct {
|
|
sync.RWMutex
|
|
allCalendars map[string]*caldav.Calendar
|
|
objects map[string]map[string]*caldav.CalendarObject
|
|
userCalendars map[string][]string
|
|
calendarOwner map[string]string
|
|
calendarAccess map[string]map[string]string
|
|
}
|
|
|
|
func NewMemBackend(cfg *config.Config) *MemBackend {
|
|
b := &MemBackend{
|
|
allCalendars: make(map[string]*caldav.Calendar),
|
|
objects: make(map[string]map[string]*caldav.CalendarObject),
|
|
userCalendars: make(map[string][]string),
|
|
calendarOwner: make(map[string]string),
|
|
calendarAccess: make(map[string]map[string]string),
|
|
}
|
|
|
|
for _, c := range cfg.Calendars {
|
|
path := fmt.Sprintf("/%s/calendars/%s/", c.Owner, c.ID)
|
|
cal := &caldav.Calendar{
|
|
Path: path,
|
|
Name: c.ID,
|
|
SupportedComponentSet: []string{"VEVENT", "VTODO"},
|
|
}
|
|
b.allCalendars[path] = cal
|
|
b.calendarOwner[path] = c.Owner
|
|
b.userCalendars[c.Owner] = append(b.userCalendars[c.Owner], path)
|
|
|
|
accessMap := make(map[string]string)
|
|
for _, a := range c.Access {
|
|
accessMap[a.User] = a.Mode
|
|
b.userCalendars[a.User] = append(b.userCalendars[a.User], path)
|
|
}
|
|
b.calendarAccess[path] = accessMap
|
|
}
|
|
|
|
return b
|
|
}
|
|
|
|
func (b *MemBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
|
|
principal, ok := ctx.Value("principal").(string)
|
|
if !ok {
|
|
return "", errors.New("no principal in context")
|
|
}
|
|
return principal, nil
|
|
}
|
|
|
|
func (b *MemBackend) getUsername(ctx context.Context) (string, error) {
|
|
principal, err := b.CurrentUserPrincipal(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.Trim(principal, "/"), nil
|
|
}
|
|
|
|
func (b *MemBackend) checkAccess(ctx context.Context, calendarPath string, requiredMode string) error {
|
|
username, err := b.getUsername(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !strings.HasSuffix(calendarPath, "/") {
|
|
calendarPath += "/"
|
|
}
|
|
|
|
owner := b.calendarOwner[calendarPath]
|
|
if owner == username {
|
|
return nil
|
|
}
|
|
|
|
mode, ok := b.calendarAccess[calendarPath][username]
|
|
if !ok {
|
|
return webdav.NewHTTPError(http.StatusForbidden, errors.New("access denied"))
|
|
}
|
|
|
|
if requiredMode == "write" && mode != "read-write" {
|
|
return webdav.NewHTTPError(http.StatusForbidden, errors.New("read-only access"))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *MemBackend) filterCalendar(ctx context.Context, calendarPath string, original *ical.Calendar) (*ical.Calendar, error) {
|
|
username, err := b.getUsername(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !strings.HasSuffix(calendarPath, "/") {
|
|
calendarPath += "/"
|
|
}
|
|
|
|
owner := b.calendarOwner[calendarPath]
|
|
if username == owner {
|
|
return original, nil
|
|
}
|
|
|
|
filtered := ical.NewCalendar()
|
|
filtered.Props = original.Props
|
|
|
|
for _, child := range original.Children {
|
|
if child.Name != "VEVENT" && child.Name != "VTODO" {
|
|
filtered.Children = append(filtered.Children, child)
|
|
continue
|
|
}
|
|
|
|
class := "PUBLIC"
|
|
if prop := child.Props.Get("CLASS"); prop != nil {
|
|
class = strings.ToUpper(prop.Value)
|
|
}
|
|
|
|
switch class {
|
|
case "PRIVATE":
|
|
continue
|
|
case "CONFIDENTIAL":
|
|
redacted := ical.NewComponent(child.Name)
|
|
propsToKeep := []string{
|
|
"UID", "DTSTAMP", "DTSTART", "DTEND", "DURATION", "CLASS",
|
|
"DUE", "COMPLETED", "STATUS", "PRIORITY", "PERCENT-COMPLETE",
|
|
}
|
|
for _, pName := range propsToKeep {
|
|
if props, ok := child.Props[pName]; ok {
|
|
redacted.Props[pName] = props
|
|
}
|
|
}
|
|
redacted.Props.SetText("SUMMARY", "Busy")
|
|
filtered.Children = append(filtered.Children, redacted)
|
|
default:
|
|
filtered.Children = append(filtered.Children, child)
|
|
}
|
|
}
|
|
|
|
if len(filtered.Children) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return filtered, nil
|
|
}
|
|
|
|
// CalDAV Backend implementation
|
|
|
|
func (b *MemBackend) CalendarHomeSetPath(ctx context.Context) (string, error) {
|
|
username, err := b.getUsername(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("/%s/calendars/", username), nil
|
|
}
|
|
|
|
func (b *MemBackend) ListCalendars(ctx context.Context) ([]caldav.Calendar, error) {
|
|
username, err := b.getUsername(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b.RLock()
|
|
defer b.RUnlock()
|
|
|
|
paths := b.userCalendars[username]
|
|
res := make([]caldav.Calendar, 0, len(paths))
|
|
for _, p := range paths {
|
|
if cal, ok := b.allCalendars[p]; ok {
|
|
res = append(res, *cal)
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (b *MemBackend) GetCalendar(ctx context.Context, path string) (*caldav.Calendar, error) {
|
|
b.RLock()
|
|
defer b.RUnlock()
|
|
|
|
if !strings.HasSuffix(path, "/") {
|
|
path += "/"
|
|
}
|
|
|
|
if cal, ok := b.allCalendars[path]; ok {
|
|
return cal, nil
|
|
}
|
|
return nil, webdav.NewHTTPError(http.StatusNotFound, errors.New("calendar not found"))
|
|
}
|
|
|
|
func (b *MemBackend) CreateCalendar(ctx context.Context, calendar *caldav.Calendar) error {
|
|
return webdav.NewHTTPError(http.StatusForbidden, errors.New("calendar creation only via config"))
|
|
}
|
|
|
|
func (b *MemBackend) ListCalendarObjects(ctx context.Context, path string, req *caldav.CalendarCompRequest) ([]caldav.CalendarObject, error) {
|
|
if err := b.checkAccess(ctx, path, "read"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b.RLock()
|
|
defer b.RUnlock()
|
|
|
|
if !strings.HasSuffix(path, "/") {
|
|
path += "/"
|
|
}
|
|
|
|
objs, ok := b.objects[path]
|
|
if !ok {
|
|
return []caldav.CalendarObject{}, nil
|
|
}
|
|
|
|
res := make([]caldav.CalendarObject, 0, len(objs))
|
|
for _, obj := range objs {
|
|
filteredData, err := b.filterCalendar(ctx, path, obj.Data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if filteredData == nil {
|
|
continue
|
|
}
|
|
newObj := *obj
|
|
newObj.Data = filteredData
|
|
res = append(res, newObj)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (b *MemBackend) GetCalendarObject(ctx context.Context, path string, req *caldav.CalendarCompRequest) (*caldav.CalendarObject, error) {
|
|
lastSlash := strings.LastIndex(path, "/")
|
|
if lastSlash == -1 {
|
|
return nil, webdav.NewHTTPError(http.StatusBadRequest, errors.New("invalid object path"))
|
|
}
|
|
parentPath := path[:lastSlash+1]
|
|
|
|
if err := b.checkAccess(ctx, parentPath, "read"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b.RLock()
|
|
defer b.RUnlock()
|
|
|
|
if objs, ok := b.objects[parentPath]; ok {
|
|
if obj, ok := objs[path]; ok {
|
|
filteredData, err := b.filterCalendar(ctx, parentPath, obj.Data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if filteredData == nil {
|
|
return nil, webdav.NewHTTPError(http.StatusNotFound, errors.New("calendar object not found"))
|
|
}
|
|
newObj := *obj
|
|
newObj.Data = filteredData
|
|
return &newObj, nil
|
|
}
|
|
}
|
|
return nil, webdav.NewHTTPError(http.StatusNotFound, errors.New("calendar object not found"))
|
|
}
|
|
|
|
func (b *MemBackend) PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *caldav.PutCalendarObjectOptions) (*caldav.CalendarObject, error) {
|
|
lastSlash := strings.LastIndex(path, "/")
|
|
if lastSlash == -1 {
|
|
return nil, webdav.NewHTTPError(http.StatusBadRequest, errors.New("invalid object path"))
|
|
}
|
|
parentPath := path[:lastSlash+1]
|
|
|
|
if err := b.checkAccess(ctx, parentPath, "write"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b.Lock()
|
|
defer b.Unlock()
|
|
|
|
obj := &caldav.CalendarObject{
|
|
Path: path,
|
|
Data: calendar,
|
|
ETag: fmt.Sprintf(`"%d"`, len(calendar.Events())),
|
|
}
|
|
|
|
if _, ok := b.objects[parentPath]; !ok {
|
|
b.objects[parentPath] = make(map[string]*caldav.CalendarObject)
|
|
}
|
|
b.objects[parentPath][path] = obj
|
|
return obj, nil
|
|
}
|
|
|
|
func (b *MemBackend) DeleteCalendarObject(ctx context.Context, path string) error {
|
|
lastSlash := strings.LastIndex(path, "/")
|
|
if lastSlash == -1 {
|
|
return webdav.NewHTTPError(http.StatusBadRequest, errors.New("invalid object path"))
|
|
}
|
|
parentPath := path[:lastSlash+1]
|
|
|
|
if err := b.checkAccess(ctx, parentPath, "write"); err != nil {
|
|
return err
|
|
}
|
|
|
|
b.Lock()
|
|
defer b.Unlock()
|
|
|
|
if objs, ok := b.objects[parentPath]; ok {
|
|
delete(objs, path)
|
|
return nil
|
|
}
|
|
return webdav.NewHTTPError(http.StatusNotFound, errors.New("calendar object not found"))
|
|
}
|
|
|
|
func (b *MemBackend) QueryCalendarObjects(ctx context.Context, path string, query *caldav.CalendarQuery) ([]caldav.CalendarObject, error) {
|
|
return b.ListCalendarObjects(ctx, path, nil)
|
|
}
|