cleanup
This commit is contained in:
33
config.yaml
33
config.yaml
@@ -1,6 +1,6 @@
|
|||||||
server:
|
server:
|
||||||
bind_address: "0.0.0.0:14243"
|
bind_address: "0.0.0.0:14243"
|
||||||
public_url: "http://nxc.nx2.site"
|
public_url: "http://localhost:8080"
|
||||||
redaction_text: "[-]"
|
redaction_text: "[-]"
|
||||||
default_class: "CONFIDENTIAL"
|
default_class: "CONFIDENTIAL"
|
||||||
|
|
||||||
@@ -9,31 +9,19 @@ database:
|
|||||||
|
|
||||||
users:
|
users:
|
||||||
- name: "daniel"
|
- name: "daniel"
|
||||||
password: "123"
|
password: "Cyclist-Hypnotize7-Blurb"
|
||||||
groups:
|
groups:
|
||||||
- family
|
- family
|
||||||
- parent
|
|
||||||
- name: "diane"
|
- name: "diane"
|
||||||
password: "123"
|
password: "Carve-Unluckily-Reprint1"
|
||||||
groups:
|
|
||||||
- family
|
|
||||||
- parent
|
|
||||||
- name: "georg"
|
|
||||||
password: "123"
|
|
||||||
groups:
|
groups:
|
||||||
- family
|
- family
|
||||||
- name: "lennart"
|
- name: "lennart"
|
||||||
password: "123"
|
password: "Baton6-Extortion-Monologue"
|
||||||
groups:
|
groups:
|
||||||
- family
|
- family
|
||||||
- name: "tessa"
|
|
||||||
password: "123"
|
|
||||||
groups:
|
|
||||||
- family
|
|
||||||
- name: "testuser"
|
|
||||||
password: "123"
|
|
||||||
- name: "shared"
|
- name: "shared"
|
||||||
password: "123"
|
password: "Oxidant-Ageless3-Dispersed"
|
||||||
|
|
||||||
calendars:
|
calendars:
|
||||||
- id: "default"
|
- id: "default"
|
||||||
@@ -43,17 +31,12 @@ calendars:
|
|||||||
access:
|
access:
|
||||||
- groups: "family"
|
- groups: "family"
|
||||||
mode: "read-write"
|
mode: "read-write"
|
||||||
- id: "tessas-inbox"
|
|
||||||
owner: "tessa"
|
|
||||||
access:
|
|
||||||
- group: "parent"
|
|
||||||
mode: "read-write"
|
|
||||||
|
|
||||||
aggregates:
|
aggregates:
|
||||||
- id: "lennart_aggregate"
|
- id: "lennart_aggregate"
|
||||||
owner: "lennart"
|
owner: "shared"
|
||||||
sources: ["default", "family"]
|
sources: [ "default", "family" ]
|
||||||
access:
|
access:
|
||||||
- group: "family"
|
- group: "diane"
|
||||||
mode: "read-only"
|
mode: "read-only"
|
||||||
- ics: "future-only"
|
- ics: "future-only"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type publicInfo struct {
|
|||||||
// access control, privacy redaction, and aggregate (virtual) calendars.
|
// access control, privacy redaction, and aggregate (virtual) calendars.
|
||||||
type DBBackend struct {
|
type DBBackend struct {
|
||||||
pool *pgxpool.Pool // Connection pool to PostgreSQL
|
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]")
|
redactionText string // Text used to hide confidential event details (e.g. "[REDACED]")
|
||||||
defaultClass string // Class assumed if non is set ("PUBLIC", "PRIVATE", "CONFIDENTIAL")
|
defaultClass string // Class assumed if non is set ("PUBLIC", "PRIVATE", "CONFIDENTIAL")
|
||||||
aggregates map[string]*config.Aggregate // In-memory map of path -> virtual calendar definitions
|
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{
|
b := &DBBackend{
|
||||||
pool: pool,
|
pool: pool,
|
||||||
|
prefix: cfg.Server.BasePath(),
|
||||||
redactionText: cfg.Server.Redaction,
|
redactionText: cfg.Server.Redaction,
|
||||||
defaultClass: cfg.Server.DefaultClass,
|
defaultClass: cfg.Server.DefaultClass,
|
||||||
aggregates: make(map[string]*config.Aggregate),
|
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.userAggs = make(map[string][]string)
|
||||||
b.publicAccess = make(map[string]publicInfo)
|
b.publicAccess = make(map[string]publicInfo)
|
||||||
|
|
||||||
|
prefix := cfg.Server.BasePath()
|
||||||
|
|
||||||
// --- Phase 1: User Sync ---
|
// --- Phase 1: User Sync ---
|
||||||
for _, u := range cfg.Users {
|
for _, u := range cfg.Users {
|
||||||
configUserNames[u.Name] = true
|
configUserNames[u.Name] = true
|
||||||
@@ -185,7 +189,7 @@ func (b *DBBackend) syncConfig(ctx context.Context, cfg *config.Config) error {
|
|||||||
|
|
||||||
// --- Phase 2: Calendar & Access Sync ---
|
// --- Phase 2: Calendar & Access Sync ---
|
||||||
for _, c := range cfg.Calendars {
|
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
|
configCalendarPaths[path] = true
|
||||||
|
|
||||||
var ownerID int
|
var ownerID int
|
||||||
@@ -243,7 +247,7 @@ func (b *DBBackend) syncConfig(ctx context.Context, cfg *config.Config) error {
|
|||||||
// Check for public access (ICS)
|
// Check for public access (ICS)
|
||||||
for _, a := range c.Access {
|
for _, a := range c.Access {
|
||||||
if a.ICS != "" {
|
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{
|
b.publicAccess[pubPath] = publicInfo{
|
||||||
InternalPath: path,
|
InternalPath: path,
|
||||||
Mode: a.ICS,
|
Mode: a.ICS,
|
||||||
@@ -255,7 +259,7 @@ func (b *DBBackend) syncConfig(ctx context.Context, cfg *config.Config) error {
|
|||||||
// --- Phase 3: Aggregate Setup ---
|
// --- Phase 3: Aggregate Setup ---
|
||||||
// Aggregates are virtual, so we only track them in memory for routing.
|
// Aggregates are virtual, so we only track them in memory for routing.
|
||||||
for _, agg := range cfg.Aggregates {
|
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_] {
|
if configCalendarPaths[p_] {
|
||||||
return fmt.Errorf("aggregate %s collides with real calendar path", 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
|
aggAccess[agg.Owner] = true
|
||||||
for _, a := range agg.Access {
|
for _, a := range agg.Access {
|
||||||
if a.ICS != "" {
|
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{
|
b.publicAccess[pubPath] = publicInfo{
|
||||||
InternalPath: p_,
|
InternalPath: p_,
|
||||||
Mode: a.ICS,
|
Mode: a.ICS,
|
||||||
@@ -548,7 +552,7 @@ func (b *DBBackend) CalendarHomeSetPath(ctx context.Context) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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.
|
// 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
|
var res []caldav.CalendarObject
|
||||||
|
|
||||||
for _, sourceID := range agg.Sources {
|
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 calID int
|
||||||
var ownerName string
|
var ownerName string
|
||||||
@@ -763,7 +767,7 @@ func (b *DBBackend) GetCalendarObject(ctx context.Context, p string, req *caldav
|
|||||||
sourceID := parts[0]
|
sourceID := parts[0]
|
||||||
realFileName := parts[1]
|
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 calID int
|
||||||
var ownerName string
|
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)
|
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
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
@@ -46,6 +48,14 @@ type ServerConfig struct {
|
|||||||
DefaultClass string `yaml:"default_class"`
|
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 {
|
type Config struct {
|
||||||
Server ServerConfig `yaml:"server"`
|
Server ServerConfig `yaml:"server"`
|
||||||
Database DatabaseConfig `yaml:"database"`
|
Database DatabaseConfig `yaml:"database"`
|
||||||
|
|||||||
57
main.go
57
main.go
@@ -4,7 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -14,7 +16,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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 {
|
if err != nil {
|
||||||
log.Fatalf("failed to load config: %v", err)
|
log.Fatalf("failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
@@ -27,8 +35,34 @@ func main() {
|
|||||||
|
|
||||||
handler := &caldav.Handler{Backend: be}
|
handler := &caldav.Handler{Backend: be}
|
||||||
|
|
||||||
|
publicURL, _ := url.Parse(cfg.Server.PublicURL)
|
||||||
|
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
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)
|
be.ServePublicICS(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -55,15 +89,26 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("%s %s (user: %s)", r.Method, r.URL.Path, user)
|
log.Printf("%s %s (user: %s)", r.Method, r.URL.Path, user)
|
||||||
|
prefix = cfg.Server.BasePath()
|
||||||
principalPath := fmt.Sprintf("/%s/", user)
|
principalPath := prefix + fmt.Sprintf("/%s/", user)
|
||||||
ctx := context.WithValue(r.Context(), "principal", principalPath)
|
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" {
|
||||||
http.Redirect(w, r, principalPath, http.StatusMovedPermanently)
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handler.ServeHTTP(w, r.WithContext(ctx))
|
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