atagen

macltok

public
Star0
Watch0
cb0668d30c14c14e0b1edddc9133ebf1e98bd5cb
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/account.rsRust834 lines27.9 KB
1
#[cfg(feature = "sqlite")]
2
use std::path::PathBuf;
3
#[cfg(not(target_family = "wasm"))]
4
use std::sync::atomic::{AtomicBool, Ordering};
5
use std::{collections::HashMap, sync::Arc};
6
7
use matrix_sdk::{
8
    Client, ClientBuilder, SendOutsideWasm, SessionChange, SessionMeta, SyncOutsideWasm,
9
    ThreadingSupport,
10
    authentication::{
11
        AuthSession,
12
        matrix::MatrixSession,
13
        oauth::{
14
            ClientId, ClientRegistrationData, CsrfToken, OAuthSession, UserSession,
15
            registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
16
        },
17
    },
18
    cross_process_lock::CrossProcessLockConfig,
19
    encryption::{BackupDownloadStrategy, EncryptionSettings},
20
    ruma::{OwnedDeviceId, OwnedUserId, serde::Raw},
21
};
22
use serde::{Deserialize, Serialize};
23
use thiserror::Error;
24
use tokio::sync::{broadcast, mpsc};
25
use url::Url;
26
27
use crate::{
28
    ClientHandle, Engine, EngineConfig, EngineError, ErrorKind, EventStream, ServerError,
29
    SessionStore,
30
    engine::{Persistence, StoreLock},
31
    error::SourceError,
32
};
33
34
/// a logged in client and the `SessionStore` its record lives in. the engine it builds
35
/// saves every token rotation there, unless told `without_persistence`
36
pub struct Session {
37
    client: Client,
38
    tracking: Tracking,
39
    store_lock: StoreLock,
40
}
41
42
/// taken as soon as the client exists, since the sdk can refresh before there's an engine
43
struct Tracking {
44
    session_changes: broadcast::Receiver<SessionChange>,
45
    persistence: Option<Persistence>,
46
    #[cfg(not(target_family = "wasm"))]
47
    inline: Arc<AtomicBool>,
48
}
49
50
#[non_exhaustive]
51
pub struct PasswordLogin {
52
    pub homeserver: String,
53
    pub username: String,
54
    pub password: String,
55
    pub device_display_name: Option<String>,
56
    /// set after a soft logout so the store's keys still belong to us
57
    pub device_id: Option<OwnedDeviceId>,
58
    /// let plain http through, for loopback hosts only
59
    pub allow_insecure_loopback: bool,
60
}
61
62
impl PasswordLogin {
63
    #[must_use]
64
    pub fn new(
65
        homeserver: impl Into<String>,
66
        username: impl Into<String>,
67
        password: impl Into<String>,
68
    ) -> Self {
69
        Self {
70
            homeserver: homeserver.into(),
71
            username: username.into(),
72
            password: password.into(),
73
            device_display_name: None,
74
            device_id: None,
75
            allow_insecure_loopback: false,
76
        }
77
    }
78
79
    #[must_use]
80
    pub fn with_device_display_name(mut self, name: impl Into<String>) -> Self {
81
        self.device_display_name = Some(name.into());
82
        self
83
    }
84
85
    #[must_use]
86
    pub fn with_device_id(mut self, device_id: OwnedDeviceId) -> Self {
87
        self.device_id = Some(device_id);
88
        self
89
    }
90
91
    #[must_use]
92
    pub fn allow_insecure_loopback(mut self, allow: bool) -> Self {
93
        self.allow_insecure_loopback = allow;
94
        self
95
    }
96
}
97
98
#[non_exhaustive]
99
pub struct OAuthLogin {
100
    pub homeserver: String,
101
    pub redirect_uri: String,
102
    pub client_uri: String,
103
    pub client_name: Option<String>,
104
    pub client_id: Option<String>,
105
    pub device_id: Option<OwnedDeviceId>,
106
    pub application_type: OAuthApplicationType,
107
    /// let plain http through, for loopback hosts only
108
    pub allow_insecure_loopback: bool,
109
}
110
111
impl OAuthLogin {
112
    #[must_use]
113
    pub fn new(
114
        homeserver: impl Into<String>,
115
        redirect_uri: impl Into<String>,
116
        client_uri: impl Into<String>,
117
        application_type: OAuthApplicationType,
118
    ) -> Self {
119
        Self {
120
            homeserver: homeserver.into(),
121
            redirect_uri: redirect_uri.into(),
122
            client_uri: client_uri.into(),
123
            client_name: None,
124
            client_id: None,
125
            device_id: None,
126
            application_type,
127
            allow_insecure_loopback: false,
128
        }
129
    }
130
131
    #[must_use]
132
    pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
133
        self.client_name = Some(name.into());
134
        self
135
    }
