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