b4644bc590361841d3dadac2a3fb066e2b0fb837
- Root handler now only serves the welcome page for GET/HEAD; all other methods (e.g. OPTIONS, PROPFIND) return 405 with an Allow header instead of always returning 200, fixing client capability probes and PROPFIND misbehavior. - Mount /files/ properly and cache one xwebdav.Handler per authenticated user so its LockSystem persists across requests instead of being recreated per-request (which broke LOCK/UNLOCK). - Remove the username segment from all DAV URLs (/cal/, /card/, /files/ are now identical for every account; the acting user is always resolved via Basic Auth, never the path). - Reintroduce a fixed literal "home" path segment (/cal/home/, /card/home/) to preserve the URL segment depth that go-webdav's caldav/carddav server relies on to classify resources (principal vs. home-set vs. collection vs. object). Removing the username had collapsed this depth, silently misclassifying requests and returning empty <multistatus> responses (DAVx5 "no resources found"). - Replace the store's single global mutex with per-user sharded locks so different users' requests no longer serialize against each other. - Add auth.NewContext test helper, WebDAV handler tests (per-user isolation, lock persistence across requests), and a concurrent multi-user store test. - Update README and copilot-instructions to document the new URL scheme and the go-webdav path-depth classification quirk. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DAV Server
A self-hosted CalDAV, CardDAV, and WebDAV server written in Go.
Features
| Protocol | Use case |
|---|---|
| CalDAV | Calendars — sync with Apple Calendar, Thunderbird, GNOME Calendar, … |
| CardDAV | Contacts — sync with Apple Contacts, GNOME Contacts, … |
| WebDAV | General file access via Finder, Windows Explorer, Nautilus, … |
- HTTP Basic Auth with bcrypt password storage
- Per-user isolated collections
- Auto-discovery via
/.well-known/caldavand/.well-known/carddav - Optional TLS (or use a reverse proxy)
- Structured logging (text or JSON)
- Graceful shutdown
- Docker & Docker Compose support
Quick start
1. Install dependencies
go mod tidy
2. Generate password hashes
go run ./tools/hashpwd mysecretpassword
# Outputs: $2b$12$...
3. Edit config.yaml
Replace the placeholder hashes with your real bcrypt hashes:
users:
alice:
password: "$2b$12$<hash generated above>"
display_name: "Alice Smith"
email: "alice@example.com"
calendars:
- personal
- work
address_books:
- contacts
4. Run the server
make run
# or
go run ./cmd/server -config config.yaml
The server starts at http://localhost:8080.
Docker
# Build and start
docker compose up --build
# Or build manually
docker build -t davserver .
docker run -p 8080:8080 \
-v ./config.yaml:/app/config.yaml:ro \
-v dav-data:/app/data \
davserver
Client configuration
Apple Calendar / Contacts (macOS / iOS)
- Go to Settings → Calendar → Accounts → Add Account → Other → Add CalDAV Account
- Enter:
- Server:
http://yourserver:8080 - Username:
alice - Password: your plaintext password
- Server:
- The app will auto-discover calendars at
/cal/.
Same flow for CardDAV with Contacts app.
Thunderbird
- Install the TbSync add-on + CalDAV & CardDAV provider
- Add a new account and point it at
http://yourserver:8080/.well-known/caldav
GNOME Calendar / Evolution
Use the GNOME Online Accounts panel:
- Server:
http://yourserver:8080 - Check CalDAV / CardDAV as appropriate
API endpoints
| Path | Description |
|---|---|
/.well-known/caldav |
Redirects to /cal/ |
/.well-known/carddav |
Redirects to /card/ |
/cal/ |
CalDAV principal (same URL for every user; resolved via Basic Auth) |
/cal/home/ |
Calendar home-set (lists the user's calendars) |
/cal/home/<calendar>/ |
Calendar collection |
/card/ |
CardDAV principal (same URL for every user; resolved via Basic Auth) |
/card/home/ |
Address book home-set (lists the user's address books) |
/card/home/<book>/ |
Address book collection |
/files/ |
WebDAV file storage (same URL for every user; resolved via Basic Auth) |
/healthz |
Health check (unauthenticated) |
Note: the
homesegment is a fixed literal (not a username or real resource) — it exists only to give the calendar/address-book home-set the path depth that the underlying CalDAV/CardDAV library expects when classifying resources by URL. Clients should never need to construct these URLs by hand; they're discovered automatically via.well-known+current-user-principal+calendar-home-set/addressbook-home-setproperties.
TLS / Reverse proxy
Self-signed certificate (development)
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
Update config.yaml:
tls:
enabled: true
cert_file: cert.pem
key_file: key.pem
Caddy reverse proxy (recommended for production)
dav.example.com {
reverse_proxy localhost:8080
}
Configuration reference
server:
host: "0.0.0.0"
port: 8080
base_url: "https://dav.example.com" # used in DAV responses
auth:
realm: "My DAV Server"
storage:
data_dir: "./data" # all user data lives here
logging:
level: "info" # debug | info | warn | error
format: "text" # text | json
tls:
enabled: false
cert_file: ""
key_file: ""
users:
<username>:
password: "<bcrypt hash>"
display_name: "Full Name"
email: "user@example.com"
calendars: # pre-created calendar names
- personal
address_books: # pre-created address book names
- contacts
Project layout
caldav-server/
├── cmd/server/ # main entrypoint
├── internal/
│ ├── auth/ # HTTP Basic Auth middleware
│ ├── caldav/ # CalDAV backend
│ ├── carddav/ # CardDAV backend
│ ├── config/ # YAML config loader
│ ├── store/ # filesystem storage layer
│ └── webdav/ # WebDAV file handler
├── tools/hashpwd/ # bcrypt password hasher CLI
├── config.yaml # sample configuration
├── Dockerfile
├── docker-compose.yaml
└── Makefile
Running tests
make test
# or
go test ./... -race
Dependencies
| Package | Purpose |
|---|---|
github.com/emersion/go-webdav |
WebDAV/CalDAV/CardDAV protocol layer |
github.com/emersion/go-ical |
iCalendar parsing/serialisation |
github.com/emersion/go-vcard |
vCard parsing/serialisation |
golang.org/x/crypto |
bcrypt |
golang.org/x/net |
golang.org/x/net/webdav |
gopkg.in/yaml.v3 |
YAML config parsing |