summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2025-08-30 02:59:51 -0400
committerOwen Jacobson <owen@grimoire.ca>2025-08-30 02:59:51 -0400
commit96b62f1018641b3dc28cc189d314a1bff475b751 (patch)
tree1e10b660f2c68cb73d3c17703257ec0be51c36af /src
parent3bd63e20777126216777b392615dc8144f21bb9a (diff)
Allow administrators to rotate VAPID keys immediately if needed.
In spite of my design preference away from CLI tools, this is a CLI tool: `pilcrow --database-url <URL> rotate-vapid-key`. This is something we can implement here and now, which does not require us to confront the long-avoided issue of how to handle the idea that some users are allowed to make server-operational changes and some aren't, by delegating the problem back to the OS. The implementation is a little half-baked to make it easier to rip out later. I would ordinarily prefer to push both `serve` (the default verb, not actually named in this change) and `rotate-vapid-key` into their own, separate CLI submodules, with their own argument structs, but that change is much more intrusive and would make this effectively permanent. This can be yanked out in a few minutes by deleting a few lines of `cli.rs` and inlining the `serve` function. Nonetheless, nothing is as permanent as a temporary solution, so I've written at least some bare-minimum operations documentation on how to use this and what it does.
Diffstat (limited to 'src')
-rw-r--r--src/cli.rs25
-rw-r--r--src/vapid/app.rs17
2 files changed, 39 insertions, 3 deletions
diff --git a/src/cli.rs b/src/cli.rs
index 378686b..263a4fd 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -10,7 +10,8 @@ use axum::{
middleware,
response::{IntoResponse, Response},
};
-use clap::{CommandFactory, Parser};
+use chrono::Utc;
+use clap::{CommandFactory, Parser, Subcommand};
use sqlx::sqlite::SqlitePool;
use tokio::net;
@@ -65,6 +66,15 @@ pub struct Args {
/// upgrades
#[arg(short = 'D', long, env, default_value = "sqlite://pilcrow.db.backup")]
backup_database_url: String,
+
+ #[command(subcommand)]
+ command: Option<Command>,
+}
+
+#[derive(Subcommand)]
+enum Command {
+ /// Immediately rotate the server's VAPID (Web Push) application key.
+ RotateVapidKey,
}
impl Args {
@@ -89,6 +99,16 @@ impl Args {
let pool = self.pool().await?;
let app = App::from(pool);
+
+ match self.command {
+ None => self.serve(app).await?,
+ Some(Command::RotateVapidKey) => app.vapid().rotate_key(&Utc::now()).await?,
+ }
+
+ Result::<_, Error>::Ok(())
+ }
+
+ async fn serve(self, app: App) -> Result<(), Error> {
let app = routes::routes(&app)
.route_layer(middleware::from_fn(clock::middleware))
.route_layer(middleware::map_response(Self::server_info()))
@@ -101,7 +121,7 @@ impl Args {
println!("{started_msg}");
serve.await?;
- Result::<_, Error>::Ok(())
+ Ok(())
}
async fn listener(&self) -> io::Result<net::TcpListener> {
@@ -140,5 +160,6 @@ fn started_msg(listener: &net::TcpListener) -> io::Result<String> {
enum Error {
Io(#[from] io::Error),
Database(#[from] db::Error),
+ Sqlx(#[from] sqlx::Error),
Umask(#[from] umask::Error),
}
diff --git a/src/vapid/app.rs b/src/vapid/app.rs
index 8886c9f..ddb1f4d 100644
--- a/src/vapid/app.rs
+++ b/src/vapid/app.rs
@@ -1,4 +1,4 @@
-use chrono::TimeDelta;
+use chrono::{TimeDelta, Utc};
use sqlx::SqlitePool;
use super::{History, repo, repo::Provider as _};
@@ -18,6 +18,21 @@ impl<'a> Vapid<'a> {
Self { db, events }
}
+ pub async fn rotate_key(&self, rotate_at: &DateTime) -> Result<(), sqlx::Error> {
+ let mut tx = self.db.begin().await?;
+ // This is called from a separate CLI utility (see `cli.rs`), and we _can't_ deliver events
+ // to active clients from another process, so don't do anything that would require us to
+ // send events, like generating a new key.
+ //
+ // Instead, the server's next `refresh_key` call will generate a key and notify clients
+ // of the change. All we have to do is remove the existing key, so that the server can know
+ // to do so.
+ tx.vapid().clear().await?;
+ tx.commit().await?;
+
+ Ok(())
+ }
+
pub async fn refresh_key(&self, ensure_at: &DateTime) -> Result<(), Error> {
let mut tx = self.db.begin().await?;
let key = tx.vapid().current().await.optional()?;