anvilsign in

collin/anvil

1//! Core domain model, persistence, and on-disk repository storage for anvil.
2//!
3//! This crate is transport-agnostic: it knows about users, repositories, the
4//! SQLite database, and bare git repositories on disk, but nothing about HTTP,
5//! SSH, or the git wire protocol. Higher layers (`anvil-web`, `anvil-ssh`,
6//! `anvil-git`) build on top of it.
7
8pub mod access;
9pub mod api_tokens;
10pub mod attachments;
11pub mod ci;
12pub mod config;
13pub mod db;
14pub mod error;
15pub mod issues;
16pub mod models;
17pub mod repos;
18pub mod sessions;
19pub mod ssh_keys;
20pub mod storage;
21pub mod usage;
22pub mod users;
23
24pub use config::Config;
25pub use error::{
26 Error,
27 Result,
28};
29pub use models::{
30 ApiToken,
31 Attachment,
32 CiArtifact,
33 CiRun,
34 Issue,
35 IssueComment,
36 Repository,
37 Session,
38 SshKey,
39 User,
40};
41
42/// Current Unix time in seconds, for `created_at` columns.
43pub(crate) fn now() -> i64 {
44 std::time::SystemTime::now()
45 .duration_since(std::time::UNIX_EPOCH)
46 .map(|d| d.as_secs() as i64)
47 .unwrap_or(0)
48}
49
50/// Shared application state: configuration plus a database handle.
51///
52/// Cloneable and cheap to pass around — `toasty::Db` is internally reference
53/// counted and backed by a connection pool.
54#[derive(Clone)]
55pub struct App {
56 pub config: Config,
57 pub db: toasty::Db,
58 /// Notifies the CI runner of newly-enqueued run ids. `None` until the runner
59 /// is started (e.g. CLI commands don't run CI). Use [`App::notify_ci`].
60 pub ci_tx: Option<tokio::sync::mpsc::UnboundedSender<i64>>,
61 /// Server-wide secret keying CSRF tokens. Persisted in the data dir so
62 /// tokens survive restarts. Wrapped in `Arc` to keep `App: Clone` cheap.
63 csrf_secret: std::sync::Arc<[u8; 32]>,
64}
65
66impl App {
67 /// Initialize application state from a config: ensure the data directories
68 /// exist, then open the database and create the schema.
69 pub async fn bootstrap(config: Config) -> Result<Self> {
70 std::fs::create_dir_all(&config.data_dir)?;
71 std::fs::create_dir_all(config.repositories_dir())?;
72
73 let db = db::connect(config.database_path()).await?;
74 let csrf_secret = std::sync::Arc::new(load_or_create_csrf_secret(&config.data_dir)?);
75
76 Ok(Self {
77 config,
78 db,
79 ci_tx: None,
80 csrf_secret,
81 })
82 }
83
84 /// Notify the CI runner that `run_id` is queued (no-op if no runner).
85 pub fn notify_ci(&self, run_id: i64) {
86 if let Some(tx) = &self.ci_tx {
87 let _ = tx.send(run_id);
88 }
89 }
90
91 /// The CSRF token bound to a given session token: `HMAC-SHA256(secret,
92 /// session)`, hex-encoded. Stable for a session's lifetime, unguessable
93 /// without the server secret, and requires no extra storage.
94 pub fn csrf_token(&self, session_token: &str) -> String {
95 use hmac::{
96 Hmac,
97 Mac,
98 };
99 let mut mac = Hmac::<sha2::Sha256>::new_from_slice(self.csrf_secret.as_slice())
100 .expect("HMAC accepts any key length");
101 mac.update(session_token.as_bytes());
102 mac.finalize()
103 .into_bytes()
104 .iter()
105 .map(|b| format!("{b:02x}"))
106 .collect()
107 }
108}
109
110/// Load the persistent CSRF secret, generating and saving it on first run.
111fn load_or_create_csrf_secret(data_dir: &std::path::Path) -> Result<[u8; 32]> {
112 use argon2::password_hash::rand_core::{
113 OsRng,
114 RngCore,
115 };
116
117 let path = data_dir.join("csrf_secret");
118 if path.exists() {
119 let bytes = std::fs::read(&path)?;
120 if let Ok(secret) = <[u8; 32]>::try_from(bytes.as_slice()) {
121 return Ok(secret);
122 }
123 // Malformed (truncated/extended) — regenerate rather than run weak.
124 }
125 let mut secret = [0u8; 32];
126 OsRng.fill_bytes(&mut secret);
127 std::fs::write(&path, secret)?;
128 #[cfg(unix)]
129 {
130 use std::os::unix::fs::PermissionsExt;
131 let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
132 }
133 Ok(secret)
134}