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