136
137
    /// a statically registered client id, skipping dynamic registration
138
    #[must_use]
139
    pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
140
        self.client_id = Some(client_id.into());
141
        self
142
    }
143
144
    #[must_use]
145
    pub fn with_device_id(mut self, device_id: OwnedDeviceId) -> Self {
146
        self.device_id = Some(device_id);
147
        self
148
    }
149
150
    #[must_use]
151
    pub fn allow_insecure_loopback(mut self, allow: bool) -> Self {
152
        self.allow_insecure_loopback = allow;
153
        self
154
    }
155
}
156
157
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158
#[non_exhaustive]
159
pub enum OAuthApplicationType {
160
    Native,
161
    Web,
162
}
163
164
pub struct PendingOAuthLogin {
165
    client: Client,
166
    sessions: Arc<dyn SessionStore>,
167
    tracking: Option<(Tracking, StoreLock)>,
168
    authorization_url: String,
169
    state: Option<CsrfToken>,
170
}
171
172
impl PendingOAuthLogin {
173
    #[must_use]
174
    pub fn authorization_url(&self) -> &str {
175
        &self.authorization_url
176
    }
177
178
    #[must_use]
179
    pub fn registered_client_id(&self) -> String {
180
        self.client
181
            .oauth()
182
            .client_id()
183
            .expect("OAuth client was registered before authorization")
184
            .as_str()
185
            .to_owned()
186
    }
187
188
    #[must_use]
189
    pub const fn is_pending(&self) -> bool {
190
        self.state.is_some()
191
    }
192
193
    pub async fn finish(&mut self, callback_url: &str) -> Result<Session, AccountError> {
194
        let callback_url = Url::parse(callback_url)
195
            .map_err(|error| account_error("parsing the OAuth callback URL", error))?;
196
        let state = self
197
            .state
198
            .take()
199
            .ok_or(AccountError::OAuthCallbackAlreadySubmitted)?;
200
        let oauth = self.client.oauth();
201
        if let Err(error) = oauth.finish_login(callback_url.into()).await {
202
            oauth.abort_login(&state).await;
203
            return Err(account_error("OAuth login", error));
204
        }
205
        let (tracking, store_lock) = self
206
            .tracking
207
            .take()
208
            .ok_or(AccountError::OAuthCallbackAlreadySubmitted)?;
209
        Session::saved(
210
            self.client.clone(),
211
            self.sessions.clone(),
212
            tracking,
213
            store_lock,
214
        )
215
        .await
216
    }
217
218
    pub async fn abort(mut self) {
219
        if let Some(state) = self.state.take() {
220
            self.client.oauth().abort_login(&state).await;
221
        }
222
    }
223
}
224
225
pub struct StoreConfig {
226
    location: StoreLocation,
227
    passphrase: String,
228
}
229
230
enum StoreLocation {
231
    #[cfg(feature = "sqlite")]
232
    Sqlite(PathBuf),
233
    #[cfg(feature = "indexeddb")]
234
    IndexedDb(String),
235
}
236
237
impl StoreConfig {
238
    #[cfg(feature = "sqlite")]
239
    #[must_use]
240
    pub fn sqlite(path: impl Into<PathBuf>, passphrase: impl Into<String>) -> Self {
241
        Self {
242
            location: StoreLocation::Sqlite(path.into()),
243
            passphrase: passphrase.into(),
244
        }
245
    }
246
247
    #[cfg(feature = "indexeddb")]
248
    #[must_use]
249
    pub fn indexeddb(name: impl Into<String>, passphrase: impl Into<String>) -> Self {
250
        Self {
251
            location: StoreLocation::IndexedDb(name.into()),
252
            passphrase: passphrase.into(),
253
        }
254
    }
255
256
    /// delete the store's databases, once nothing has them open
257
    pub async fn delete(&self) -> Result<(), AccountError> {
258
        match &self.location {
259
            #[cfg(feature = "sqlite")]
260
            StoreLocation::Sqlite(path) => crate::session_store::delete_sqlite_store(path)
261
                .map_err(|error| account_error("deleting the Matrix store", error)),
262
            #[cfg(feature = "indexeddb")]
263
            StoreLocation::IndexedDb(name) => crate::session_store::delete_indexeddb_store(name)
264
                .await
265
                .map_err(|error| error.into_account_error("deleting the Matrix store")),
266
        }
267
    }
268
269
    /// taken before the client opens the store, and held until the engine and every
270
    /// `ClientHandle` are gone
271
    pub(crate) async fn lock(&self) -> Result<StoreLock, AccountError> {
272
        match &self.location {
273
            #[cfg(feature = "sqlite")]
274
            StoreLocation::Sqlite(path) => StoreLock::file(path),
275
            #[cfg(feature = "indexeddb")]
276
            StoreLocation::IndexedDb(name) => StoreLock::web(name).await,
277
        }
278
    }
279
280
    pub(crate) fn passphrase(&self) -> &str {
281
        &self.passphrase
282
    }
283
284
    #[cfg(feature = "indexeddb")]
285
    pub(crate) fn indexeddb_name(&self) -> &str {
286
        let StoreLocation::IndexedDb(name) = &self.location;
287
        name
288
    }
289
}
290
291
/// Serializable authentication secrets. A `SessionStore` keeps it, and the engine
292
/// replaces it there on every token rotation.
293
#[derive(Clone, Deserialize, Serialize)]
294
pub struct SessionRecord {
295
    homeserver: String,
296
    authentication: AuthenticationRecord,
297
}
298
299
#[derive(Clone, Deserialize, Serialize)]
300
#[serde(tag = "type", content = "session", rename_all = "snake_case")]
301
enum AuthenticationRecord {
302
    Matrix(MatrixSession),
303
    OAuth {
304
        client_id: ClientId,
305
        user: UserSession,
306
    },
307
    SoftLoggedOut {
308
        meta: SessionMeta,
309
        client_id: Option<ClientId>,
310
    },
311
}
312
313
impl SessionRecord {
314
    #[must_use]
315
    pub fn homeserver(&self) -> &str {
316
        &self.homeserver
317
    }
318
319
    pub(crate) fn user_id(&self) -> &OwnedUserId {
320
        match &self.authentication {
321
            AuthenticationRecord::Matrix(session) => &session.meta.user_id,
322
            AuthenticationRecord::OAuth { user, .. } => &user.meta.user_id,
323
            AuthenticationRecord::SoftLoggedOut { meta, .. } => &meta.user_id,
324
        }
325
    }
326
327
    pub(crate) fn soft_logout(&self) -> Option<SoftLogout> {
328
        let AuthenticationRecord::SoftLoggedOut { meta, client_id } = &self.authentication else {
329
            return None;
330
        };
331
        Some(SoftLogout {
332
            homeserver: self.homeserver.clone(),
333
            user_id: meta.user_id.clone(),
334
            device_id: meta.device_id.clone(),
335
            client_id: client_id
336
                .as_ref()
337
                .map(|client_id| client_id.as_str().to_owned()),
338
        })
339
    }
340
341
    pub(crate) fn soft_logged_out(self) -> Self {
342
        let authentication = match self.authentication {
343
            AuthenticationRecord::Matrix(session) => AuthenticationRecord::SoftLoggedOut {
344
                meta: session.meta,
345
                client_id: None,
346
            },
347
            AuthenticationRecord::OAuth { client_id, user } => {
348
                AuthenticationRecord::SoftLoggedOut {
349
                    meta: user.meta,
350
                    client_id: Some(client_id),
351
                }
352
            }
353
            soft @ AuthenticationRecord::SoftLoggedOut { .. } => soft,
354
        };
355
        Self {
356
            homeserver: self.homeserver,
357
            authentication,
358
        }
359
    }
360
}
361
362
pub enum Restore {
363
    Resumed(Session),
364
    SoftLoggedOut(SoftLogout),
365
}
366
367
/// who to log in again as. the store keeps this device's e2ee keys, so a login
368
/// as any other device would orphan them
369
#[derive(Clone, Debug)]
370
#[non_exhaustive]
371
pub struct SoftLogout {
372
    pub homeserver: String,
373
    pub user_id: OwnedUserId,
374
    pub device_id: OwnedDeviceId,
375
    /// set when the login was OAuth, for `OAuthLogin::with_client_id`
376
    pub client_id: Option<String>,
377
}
378
379
impl SoftLogout {
380
    #[must_use]
381
    pub fn password_login(&self, password: impl Into<String>) -> PasswordLogin {
382
        PasswordLogin::new(self.homeserver.clone(), self.user_id.as_str(), password)
383
            .with_device_id(self.device_id.clone())
384
            .allow_insecure_loopback(true)
385
    }
386
387
    #[must_use]
388
    pub fn oauth_login(
389
        &self,
390
        redirect_uri: impl Into<String>,
391
        client_uri: impl Into<String>,
392
        application_type: OAuthApplicationType,
393
    ) -> OAuthLogin {
394
        let login = OAuthLogin::new(
395
            self.homeserver.clone(),
396
            redirect_uri,
397
            client_uri,
398
            application_type,
399
        )
400
        .with_device_id(self.device_id.clone())
401
        .allow_insecure_loopback(true);
402
        match &self.client_id {
403
            Some(client_id) => login.with_client_id(client_id.clone()),
404
            None => login,
405
        }
406
    }
407
}
408
409
#[derive(Debug, Error)]
410
pub enum AccountError {
411
    #[error("{context} failed")]
412
    Operation {
413
        context: &'static str,
414
        kind: ErrorKind,
415
        server: Option<ServerError>,
416
        #[source]
417
        source: Box<SourceError>,
418
    },
419
    #[error("the homeserver returned no authenticated session")]
420
    MissingSession,
421
    #[error("the SDK returned an unsupported authentication session")]
422
    UnsupportedAuthentication,
423
    #[error("the OAuth callback was already submitted")]
424
    OAuthCallbackAlreadySubmitted,
425
    /// what was typed can't name a homeserver, so nothing was sent anywhere
426
    #[error("that isn't a usable homeserver, {0}")]
427
    InvalidHomeserver(&'static str),
428
    /// `Accounts` already has a live login for them. a second would be another device
429
    #[error("{0} is already logged in")]
430
    AlreadyLoggedIn(OwnedUserId),
431
    /// an IndexedDB delete is waiting on the store's other connections. it goes through
432
    /// once they close, and opening the store again waits behind it
433
    #[error("the store is still open elsewhere, it will be deleted once that closes")]
434
    StoreDeletePending,
435
    /// another engine, in this process or another, has the store. only one runs per store
436
    #[error("another engine is using this store")]
437
    StoreInUse,
438
    /// the login worked but its first record didn't save. the server already has the
439
    /// device, so retry the save rather than log in again
440
    #[error("saving the new login's record failed")]
441
    Unsaved {
442
        session: Box<UnsavedSession>,
443
        #[source]
444
        source: Box<AccountError>,
445
    },
446
    #[error(transparent)]
447
    Engine(#[from] EngineError),
448
}
449
450
impl AccountError {
451
    /// what the homeserver said, when it refused
452
    #[must_use]
453
    pub fn server(&self) -> Option<&ServerError> {
454
        match self {
455
            Self::Operation { server, .. } => server.as_ref(),
456
            Self::Unsaved { source, .. } => source.server(),
457
            _ => None,
458
        }
459
    }
460
}
461
462
/// a live login whose record never reached its `SessionStore`
463
pub struct UnsavedSession {
464
    session: Session,
465
    sessions: Arc<dyn SessionStore>,
466
}
467
468
impl UnsavedSession {
469
    pub async fn save(self) -> Result<Session, AccountError> {
470
        let saved = match session_record(&self.session.client) {
471
            Ok(record) => self.sessions.save(&record).await,
472
            Err(error) => Err(error),
473
        };
474
        match saved {
475
            Ok(()) => Ok(self.session),
476
            Err(source) => Err(AccountError::Unsaved {
477
                session: Box::new(self),
478
                source: Box::new(source),
479
            }),
480
        }
481
    }
482
483
    /// the record to keep somewhere else, if the store won't take it
484
    pub fn record(&self) -> Result<SessionRecord, AccountError> {
485
        session_record(&self.session.client)
486
    }
487
}
488
489
impl std::fmt::Debug for UnsavedSession {
490
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
491
        formatter
492
            .debug_struct("UnsavedSession")
493
            .finish_non_exhaustive()
494
    }
495
}
496
497
impl Session {
498
    pub async fn login_password(
499
        login: PasswordLogin,
500
        sessions: Arc<dyn SessionStore>,
501
    ) -> Result<Self, AccountError> {
502
        homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
503
        let store_lock = sessions.store().lock().await?;
504
        Self::login_password_locked(login, sessions, store_lock).await
505
    }
506
507
    pub(crate) async fn login_password_locked(
508
        login: PasswordLogin,
509
        sessions: Arc<dyn SessionStore>,
510
        store_lock: StoreLock,
511
    ) -> Result<Self, AccountError> {
512
        let homeserver = homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
513
        let client = matrix_client_builder(sessions.store())
514
            .server_name_or_homeserver_url(homeserver)
515
            .build()
516
            .await
517
            .map_err(|error| account_error("building the Matrix client", error))?;
518
        let tracking = Tracking::start(&client, sessions.clone())?;
519
        let mut request = client
520
            .matrix_auth()
521
            .login_username(login.username.trim(), &login.password)
522
            .request_refresh_token();
523
        if let Some(name) = login.device_display_name.as_deref() {
524
            request = request.initial_device_display_name(name);
525
        }
526
        if let Some(device_id) = login.device_id.as_deref() {
527
            request = request.device_id(device_id.as_str());
528
        }
529
        request
530
            .send()
531
            .await
532
            .map_err(|error| account_error("password login", error))?;
533
        Self::saved(client, sessions, tracking, store_lock).await
534
    }
535
536
    /// the pending login lives only in this process, so the one that begins it has to finish it
537
    pub async fn begin_oauth(
538
        login: OAuthLogin,
539
        sessions: Arc<dyn SessionStore>,
540
    ) -> Result<PendingOAuthLogin, AccountError> {
541
        homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
542
        let store_lock = sessions.store().lock().await?;
543
        Self::begin_oauth_locked(login, sessions, store_lock).await
544
    }
545
546
    pub(crate) async fn begin_oauth_locked(
547
        login: OAuthLogin,
548
        sessions: Arc<dyn SessionStore>,
549
        store_lock: StoreLock,
550
    ) -> Result<PendingOAuthLogin, AccountError> {
551
        let redirect_uri = Url::parse(login.redirect_uri.trim())
552
            .map_err(|error| account_error("parsing the OAuth redirect URI", error))?;
553
        let client_uri = Url::parse(login.client_uri.trim())
554
            .map_err(|error| account_error("parsing the OAuth client URI", error))?;
555
        let homeserver = homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
556
        let client = matrix_client_builder(sessions.store())
557
            .server_name_or_homeserver_url(homeserver)
558
            .build()
559
            .await
560
            .map_err(|error| account_error("building the Matrix client", error))?;
561
        let tracking = Tracking::start(&client, sessions.clone())?;
562
        let application_type = match login.application_type {
563
            OAuthApplicationType::Native => ApplicationType::Native,
564
            OAuthApplicationType::Web => ApplicationType::Web,
565
        };
566
        let mut metadata = ClientMetadata::new(
567
            application_type,
568
            vec![OAuthGrantType::AuthorizationCode {
569
                redirect_uris: vec![redirect_uri.clone()],
570
            }],
571
            Localized::new(client_uri, []),
572
        );
573
        metadata.client_name = login.client_name.map(|name| Localized::new(name, []));
574
        let metadata = Raw::new(&metadata)
575
            .map_err(|error| account_error("serializing OAuth client metadata", error))?;
576
        let mut registration = ClientRegistrationData::new(metadata);
577
        if let Some(client_id) = login.client_id {
578
            registration.static_registrations = Some(HashMap::from([(
579
                client.homeserver(),
580
                ClientId::new(client_id),
581
            )]));
582
        }
583
        let authorization = client
584
            .oauth()
585
            .login(redirect_uri, login.device_id, Some(registration), None)
586
            .build()
587
            .await
588
            .map_err(|error| account_error("starting OAuth login", error))?;
589
        Ok(PendingOAuthLogin {
590
            client,
591
            sessions,
592
            tracking: Some((tracking, store_lock)),
593
            authorization_url: authorization.url.to_string(),
594
            state: Some(authorization.state),
595
        })
596
    }
597
598
    /// Restore the record `sessions` holds, or `None` when it holds none.
599
    /// The caller must also handle rejection and explicit logout.
600
    pub async fn restore(sessions: Arc<dyn SessionStore>) -> Result<Option<Restore>, AccountError> {
601
        let Some(record) = sessions.load().await? else {
602
            return Ok(None);
603
        };
604
        let authentication = match record.authentication {
605
            AuthenticationRecord::Matrix(session) => AuthSession::Matrix(session),
606
            AuthenticationRecord::OAuth { client_id, user } => {
607
                OAuthSession { client_id, user }.into()
608
            }
609
            AuthenticationRecord::SoftLoggedOut { meta, client_id } => {
610
                return Ok(Some(Restore::SoftLoggedOut(SoftLogout {
611
                    homeserver: record.homeserver,
612
                    user_id: meta.user_id,
613
                    device_id: meta.device_id,
614
                    client_id: client_id.map(|client_id| client_id.as_str().to_owned()),
615
                })));
616
            }
617
        };
618
        let store_lock = sessions.store().lock().await?;
619
        let client = matrix_client_builder(sessions.store())
620
            .homeserver_url(&record.homeserver)
621
            .build()
622
            .await
623
            .map_err(|error| account_error("reopening the Matrix store", error))?;
624
        let tracking = Tracking::start(&client, sessions)?;
625
        client
626
            .restore_session(authentication)
627
            .await
628
            .map_err(|error| account_error("restoring the authenticated session", error))?;
629
        Ok(Some(Restore::Resumed(Self {
630
            client,
631
            tracking,
632
            store_lock,
633
        })))
634
    }
635
636
    /// leave the record to the host, which then saves `ClientHandle::session_record()`
637
    /// on every `SessionTokensChanged`
638
    #[must_use]
639
    pub fn without_persistence(mut self) -> Self {
640
        self.tracking.persistence = None;
641
        #[cfg(not(target_family = "wasm"))]
642
        self.tracking.inline.store(false, Ordering::Release);
643
        self
644
    }
645
646
    pub async fn build(
647
        self,
648
        config: EngineConfig,
649
    ) -> Result<(Engine, ClientHandle, EventStream), AccountError> {
650
        Engine::with_persistence(
651
            self.client,
652
            config,
653
            self.tracking.session_changes,
654
            self.tracking.persistence,
655
            Some(self.store_lock),
656
        )
657
        .await
658
        .map_err(AccountError::from)
659
    }
660
661
    async fn saved(
662
        client: Client,
663
        sessions: Arc<dyn SessionStore>,
664
        tracking: Tracking,
665
        store_lock: StoreLock,
666
    ) -> Result<Self, AccountError> {
667
        UnsavedSession {
668
            session: Self {
669
                client,
670
                tracking,
671
                store_lock,
672
            },
673
            sessions,
674
        }
675
        .save()
676
        .await
677
    }
678
}
679
680
impl Tracking {
681
    fn start(client: &Client, sessions: Arc<dyn SessionStore>) -> Result<Self, AccountError> {
682
        let session_changes = client.subscribe_to_session_changes();
683
        #[cfg_attr(target_family = "wasm", allow(unused_variables))]
684
        let (results, inline_results) = mpsc::unbounded_channel();
685
        #[cfg(not(target_family = "wasm"))]
686
        let inline = save_inline(client, sessions.clone(), results)?;
687
        Ok(Self {
688
            session_changes,
689
            persistence: Some(Persistence {
690
                save: Box::new(move |client| {
691
                    let sessions = sessions.clone();
692
                    Box::pin(async move {
693
                        let record = session_record(&client).map_err(save_failed)?;
694
                        sessions.save(&record).await.map_err(save_failed)
695
                    })
696
                }),
697
                inline: inline_results,
698
            }),
699
            #[cfg(not(target_family = "wasm"))]
700
            inline,
701
        })
702
    }
703
}
704
705
/// the sdk calls this inside its refresh, after it has the new tokens and before it uses
706
/// them, so a store that writes there and then can't lose a refresh token to a crash
707
#[cfg(not(target_family = "wasm"))]
708
fn save_inline(
709
    client: &Client,
710
    sessions: Arc<dyn SessionStore>,
711
    results: mpsc::UnboundedSender<Result<(), crate::Error>>,
712
) -> Result<Arc<AtomicBool>, AccountError> {
713
    let enabled = Arc::new(AtomicBool::new(true));
714
    let saving = enabled.clone();
715
    client
716
        .set_session_callbacks(
717
            // only asked for with the cross process refresh lock, which we never enable
718
            Box::new(|client| {
719
                client
720
                    .session_tokens()
721
                    .ok_or_else(|| AccountError::MissingSession.into())
722
            }),
723
            Box::new(move |client| {
724
                if !saving.load(Ordering::Acquire) {
725
                    return Ok(());
726
                }
727
                let record = session_record(&client)?;
728
                if let Some(result) = sessions.save_blocking(&record) {
729
                    let _ = results.send(result.map_err(save_failed));
730
                }
731
                Ok(())
732
            }),
733
        )
734
        .map_err(|error| account_error("registering the session callbacks", error))?;
735
    Ok(enabled)
736
}
737
738
fn save_failed(error: AccountError) -> crate::Error {
739
    let message = format!("saving the session record failed, {error}");
740
    crate::Error::with_source(ErrorKind::Store, message, error)
741
}
742
743
pub(crate) fn session_record(client: &Client) -> Result<SessionRecord, AccountError> {
744
    let authentication = match client.session().ok_or(AccountError::MissingSession)? {
745
        AuthSession::Matrix(session) => AuthenticationRecord::Matrix(session),
746
        AuthSession::OAuth(session) => {
747
            let OAuthSession { client_id, user } = *session;
748
            AuthenticationRecord::OAuth { client_id, user }
749
        }
750
        _ => return Err(AccountError::UnsupportedAuthentication),
751
    };
752
    Ok(SessionRecord {
753
        homeserver: client.homeserver().to_string(),
754
        authentication,
755
    })
756
}
757
758
fn encryption_settings() -> EncryptionSettings {
759
    EncryptionSettings {
760
        auto_enable_cross_signing: true,
761
        backup_download_strategy: BackupDownloadStrategy::OneShot,
762
        auto_enable_backups: true,
763
    }
764
}
765
766
/// `StoreLock` already keeps every other client off the store, and the sdk's lease would cost
767
/// a store write per locked section and a task renewing it every 50ms
768
fn matrix_client_builder(store: &StoreConfig) -> ClientBuilder {
769
    let builder = Client::builder()
770
        .with_encryption_settings(encryption_settings())
771
        .with_threading_support(ThreadingSupport::Enabled {
772
            with_subscriptions: false,
773
        })
774
        .handle_refresh_tokens()
775
        .cross_process_store_config(CrossProcessLockConfig::SingleProcess);
776
    match &store.location {
777
        #[cfg(feature = "sqlite")]
778
        StoreLocation::Sqlite(path) => builder.sqlite_store(path, Some(&store.passphrase)),
779
        #[cfg(feature = "indexeddb")]
780
        StoreLocation::IndexedDb(name) => builder.indexeddb_store(name, Some(&store.passphrase)),
781
    }
782
}
783
784
/// a bare server name goes through untouched, since that's what makes the sdk try `.well-known`
785
pub(crate) fn homeserver_input(
786
    input: &str,
787
    allow_insecure_loopback: bool,
788
) -> Result<&str, AccountError> {
789
    let input = input.trim();
790
    let invalid = |reason| Err(AccountError::InvalidHomeserver(reason));
791
    if input.is_empty() {
792
        return invalid("it's empty");
793
    }
794
    if input.contains(char::is_whitespace) {
795
        return invalid("it has spaces in it");
796
    }
797
    if !input.contains("://") {
798
        return match <&ruma::ServerName>::try_from(input) {
799
            Ok(_) => Ok(input),
800
            Err(_) => invalid("it's neither a server name nor a url"),
801
        };
802
    }
803
    let Ok(url) = Url::parse(input) else {
804
        return invalid("it isn't a valid url");
805
    };
806
    if !url.username().is_empty() || url.password().is_some() {
807
        return invalid("it has a username in it");
808
    }
809
    let loopback = match url.host() {
810
        None => return invalid("it has no host"),
811
        Some(url::Host::Domain(domain)) => domain.eq_ignore_ascii_case("localhost"),
812
        Some(url::Host::Ipv4(address)) => address.is_loopback(),
813
        Some(url::Host::Ipv6(address)) => address.is_loopback(),
814
    };
815
    match url.scheme() {
816
        "https" => Ok(input),
817
        "http" if allow_insecure_loopback && loopback => Ok(input),
818
        "http" if allow_insecure_loopback => invalid("plain http only reaches loopback hosts"),
819
        "http" => invalid("it isn't https"),
820
        _ => invalid("only http and https are spoken"),
821
    }
822
}
823
824
pub(crate) fn account_error(
825
    context: &'static str,
826
    source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
827
) -> AccountError {
828
    AccountError::Operation {
829
        context,
830
        kind: crate::error::classify(&source),
831
        server: ServerError::find(&source),
832
        source: Box::new(source),
833
    }
834
}