• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lennart-k / rustical / #76

05 Dec 2025 02:03PM UTC coverage: 20.164% (+0.09%) from 20.075%
#76

push

web-flow
Merge pull request #138 from lennart-k/feature/birthday-calendar

Feature/birthday calendar

31 of 205 new or added lines in 7 files covered. (15.12%)

8 existing lines in 3 files now uncovered.

933 of 4627 relevant lines covered (20.16%)

0.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/app.rs
1
use crate::config::NextcloudLoginConfig;
2
use axum::Router;
3
use axum::body::{Body, HttpBody};
4
use axum::extract::{DefaultBodyLimit, Request};
5
use axum::middleware::Next;
6
use axum::response::{Redirect, Response};
7
use axum::routing::{any, options};
8
use axum_extra::TypedHeader;
9
use headers::{HeaderMapExt, UserAgent};
10
use http::header::CONNECTION;
11
use http::{HeaderValue, StatusCode};
12
use rustical_caldav::caldav_router;
13
use rustical_carddav::carddav_router;
14
use rustical_frontend::nextcloud_login::nextcloud_login_router;
15
use rustical_frontend::{FrontendConfig, frontend_router};
16
use rustical_oidc::OidcConfig;
17
use rustical_store::auth::AuthenticationProvider;
18
use rustical_store::{
19
    AddressbookStore, CalendarStore, CombinedCalendarStore, PrefixedCalendarStore,
20
    SubscriptionStore,
21
};
22
use std::sync::Arc;
23
use std::time::Duration;
24
use tower_http::catch_panic::CatchPanicLayer;
25
use tower_http::classify::ServerErrorsFailureClass;
26
use tower_http::trace::TraceLayer;
27
use tower_sessions::cookie::SameSite;
28
use tower_sessions::{Expiry, MemoryStore, SessionManagerLayer};
29
use tracing::Span;
30
use tracing::field::display;
31

32
#[allow(
33
    clippy::too_many_arguments,
34
    clippy::too_many_lines,
35
    clippy::cognitive_complexity
36
)]
NEW
37
pub fn make_app<
×
38
    AS: AddressbookStore + PrefixedCalendarStore,
39
    CS: CalendarStore,
40
    S: SubscriptionStore,
