From e496c29101a7af77f6cc058d547d707f2e5f0823 Mon Sep 17 00:00:00 2001 From: "Lennart J. Kurzweg (Nx2)" Date: Thu, 23 Apr 2026 23:09:24 +0200 Subject: [PATCH] yeah --- config.yaml | 24 ------------------ internal/backend/db.go | 18 ++++++++++++++ internal/config/config.go | 13 +++++----- internal/extra/injector.go | 50 +++++++++++++++++++++++++++----------- main.go | 24 +++++++++--------- 5 files changed, 73 insertions(+), 56 deletions(-) diff --git a/config.yaml b/config.yaml index 883eeb0..5c57fcc 100644 --- a/config.yaml +++ b/config.yaml @@ -14,34 +14,10 @@ smtp: password_cmd: echo "Vastly-Wrinkle9-Corsage" users: - - name: alice - password: 123 - groups: - - family - - name: bob - password: abc - groups: - - family calendars: - - id: test - owner: alice - color: '#F6F5F4' - - id: family - owner: shared - color: '#999999' - access: - - group: family - mode: read-write address_books: - - id: contacts - owner: alice - - id: family - owner: bob - access: - - group: family - mode: read-write aggregates: diff --git a/internal/backend/db.go b/internal/backend/db.go index 151d81f..faa8b44 100644 --- a/internal/backend/db.go +++ b/internal/backend/db.go @@ -229,6 +229,7 @@ func (b *DBBackend) syncConfig(ctx context.Context, cfg *config.Config) error { if a.User != "" { tUsers = append(tUsers, a.User) } + tUsers = append(tUsers, a.Users...) if a.Group != "" { tUsers = append(tUsers, groupMembers[a.Group]...) } @@ -299,6 +300,7 @@ func (b *DBBackend) syncConfig(ctx context.Context, cfg *config.Config) error { if a.User != "" { tUsers = append(tUsers, a.User) } + tUsers = append(tUsers, a.Users...) if a.Group != "" { tUsers = append(tUsers, groupMembers[a.Group]...) } @@ -350,6 +352,7 @@ func (b *DBBackend) syncConfig(ctx context.Context, cfg *config.Config) error { if a.User != "" { tUsers = append(tUsers, a.User) } + tUsers = append(tUsers, a.Users...) if a.Group != "" { tUsers = append(tUsers, groupMembers[a.Group]...) } @@ -1465,3 +1468,18 @@ func (b *DBBackend) DeleteAddressObject(ctx context.Context, p string) error { func (b *DBBackend) QueryAddressObjects(ctx context.Context, p string, query *carddav.AddressBookQuery) ([]carddav.AddressObject, error) { return b.ListAddressObjects(ctx, p, nil) } + +func (b *DBBackend) HasAddressBooks(ctx context.Context) (bool, error) { + username, err := b.getUsername(ctx) + if err != nil { + return false, err + } + var exists bool + err = b.pool.QueryRow(ctx, ` + SELECT EXISTS ( + SELECT 1 FROM addressbooks + WHERE owner_id = (SELECT id FROM users WHERE name = $1) + OR id IN (SELECT addressbook_id FROM addressbook_access WHERE user_id = (SELECT id FROM users WHERE name = $1)) + )`, username).Scan(&exists) + return exists, err +} diff --git a/internal/config/config.go b/internal/config/config.go index 5398432..ef0cc6a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,11 +12,12 @@ import ( ) type Access struct { - User string `yaml:"user,omitempty"` - Group string `yaml:"group,omitempty"` - Groups string `yaml:"groups,omitempty"` - Mode string `yaml:"mode"` // "read-only" or "read-write" - ICS string `yaml:"ics,omitempty"` + User string `yaml:"user,omitempty"` + Users []string `yaml:"users,omitempty"` + Group string `yaml:"group,omitempty"` + Groups string `yaml:"groups,omitempty"` + Mode string `yaml:"mode"` // "read-only" or "read-write" + ICS string `yaml:"ics,omitempty"` } type Calendar struct { @@ -85,7 +86,7 @@ type Config struct { func ResolvePassword(password, passwordCmd string) (string, error) { if passwordCmd != "" { - cmd := exec.Command("/bin/sh", "-c", passwordCmd) + cmd := exec.Command("sh", "-c", passwordCmd) out, err := cmd.Output() if err != nil { return "", fmt.Errorf("failed to run password command: %v", err) diff --git a/internal/extra/injector.go b/internal/extra/injector.go index b9ddef2..280e2b8 100644 --- a/internal/extra/injector.go +++ b/internal/extra/injector.go @@ -107,10 +107,23 @@ func InjectColor(r *http.Request, ctx context.Context, handler http.Handler, w h w.Write(body) } -// modify caledar probfind -// -// this again modeled after the Radicale response -// Largly AI generated agian +func HandleDiscoveryOptions(r *http.Request, ctx context.Context, handler http.Handler, w http.ResponseWriter, be *backend.DBBackend) { + buf := &bytes.Buffer{} + rw := &responseWriter{w, buf, http.StatusOK} + handler.ServeHTTP(rw, r.WithContext(ctx)) + + if has, _ := be.HasAddressBooks(ctx); has { + dav := w.Header().Get("DAV") + if dav == "" { + w.Header().Set("DAV", "1, 3, addressbook, calendar-access") + } else if !strings.Contains(dav, "addressbook") { + w.Header().Set("DAV", dav+", addressbook") + } + } + w.WriteHeader(rw.status) + w.Write(buf.Bytes()) +} + func HandleDiscoveryPropfind(r *http.Request, ctx context.Context, handler http.Handler, w http.ResponseWriter, be *backend.DBBackend) { reqBody, _ := io.ReadAll(r.Body) r.Body = io.NopCloser(bytes.NewBuffer(reqBody)) @@ -121,9 +134,19 @@ func HandleDiscoveryPropfind(r *http.Request, ctx context.Context, handler http. body := buf.Bytes() - // 1. Unconditionally add namespaces to root tag - reMultistatus := regexp.MustCompile(`(<[a-zA-Z0-9]*:?multistatus)`) - body = reMultistatus.ReplaceAll(body, []byte(`$1 xmlns:C="urn:ietf:params:xml:ns:caldav" xmlns:card="urn:ietf:params:xml:ns:carddav"`)) + calHome, _ := be.CalendarHomeSetPath(ctx) + cardHome, _ := be.AddressBookHomeSetPath(ctx) + hasAddressBooks, _ := be.HasAddressBooks(ctx) + + // Ensure DAV: addressbook header is present if user has address books + if hasAddressBooks { + dav := w.Header().Get("DAV") + if dav == "" { + w.Header().Set("DAV", "1, 3, addressbook, calendar-access") + } else if !strings.Contains(dav, "addressbook") { + w.Header().Set("DAV", dav+", addressbook") + } + } // 2. Response processing reResponse := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?response.*?>.*?`) @@ -139,10 +162,7 @@ func HandleDiscoveryPropfind(r *http.Request, ctx context.Context, handler http. return resp } - calHome, _ := be.CalendarHomeSetPath(ctx) - cardHome, _ := be.AddressBookHomeSetPath(ctx) - - // Strip these tags ONLY from non-200 propstats + // Strip these tags ONLY from non-200 propstats to avoid duplicates or 404 overrides resp = rePropstat.ReplaceAllFunc(resp, func(ps []byte) []byte { if !reStatusOk.Match(ps) { reTags := regexp.MustCompile(`(?s)<[a-zA-Z0-9:]*(calendar-home-set|addressbook-home-set).*?/>|<[a-zA-Z0-9:]*(calendar-home-set|addressbook-home-set).*?>.*?`) @@ -152,11 +172,13 @@ func HandleDiscoveryPropfind(r *http.Request, ctx context.Context, handler http. }) props := "" + // Inject calendar-home-set if missing (with local namespace definition for safety) if calHome != "" && !strings.Contains(string(resp), "calendar-home-set") { - props += fmt.Sprintf("%s", calHome) + props += fmt.Sprintf("%s", calHome) } - if cardHome != "" && !strings.Contains(string(resp), "addressbook-home-set") { - props += fmt.Sprintf("%s", cardHome) + // Inject addressbook-home-set if missing and user has address books + if hasAddressBooks && cardHome != "" && !strings.Contains(string(resp), "addressbook-home-set") { + props += fmt.Sprintf("%s", cardHome) } if props == "" { diff --git a/main.go b/main.go index 6ea04cf..e857a2f 100644 --- a/main.go +++ b/main.go @@ -106,20 +106,25 @@ func main() { principalPath := prefix + fmt.Sprintf("/%s/", user) ctx := context.WithValue(r.Context(), "principal", principalPath) - // set header for carddav + // set header for carddav if user has address books if slices.Contains([]string{ + "/", prefix + "/", "/.well-known/carddav", prefix + "/.well-known/carddav", principalPath, strings.TrimSuffix(principalPath, "/"), }, r.URL.Path) || strings.Contains(r.URL.Path, "/addressbooks/") { - w.Header().Add("DAV", "addressbook") + if has, _ := be.HasAddressBooks(ctx); has { + w.Header().Add("DAV", "addressbook") + } } - // for caldav discovery + // for caldav and carddav discovery if slices.Contains([]string{ "/.well-known/caldav", prefix + "/.well-known/caldav", + "/.well-known/carddav", + prefix + "/.well-known/carddav", }, r.URL.Path) { http.Redirect(w, r, fmt.Sprintf("%s://%s%s", scheme, r.Host, principalPath), http.StatusMovedPermanently) return @@ -142,19 +147,14 @@ func main() { // catch weird requests } else { - if strings.HasSuffix(r.URL.Path, user+"/") || strings.HasSuffix(r.URL.Path, user) { - // For principal path, use merged discovery handler + if strings.HasSuffix(r.URL.Path, user+"/") || strings.HasSuffix(r.URL.Path, user) || r.URL.Path == "/" || r.URL.Path == prefix+"/" { + // For principal path or root, use merged discovery handler if r.Method == "PROPFIND" { extra.HandleDiscoveryPropfind(r, ctx, caldavHandler, w, be) + } else if r.Method == "OPTIONS" { + extra.HandleDiscoveryOptions(r, ctx, caldavHandler, w, be) } else { caldavHandler.ServeHTTP(w, r.WithContext(ctx)) - // Ensure DAV header includes addressbook for OPTIONS - if r.Method == "OPTIONS" { - dav := w.Header().Get("DAV") - if dav != "" && !strings.Contains(dav, "addressbook") { - w.Header().Set("DAV", dav+", addressbook") - } - } } } else { http.NotFound(w, r)