Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dav-server"
version = "0.9.0"
version = "0.10.0"
readme = "README.md"
description = "Rust WebDAV server library. A fork of the webdav-handler crate."
repository = "https://github.com/messense/dav-server-rs"
Expand Down Expand Up @@ -74,6 +74,6 @@ clap = { version = "4.0.0", features = ["derive"] }
env_logger = "0.11.0"
actix-web = { version = "4.0.0-beta.15", default-features = false, features = ["macros"] }
hyper = { version = "1.1.0", features = ["http1", "server"] }
hyper-util = { version = "0.1.2", features = ["tokio"] }
hyper-util = { version = "0.1.19", features = ["tokio"] }
tokio = { version = "1.3.0", features = ["full"] }
axum = { version = "0.8", features = [] }
axum = { version = "0.8", features = [] }
30 changes: 13 additions & 17 deletions README.CalDAV.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ CalDAV is an extension of WebDAV that provides a standard way to access and mana

The CalDAV implementation in dav-server includes:

- **Calendar Collections**: Special WebDAV collections that contain calendar data
- **MKCALENDAR Method**: Create new calendar collections
- **Calendar Collections**: A directory that functions as a calendar, containing one `.ics` file for each event.
- **MKCALENDAR Method**: Create new calendar collection
- **REPORT Method**: Query calendar data with filters
- **CalDAV Properties**: Calendar-specific WebDAV properties
- **iCalendar Support**: Parse and validate iCalendar data
Expand All @@ -32,10 +32,6 @@ CalDAV support is available as an optional cargo feature:
dav-server = { version = "0.8", features = ["caldav"] }
```

This adds the following dependencies:
- `icalendar`: For parsing and validating iCalendar data
- `chrono`: For date/time handling

## Quick Start

Here's a basic CalDAV server setup:
Expand All @@ -44,10 +40,13 @@ Here's a basic CalDAV server setup:
use dav_server::{DavHandler, fakels::FakeLs, localfs::LocalFs};

let server = DavHandler::builder()
.filesystem(LocalFs::new("/calendars", false, false, false))
.filesystem(LocalFs::new("/dav_files", false, false, false))
.locksystem(FakeLs::new())
.build_handler();
```
## Important Setup Notes

**CalDAV Directory Creation**: The `/calendars` directory (defined in `dav_server::caldav::DEFAULT_CALDAV_DIRECTORY`) must exist before CalDAV operations. `MemFs` and `LocalFs` create it automatically, but custom `GuardedFileSystem` implementations must initialize it during startup.

## CalDAV Methods

Expand All @@ -56,13 +55,13 @@ let server = DavHandler::builder()
Creates a new calendar collection:

```bash
curl -X MKCALENDAR http://localhost:8080/my-calendar/
curl -X MKCALENDAR http://localhost:8080/calendars/my-calendar/
```

With properties:

```bash
curl -X MKCALENDAR http://localhost:8080/my-calendar/ \
curl -X MKCALENDAR http://localhost:8080/calendars/my-calendar/ \
-H "Content-Type: application/xml" \
--data '<?xml version="1.0" encoding="utf-8" ?>
<C:mkcalendar xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
Expand All @@ -82,7 +81,7 @@ Query calendar data:
#### Calendar Query

```bash
curl -X REPORT http://localhost:8080/my-calendar/ \
curl -X REPORT http://localhost:8080/calendars/my-calendar/ \
-H "Content-Type: application/xml" \
-H "Depth: 1" \
--data '<?xml version="1.0" encoding="utf-8" ?>
Expand All @@ -103,7 +102,7 @@ curl -X REPORT http://localhost:8080/my-calendar/ \
#### Calendar Multiget

```bash
curl -X REPORT http://localhost:8080/my-calendar/ \
curl -X REPORT http://localhost:8080/calendars/my-calendar/ \
-H "Content-Type: application/xml" \
--data '<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
Expand Down Expand Up @@ -141,7 +140,7 @@ The implementation supports standard CalDAV properties:
Store iCalendar data using PUT:

```bash
curl -X PUT http://localhost:8080/my-calendar/event.ics \
curl -X PUT http://localhost:8080/calendars/my-calendar/event.ics \
-H "Content-Type: text/calendar" \
--data 'BEGIN:VCALENDAR
VERSION:2.0
Expand All @@ -161,7 +160,7 @@ END:VCALENDAR'
Use GET to retrieve individual calendar resources:

```bash
curl http://localhost:8080/my-calendar/event.ics
curl http://localhost:8080/calendars/my-calendar/event.ics
```

## Client Compatibility
Expand All @@ -184,7 +183,7 @@ Current limitations include:
- No recurring event expansion in queries

## Example Applications
These calendar server examples lacks authentication and does not support user-specific access.
These calendar server examples lacks authentication and does not support user-specific access. The default FileSystems can only create collections on the path "/calendars".
For a production environment, you should implement the GuardedFileSystem for better security and user management.