41
>(
42
    addr_store: Arc<AS>,
43
    cal_store: Arc<CS>,
44
    subscription_store: Arc<S>,
45
    auth_provider: Arc<impl AuthenticationProvider>,
46
    frontend_config: FrontendConfig,
47
    oidc_config: Option<OidcConfig>,
48
    nextcloud_login_config: &NextcloudLoginConfig,
49
    dav_push_enabled: bool,
50
    session_cookie_samesite_strict: bool,
51
    payload_limit_mb: usize,
52
) -> Router<()> {
NEW
53
    let birthday_store = addr_store.clone();
×
54
    let combined_cal_store =
×
55
        Arc::new(CombinedCalendarStore::new(cal_store).with_store(birthday_store));
×
56

57
    let mut router = Router::new()
×
58
        // endpoint to be used by healthcheck to see if rustical is online
59
        .route("/ping", axum::routing::get(async || "Pong!"))
×
60
        .merge(caldav_router(
×
61
            "/caldav",
×
62
            auth_provider.clone(),
×
63
            combined_cal_store.clone(),
×
64
            subscription_store.clone(),
×
65
            false,
×
66
        ))
67
        .merge(caldav_router(
×
68
            "/caldav-compat",
×
69
            auth_provider.clone(),
×
70
            combined_cal_store.clone(),
×
71
            subscription_store.clone(),
×
72
            true,
×
73
        ))
74
        .route(
75
            "/.well-known/caldav",
76
            any(async |TypedHeader(ua): TypedHeader<UserAgent>| {
×
77
                if ua.as_str().contains("remindd") || ua.as_str().contains("dataaccessd") {
×
78
                    // remindd is an Apple Calendar User Agent
79
                    // Even when explicitly configuring a principal URL in Apple Calendar Apple
80
                    // will not respect that configuration but call /.well-known/caldav,
81
                    // so sadly we have to do this user-agent filtering. :(
82
                    // (I should have never gotten an Apple device)
83
                    return Redirect::permanent("/caldav-compat");
×
84
                }
85
                Redirect::permanent("/caldav")
×
86
            }),
87
        )
88
        .merge(carddav_router(
×
89
            "/carddav",
×
90
            auth_provider.clone(),
×
91
            addr_store.clone(),
×
92
            subscription_store.clone(),
×
93
        ));
94

95
    // GNOME Accounts needs to discover a WebDAV Files endpoint to complete the setup
96
    // It looks at / as well as /remote.php/dav (Nextcloud)
97
    // This is not nice but we offer this as a sacrificial route to ensure the CalDAV/CardDAV setup
98
    // works.
99
    // See:
100
    // https://github.com/GNOME/gnome-online-accounts/blob/master/src/goabackend/goadavclient.c
101
    // https://github.com/GNOME/gnome-online-accounts/blob/master/src/goabackend/goawebdavprovider.c
102
    router = router.route(
×
103
        "/remote.php/dav",
×
104
        options(async || {
×
105
            let mut resp = Response::builder().status(StatusCode::OK);
×
106
            resp.headers_mut()
×
107
                .unwrap()
×
108
                .insert("DAV", HeaderValue::from_static("1"));
×
109
            resp.body(Body::empty()).unwrap()
×
110
        }),
111
    );
112

113
    let session_store = MemoryStore::default();
×
114
    if frontend_config.enabled {
×
115
        router = router.merge(frontend_router(
×
116
            "/frontend",
×
117
            auth_provider.clone(),
×
118
            combined_cal_store,
×
119
            addr_store,
×
120
            frontend_config,
×
121
            oidc_config,
×
122
        ));
123
    }
124

125
    if nextcloud_login_config.enabled {
×
126
        router = router.nest("/index.php/login/v2", nextcloud_login_router(auth_provider));
×
127
    }
128

129
    if dav_push_enabled {
×
130
        router = router.merge(rustical_dav_push::subscription_service(subscription_store));
×
131
    }
132

133
    router
×
134
        .layer(
135
            SessionManagerLayer::new(session_store)
×
136
                .with_name("rustical_session")
×
137
                .with_secure(true)
×
138
                .with_same_site(if session_cookie_samesite_strict {
×
139
                    SameSite::Strict
×
140
                } else {
141
                    SameSite::Lax
×
142
                })
143
                .with_expiry(Expiry::OnInactivity(
×
144
                    tower_sessions::cookie::time::Duration::hours(2),
×
145
                )),
146
        )
147
        .layer(CatchPanicLayer::new())
×
148
        .layer(
149
            TraceLayer::new_for_http()
×
150
                .make_span_with(|request: &Request| {
×
151
                    tracing::info_span!(
×
152
                        "http-request",
×
153
                        status = tracing::field::Empty,
×
154
                        otel.name = tracing::field::display(format!(
×
155
                            "{} {}",
×
156
                            request.method(),
×
157
                            request.uri()
×
158
                        )),
159
                        ua = tracing::field::Empty,
×
160
                    )
161
                })
162
                .on_request(|req: &Request, span: &Span| {
×
163
                    span.record("method", display(req.method()));
×
164
                    span.record("path", display(req.uri()));
×
165
                    if let Some(ua) = req.headers().typed_get::<UserAgent>() {
×
166
                        span.record("ua", display(ua));
×
167
                    }
168
                })
169
                .on_response(|response: &Response, _latency: Duration, span: &Span| {
×
170
                    span.record("status", display(response.status()));
×
171
                    if response.status().is_server_error() {
×
172
                        tracing::error!("server error");
×
173
                    } else if response.status().is_client_error() {
×
174
                        match response.status() {
×
175
                            StatusCode::UNAUTHORIZED => {
×
176
                                // The iOS client always tries an unauthenticated request first so
177
                                // logging 401's as errors would clog up our logs
178
                                tracing::debug!("unauthorized");
×
179
                            }
180
                            StatusCode::NOT_FOUND => {
×
181
                                // Clients like GNOME Calendar will try to reach /remote.php/webdav
182
                                // quite often clogging up the logs
183
                                tracing::info!("client error");
×
184
                            }
185
                            _ => {
×
186
                                tracing::error!("client error");
×
187
                            }
188
                        }
189
                    }
190
                })
191
                .on_failure(
×
192
                    |_error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| {
×
193
                        tracing::error!("something went wrong");
×
194
                    },
195
                ),
196
        )
197
        .layer(axum::middleware::from_fn(
×
198
            async |req: Request, next: Next| {
×
199
                // Closes the connection if the request body might've not been fully consumed
200
                // Otherwise subsequent requests reusing the connection might fail.
201
                // See https://github.com/lennart-k/rustical/issues/77
202
                let body_empty = req.body().is_end_stream();
×
203
                let mut response = next.run(req).await;
×
204
                if !body_empty
×
205
                    && (response.status().is_server_error() || response.status().is_client_error())
×
206
                {
207
                    response
×
208
                        .headers_mut()
×
209
                        .insert(CONNECTION, HeaderValue::from_static("close"));
×
210
                }
211
                response
×
212
            },
213
        ))
214
        .layer(DefaultBodyLimit::max(payload_limit_mb * 1000 * 1000))
×
215
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc