- web/ts/login.ts: small vanilla TS module toggling the password input's type between 'password'/'text' via a 'Show/Hide' button, so users can rule out typos before submitting. Compiled to web/static/login.js (ES module) via tsc (web/tsconfig.json, new 'make web-ts'/'web-assets' Makefile targets) and loaded via <script type="module">. Compiled JS is committed/embedded the same way as the compiled CSS — no Node.js needed at runtime. - Verified end-to-end against the live proxied server (https://local.unqr.dev/web/login): POST /web/login correctly returns 303 + Set-Cookie, and the session then authorizes GET /web/ — the server-side login flow is confirmed working correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
329 lines
9.7 KiB
Markdown
329 lines
9.7 KiB
Markdown
# 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
|
|
- **Calendar/address book sharing** — grant other users read or write
|
|
access to your calendars/address books
|
|
- **Web UI** — a small dashboard (login, manage shares) at `/web/`, built
|
|
with templ + Tailwind + htmx
|
|
- Auto-discovery via `/.well-known/caldav` and `/.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
|
|
|
|
```bash
|
|
go mod tidy
|
|
```
|
|
|
|
### 2. Generate password hashes
|
|
|
|
```bash
|
|
go run ./tools/hashpwd mysecretpassword
|
|
# Outputs: $2b$12$...
|
|
```
|
|
|
|
### 3. Create your `config.yaml`
|
|
|
|
Copy the example config and edit it — `config.yaml` is git-ignored so your
|
|
real credentials/domain never get committed:
|
|
|
|
```bash
|
|
cp config.example.yaml config.yaml
|
|
```
|
|
|
|
Replace the placeholder hashes with your real bcrypt hashes:
|
|
|
|
```yaml
|
|
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
|
|
|
|
```bash
|
|
make run
|
|
# or
|
|
go run ./cmd/server -config config.yaml
|
|
```
|
|
|
|
The server starts at **http://localhost:8080**.
|
|
|
|
---
|
|
|
|
## Docker
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
1. Go to **Settings → Calendar → Accounts → Add Account → Other → Add CalDAV Account**
|
|
2. Enter:
|
|
- **Server**: `http://yourserver:8080`
|
|
- **Username**: `alice`
|
|
- **Password**: your plaintext password
|
|
3. The app will auto-discover calendars at `/cal/`.
|
|
|
|
Same flow for **CardDAV** with Contacts app.
|
|
|
|
### Thunderbird
|
|
|
|
1. Install the **TbSync** add-on + **CalDAV & CardDAV** provider
|
|
2. 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 `home` segment 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-set` properties.
|
|
|
|
---
|
|
|
|
## Sharing calendars and address books
|
|
|
|
A user can grant another user read or write access to one of their own
|
|
calendars or address books. Shared resources show up automatically in the
|
|
grantee's own home-set alongside their own calendars — no separate account
|
|
or extra client configuration needed.
|
|
|
|
Sharing grants are stored in a small SQLite database at
|
|
`<data_dir>/nidus.db` (not in `config.yaml`) and can be managed either via
|
|
the `nidusctl` CLI or the web UI's dashboard (see below):
|
|
|
|
```bash
|
|
# Give bob write access to alice's "work" calendar
|
|
go run ./tools/nidusctl -config config.yaml calendar share alice work bob write
|
|
|
|
# List everyone alice's "work" calendar is shared with
|
|
go run ./tools/nidusctl -config config.yaml calendar shares alice work
|
|
|
|
# Revoke access
|
|
go run ./tools/nidusctl -config config.yaml calendar unshare alice work bob
|
|
|
|
# Address books work the same way, using "addressbook" instead of "calendar"
|
|
go run ./tools/nidusctl -config config.yaml addressbook share alice contacts bob read
|
|
```
|
|
|
|
Or via `make`: `make nidusctl ARGS="calendar share alice work bob write"`.
|
|
|
|
A calendar that `alice` shares with `bob` appears in bob's calendar
|
|
home-set as `/cal/home/alice~work/` (i.e. `<owner>~<calendar name>`) — the
|
|
data itself still physically lives under alice's own storage; nothing is
|
|
copied. The same scheme applies to address books under `/card/home/`.
|
|
Read-only shares reject any write (PUT/DELETE) with `403 Forbidden`.
|
|
|
|
---
|
|
|
|
## Web UI
|
|
|
|
A small server-rendered dashboard is served at `/web/` (separate from the
|
|
DAV endpoints, which stay on HTTP Basic Auth):
|
|
|
|
- **Login** (`/web/login`) — cookie-based session, stored server-side in
|
|
`nidus.db` (`web_sessions` table), independent of DAV Basic Auth. Includes
|
|
a "Show/Hide" password toggle to rule out typos before submitting.
|
|
- **Dashboard** (`/web/`) — lists your own calendars/address books, who
|
|
they're shared with, and any resources other users have shared with you.
|
|
- **Share management** — add/remove shares directly from the dashboard
|
|
(same effect as `nidusctl`); updates happen in place via
|
|
[htmx](https://htmx.org/) without a full page reload.
|
|
- **Logout** (`/web/logout`).
|
|
|
|
Implementation: [templ](https://templ.guide/) for type-safe Go HTML
|
|
templates, [Tailwind CSS v4](https://tailwindcss.com/) for styling, htmx
|
|
for the sprinkles of dynamic behavior (form submission via POST/DELETE,
|
|
partial page swaps), and TypeScript (compiled to plain JS, `web/ts/`) for
|
|
the few bits of client-side-only logic (e.g. the password-visibility
|
|
toggle) — no separate JS framework needed. The compiled CSS, compiled JS,
|
|
and the htmx bundle are all embedded into the Go binary
|
|
(`web/staticassets.go`), so no Node.js is required at runtime, only when
|
|
you change styles, templates, or TypeScript during development:
|
|
|
|
```bash
|
|
make web-deps # once, installs the Tailwind CLI + TypeScript compiler (needs Node.js/npm)
|
|
make web-assets # regenerate templ code + rebuild web/static/app.css and web/static/*.js
|
|
```
|
|
|
|
---
|
|
|
|
## TLS / Reverse proxy
|
|
|
|
### Self-signed certificate (development)
|
|
|
|
```bash
|
|
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
|
|
```
|
|
|
|
Update `config.yaml`:
|
|
|
|
```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
|
|
|
|
```yaml
|
|
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 (DAV endpoints)
|
|
│ ├── caldav/ # CalDAV backend
|
|
│ ├── carddav/ # CardDAV backend
|
|
│ ├── config/ # YAML config loader
|
|
│ ├── db/ # SQLite store (shares, web UI sessions)
|
|
│ ├── store/ # filesystem storage layer
|
|
│ ├── web/ # web UI (cookie sessions, dashboard, share mgmt)
|
|
│ │ └── templates/ # templ templates (+ generated *_templ.go)
|
|
│ └── webdav/ # WebDAV file handler
|
|
├── tools/hashpwd/ # bcrypt password hasher CLI
|
|
├── tools/nidusctl/ # sharing-grant admin CLI
|
|
├── web/ # front-end assets: Tailwind input/config, static/
|
|
│ └── static/ # compiled app.css + htmx.min.js (embedded into the binary)
|
|
├── config.example.yaml # sample configuration (copy to config.yaml)
|
|
├── Dockerfile
|
|
├── docker-compose.yaml
|
|
└── Makefile
|
|
```
|
|
|
|
---
|
|
|
|
## Running tests
|
|
|
|
```bash
|
|
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 |
|
|
| `modernc.org/sqlite` | Pure-Go SQLite driver (shares, web UI sessions) |
|
|
| `github.com/a-h/templ` | Type-safe Go HTML templates (web UI) |
|
|
|
|
Front-end (dev-only, not required at runtime — see [Web UI](#web-ui)):
|
|
|
|
| Tool | Purpose |
|
|
|------|---------|
|
|
| Tailwind CSS v4 (`web/package.json`) | Utility-first CSS, compiled to `web/static/app.css` |
|
|
| [htmx](https://htmx.org/) (`web/static/htmx.min.js`, vendored) | Partial page updates without a JS framework |
|