cleanup
This commit is contained in:
33
config.yaml
33
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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
|
||||
55
main.go
55
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" {
|
||||
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))
|
||||
})
|
||||
|
||||
|
||||
35
test.py
35
test.py
@@ -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}")
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user