### Calendar Server
Expand All @@ -198,9 +197,6 @@ async fn main() {
let server = DavHandler::builder()
.filesystem(LocalFs::new("/calendars", false, false, false))
.locksystem(FakeLs::new())
// Use .strip_prefix if you want to start the handler with a prefix path like "/calendars".
// None will start with root ("/")
// .strip_prefix("/calendars")
.build_handler();

// Serve on port 8080
Expand Down
135 changes: 92 additions & 43 deletions examples/caldav.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,62 @@
//! The server will be available at http://localhost:8080
//! You can connect to it using CalDAV clients like Thunderbird, Apple Calendar, etc.

use dav_server::{DavHandler, DavMethodSet, fakels::FakeLs, memfs::MemFs};
use hyper::{server::conn::http1, service::service_fn};
use hyper_util::rt::TokioIo;
use std::{convert::Infallible, net::SocketAddr};
use axum::{
Extension, Router,
body::Body,
extract::Request,
http::{HeaderValue, StatusCode},
middleware::{self, Next},
response::IntoResponse,
routing::any,
};
use chrono::Datelike;
use dav_server::{DavHandler, caldav::DEFAULT_CALDAV_DIRECTORY, fakels::FakeLs, localfs};
use http_body_util::BodyExt;
use std::sync::Arc;
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn main() {
env_logger::init();
let addr = "127.0.0.1:8080";

let addr: SocketAddr = ([127, 0, 0, 1], 8080).into();

// Set up the DAV handler with CalDAV support
// Note: Using MemFs for this example because it supports the property operations
// needed for CalDAV collections. For production use, you'd want a filesystem
// implementation that persists properties to disk (e.g., using extended attributes
// or sidecar files).
let dav_server = DavHandler::builder()
.filesystem(MemFs::new())
.filesystem(localfs::LocalFs::new("/tmp", true, false, false))
.locksystem(FakeLs::new())
.methods(DavMethodSet::all())
.autoindex(true)
.build_handler();

let listener = TcpListener::bind(addr).await?;
let router = Router::new()
.route("/.well-known/caldav", any(handle_caldav_redirect))
.route("/", any(handle_caldav))
.route("/{*path}", any(handle_caldav))
.layer(Extension(Arc::new(dav_server)))
.layer(middleware::from_fn(log_request_middleware));

let listener = TcpListener::bind(&addr).await.unwrap();

println!("CalDAV server listening on {}", addr);
println!("Calendar collections can be accessed at http://{}", addr);
println!("CalDAV server listening on http://{}", addr);
println!(
"Calendar collections can be accessed at http://{}{}",
addr, DEFAULT_CALDAV_DIRECTORY
);
println!();
println!("NOTE: This example uses in-memory storage. Data will be lost when the server stops.");
println!(
"NOTE: This example stores data in a temporary directory (/tmp). Data may be lost when the server stops or when temporary files are cleaned."
);
println!();
println!("To create a calendar collection, use:");
println!(" curl -i -X MKCALENDAR http://{}/my-calendar/", addr);
println!(
" curl -i -X MKCALENDAR http://{}{}/my-calendar/",
addr, DEFAULT_CALDAV_DIRECTORY
);
println!();
println!("To add a calendar event, use:");
println!(" curl -i -X PUT http://{}/my-calendar/event1.ics \\", addr);
println!(
" curl -i -X PUT http://{}{}/my-calendar/event1.ics \\",
addr, DEFAULT_CALDAV_DIRECTORY
);
println!(" -H 'Content-Type: text/calendar' \\");
println!(" --data-binary @event.ics");
println!();
Expand All @@ -53,37 +74,65 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("PRODID:-//Example Corp//CalDAV Client//EN");
println!("BEGIN:VEVENT");
println!("UID:12345@example.com");
println!("DTSTART:20240101T120000Z");
println!("DTEND:20240101T130000Z");
let next_year = chrono::Local::now().year_ce().1 + 1;
println!("DTSTART:{}0101T120000Z", next_year);
println!("DTEND:{}0101T130000Z", next_year);
println!("SUMMARY:New Year Meeting");
println!("DESCRIPTION:Planning meeting for the new year");
println!("END:VEVENT");
println!("END:VCALENDAR");

// Start the server loop
loop {
let (stream, _) = listener.accept().await?;
let dav_server = dav_server.clone();
axum::serve(listener, router).await.unwrap();
}

async fn handle_caldav_redirect() -> (
StatusCode,
[(axum::http::header::HeaderName, HeaderValue); 1],
) {
(
StatusCode::MOVED_PERMANENTLY,
[(
axum::http::header::LOCATION,
HeaderValue::from_static(DEFAULT_CALDAV_DIRECTORY),
)],
)
}

let io = TokioIo::new(stream);
async fn handle_caldav(
Extension(dav): Extension<Arc<DavHandler>>,
req: Request,
) -> impl IntoResponse {
dav.handle(req).await
}

tokio::task::spawn(async move {
if let Err(err) = http1::Builder::new()
.serve_connection(
io,
service_fn({
move |req| {
let dav_server = dav_server.clone();
async move { Ok::<_, Infallible>(dav_server.handle(req).await) }
}
}),
)
.await
{
eprintln!("Failed serving connection: {err:?}");
}
});
async fn log_request_middleware(request: Request, next: Next) -> impl IntoResponse {
// Print request line and headers
println!("\n========== CLIENT REQUEST ==========");
println!("{} {}", request.method(), request.uri(),);
println!("--- Headers ---");
for (name, value) in request.headers() {
println!("{}: {}", name, value.to_str().unwrap_or("<binary>"));
}

// Read and print body
let (parts, body) = request.into_parts();
let collected = body.collect().await.unwrap_or_default();
let body_bytes = collected.to_bytes();

if !body_bytes.is_empty() {
println!("--- Body ---");
if let Ok(body_str) = std::str::from_utf8(&body_bytes) {
println!("{}", body_str);
} else {
println!("<binary data: {} bytes>", body_bytes.len());
}
}
println!("====================================\n");

// Reconstruct request with body
let request = axum::http::Request::from_parts(parts, Body::from(body_bytes));

next.run(request).await
}

#[cfg(not(feature = "caldav"))]
Expand Down
Loading
Loading