From b5a3a63dde4886335bf8063b654288ce54357bf7 Mon Sep 17 00:00:00 2001 From: "Lennart J. Kurzweg (Nx2)" Date: Mon, 23 Mar 2026 22:06:12 +0100 Subject: [PATCH] cleanup --- config.yaml | 33 ++++++----------------- internal/backend/db.go | 18 ++++++++----- internal/config/config.go | 10 +++++++ main.go | 57 ++++++++++++++++++++++++++++++++++----- test.py | 35 ------------------------ verify_group.py | 39 --------------------------- 6 files changed, 80 insertions(+), 112 deletions(-) delete mode 100644 test.py delete mode 100644 verify_group.py diff --git a/config.yaml b/config.yaml index c72f321..42073cd 100644 --- a/config.yaml +++ b/config.yaml @@ -1,6 +1,6 @@ server: bind_address: "0.0.0.0:14243" - public_url: "http://nxc.nx2.site" + public_url: "http://localhost:8080" redaction_text: "[-]" default_class: "CONFIDENTIAL" @@ -9,31 +9,19 @@ database: users: - name: "daniel" - password: "123" + password: "Cyclist-Hypnotize7-Blurb" groups: - family - - parent - name: "diane" - password: "123" - groups: - - family - - parent - - name: "georg" - password: "123" + password: "Carve-Unluckily-Reprint1" groups: - family - name: "lennart" - password: "123" + password: "Baton6-Extortion-Monologue" groups: - family - - name: "tessa" - password: "123" - groups: - - family - - name: "testuser" - password: "123" - name: "shared" - password: "123" + password: "Oxidant-Ageless3-Dispersed" calendars: - id: "default" @@ -43,17 +31,12 @@ calendars: access: - groups: "family" mode: "read-write" - - id: "tessas-inbox" - owner: "tessa" - access: - - group: "parent" - mode: "read-write" aggregates: - id: "lennart_aggregate" - owner: "lennart" - sources: ["default", "family"] + owner: "shared" + sources: [ "default", "family" ] access: - - group: "family" + - group: "diane" mode: "read-only" - ics: "future-only" diff --git a/internal/backend/db.go b/internal/backend/db.go index 9bd7913..71708a1 100644 --- a/internal/backend/db.go +++ b/internal/backend/db.go @@ -32,6 +32,7 @@ type publicInfo struct { // access control, privacy redaction, and aggregate (virtual) calendars. type DBBackend struct { pool *pgxpool.Pool // Connection pool to PostgreSQL + prefix string // Public URL base path prefix redactionText string // Text used to hide confidential event details (e.g. "[REDACED]") defaultClass string // Class assumed if non is set ("PUBLIC", "PRIVATE", "CONFIDENTIAL") aggregates map[string]*config.Aggregate // In-memory map of path -> virtual calendar definitions @@ -51,6 +52,7 @@ func NewDBBackend(ctx context.Context, cfg *config.Config) (*DBBackend, error) { b := &DBBackend{ pool: pool, + prefix: cfg.Server.BasePath(), redactionText: cfg.Server.Redaction, defaultClass: cfg.Server.DefaultClass, aggregates: make(map[string]*config.Aggregate), @@ -163,6 +165,8 @@ func (b *DBBackend) syncConfig(ctx context.Context, cfg *config.Config) error { b.userAggs = make(map[string][]string) b.publicAccess = make(map[string]publicInfo) + prefix := cfg.Server.BasePath() + // --- Phase 1: User Sync --- for _, u := range cfg.Users { configUserNames[u.Name] = true @@ -185,7 +189,7 @@ func (b *DBBackend) syncConfig(ctx context.Context, cfg *config.Config) error { // --- Phase 2: Calendar & Access Sync --- for _, c := range cfg.Calendars { - path := fmt.Sprintf("/%s/calendars/%s/", c.Owner, c.ID) + path := prefix + fmt.Sprintf("/%s/calendars/%s/", c.Owner, c.ID) configCalendarPaths[path] = true var ownerID int @@ -243,7 +247,7 @@ func (b *DBBackend) syncConfig(ctx context.Context, cfg *config.Config) error { // Check for public access (ICS) for _, a := range c.Access { if a.ICS != "" { - pubPath := fmt.Sprintf("/public/%s/%s.ics", c.Owner, c.ID) + pubPath := prefix + fmt.Sprintf("/public/%s/%s.ics", c.Owner, c.ID) b.publicAccess[pubPath] = publicInfo{ InternalPath: path, Mode: a.ICS, @@ -255,7 +259,7 @@ func (b *DBBackend) syncConfig(ctx context.Context, cfg *config.Config) error { // --- Phase 3: Aggregate Setup --- // Aggregates are virtual, so we only track them in memory for routing. for _, agg := range cfg.Aggregates { - p_ := fmt.Sprintf("/%s/calendars/%s/", agg.Owner, agg.ID) + p_ := prefix + fmt.Sprintf("/%s/calendars/%s/", agg.Owner, agg.ID) if configCalendarPaths[p_] { return fmt.Errorf("aggregate %s collides with real calendar path", p_) } @@ -267,7 +271,7 @@ func (b *DBBackend) syncConfig(ctx context.Context, cfg *config.Config) error { aggAccess[agg.Owner] = true for _, a := range agg.Access { if a.ICS != "" { - pubPath := fmt.Sprintf("/public/%s/%s.ics", agg.Owner, agg.ID) + pubPath := prefix + fmt.Sprintf("/public/%s/%s.ics", agg.Owner, agg.ID) b.publicAccess[pubPath] = publicInfo{ InternalPath: p_, Mode: a.ICS, @@ -548,7 +552,7 @@ func (b *DBBackend) CalendarHomeSetPath(ctx context.Context) (string, error) { if err != nil { return "", err } - return fmt.Sprintf("/%s/calendars/", username), nil + return b.prefix + fmt.Sprintf("/%s/calendars/", username), nil } // ListCalendars returns all calendars (real and virtual) the user has access to. @@ -699,7 +703,7 @@ func (b *DBBackend) listAggregateObjects(ctx context.Context, aggPath string, ag var res []caldav.CalendarObject for _, sourceID := range agg.Sources { - sourcePath := fmt.Sprintf("/%s/calendars/%s/", agg.Owner, sourceID) + sourcePath := b.prefix + fmt.Sprintf("/%s/calendars/%s/", agg.Owner, sourceID) var calID int var ownerName string @@ -763,7 +767,7 @@ func (b *DBBackend) GetCalendarObject(ctx context.Context, p string, req *caldav sourceID := parts[0] realFileName := parts[1] - sourcePath := fmt.Sprintf("/%s/calendars/%s/", agg.Owner, sourceID) + sourcePath := b.prefix + fmt.Sprintf("/%s/calendars/%s/", agg.Owner, sourceID) var calID int var ownerName string err := b.pool.QueryRow(ctx, "SELECT c.id, u.name FROM calendars c JOIN users u ON c.owner_id = u.id WHERE c.path = $1", sourcePath).Scan(&calID, &ownerName) diff --git a/internal/config/config.go b/internal/config/config.go index 95ac938..e0b51cd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,8 +1,10 @@ package config import ( + "net/url" "os" "slices" + "strings" "gopkg.in/yaml.v3" ) @@ -46,6 +48,14 @@ type ServerConfig struct { DefaultClass string `yaml:"default_class"` } +func (s ServerConfig) BasePath() string { + u, err := url.Parse(s.PublicURL) + if err != nil { + return "" + } + return strings.TrimSuffix(u.Path, "/") +} + type Config struct { Server ServerConfig `yaml:"server"` Database DatabaseConfig `yaml:"database"` diff --git a/main.go b/main.go index 969db37..f9e0e5f 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,9 @@ import ( "context" "fmt" "log" + "os" "net/http" + "net/url" "strings" "time" @@ -14,7 +16,13 @@ import ( ) func main() { - cfg, err := config.Load("config.yaml") + path := "config.yaml"; + if len(os.Args) != 0 { + if os.Args[1] == "-c" { + path = os.Args[2] + } + } + cfg, err := config.Load() if err != nil { log.Fatalf("failed to load config: %v", err) } @@ -27,8 +35,34 @@ func main() { handler := &caldav.Handler{Backend: be} + publicURL, _ := url.Parse(cfg.Server.PublicURL) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.URL.Path, "/public/") { + // Proxy-aware normalization: + if publicURL != nil && publicURL.Host != "" { + r.Host = publicURL.Host + r.URL.Host = publicURL.Host + + // Detect scheme: prioritize X-Forwarded-Proto, then PublicURL + scheme := publicURL.Scheme + if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" { + scheme = proto + } + r.URL.Scheme = scheme + + // Also rewrite WebDAV Destination header (used for MOVE/COPY) + if dest := r.Header.Get("Destination"); dest != "" { + destURL, err := url.Parse(dest) + if err == nil { + destURL.Host = publicURL.Host + destURL.Scheme = scheme + r.Header.Set("Destination", destURL.String()) + } + } + } + + prefix := cfg.Server.BasePath() + if strings.HasPrefix(r.URL.Path, prefix+"/public/") { be.ServePublicICS(w, r) return } @@ -55,15 +89,26 @@ func main() { } log.Printf("%s %s (user: %s)", r.Method, r.URL.Path, user) - - principalPath := fmt.Sprintf("/%s/", user) + prefix = cfg.Server.BasePath() + principalPath := prefix + fmt.Sprintf("/%s/", user) ctx := context.WithValue(r.Context(), "principal", principalPath) - if r.URL.Path == "/.well-known/caldav" { - http.Redirect(w, r, principalPath, http.StatusMovedPermanently) + if r.URL.Path == "/.well-known/caldav" || r.URL.Path == prefix+"/.well-known/caldav" { + // If we normalized the request, use the normalized host/scheme for the redirect + if publicURL != nil && publicURL.Host != "" { + scheme := r.URL.Scheme + if scheme == "" { + scheme = "http" + } + target := fmt.Sprintf("%s://%s%s", scheme, r.Host, principalPath) + http.Redirect(w, r, target, http.StatusMovedPermanently) + } else { + http.Redirect(w, r, principalPath, http.StatusMovedPermanently) + } return } + handler.ServeHTTP(w, r.WithContext(ctx)) }) diff --git a/test.py b/test.py deleted file mode 100644 index ecd50fe..0000000 --- a/test.py +++ /dev/null @@ -1,35 +0,0 @@ -import os -import yaml -from caldav import DAVClient -from ics import Calendar -from datetime import datetime - -if __name__ == "__main__": - with open("config.yaml") as f: - config = yaml.safe_load(f) - - url = "http://localhost:8080/" - - # Diane checks the aggregate 'lennart' calendar - diane_client = DAVClient(url, username="diane", password="123") - d_principal = diane_client.principal() - - print("\n--- Diane viewing Aggregate 'lennart' calendar ---") - agg_path = "/lennart/calendars/lennart/" - lennart_agg = next(c for c in d_principal.calendars() if agg_path in c.url.path) - - events = lennart_agg.events() - print(f"Events found in aggregate: {len(events)}") - for e in events: - print(f" - Path: {e.url.path}") - if not e.url.path.startswith(agg_path): - print(f" ERROR: Path {e.url.path} is NOT under the aggregate {agg_path}!") - - # Test individual GET (GetCalendarObject) - try: - data = e.data - c = Calendar(data) - for ev in c.events: - print(f" Fetched: {ev.name} (UID: {ev.uid})") - except Exception as err: - print(f" ERROR fetching individual item: {err}") diff --git a/verify_group.py b/verify_group.py deleted file mode 100644 index e1cb7b3..0000000 --- a/verify_group.py +++ /dev/null @@ -1,39 +0,0 @@ -import psycopg2 -import sys - -def check_db(): - try: - conn = psycopg2.connect("postgres://nxcaldav@localhost:5432/nxcaldav") - cur = conn.cursor() - - print("--- Users ---") - cur.execute("SELECT id, name FROM users") - users = cur.fetchall() - for u in users: - print(u) - - print("\n--- Calendars ---") - cur.execute("SELECT id, path, owner_id FROM calendars") - cals = cur.fetchall() - for c in cals: - print(c) - - print("\n--- Calendar Access (Diane) ---") - cur.execute(""" - SELECT c.path, ca.mode - FROM calendar_access ca - JOIN calendars c ON ca.calendar_id = c.id - JOIN users u ON ca.user_id = u.id - WHERE u.name = 'diane' - """) - access = cur.fetchall() - for a in access: - print(a) - - cur.close() - conn.close() - except Exception as e: - print(f"Error: {e}") - -if __name__ == "__main__": - check_db()