use tracing::debug;
use crate::db::Database;
pub struct Http01Token;
impl Http01Token {
pub async fn publish(
token: &str,
key_authorization: &str,
now: i64,
expires_at: i64,
database: &Database,
) -> Result<(), sqlx::Error> {
crate::sql::query(
"INSERT INTO http01_tokens (token, key_authorization, created_at, expires_at) \
VALUES (?, ?, ?, ?) \
ON CONFLICT (token) DO UPDATE SET \
key_authorization = excluded.key_authorization, expires_at = excluded.expires_at;",
)
.bind(token)
.bind(key_authorization)
.bind(now)
.bind(expires_at)
.execute(database)
.await?;
debug!(event = "db_http_01_token_published", outcome = "success", token = %token, expires_at);
Ok(())
}
pub async fn retract(token: &str, database: &Database) -> Result<(), sqlx::Error> {
let result = crate::sql::query("DELETE FROM http01_tokens WHERE token = ?;")
.bind(token)
.execute(database)
.await?;
debug!(
event = "db_http_01_token_retracted",
outcome = "success",
token = %token,
rows_removed = result.rows_affected(),
);
Ok(())
}
pub async fn lookup(
token: &str,
now: i64,
database: &Database,
) -> Result<Option<String>, sqlx::Error> {
crate::sql::query(
"SELECT key_authorization FROM http01_tokens WHERE token = ? AND expires_at > ?;",
)
.bind(token)
.bind(now)
.fetch_optional(database)
.await?
.map(|row| row.try_get("key_authorization"))
.transpose()
}
pub async fn cleanup(now: i64, database: &Database) -> Result<u64, sqlx::Error> {
let result = crate::sql::query("DELETE FROM http01_tokens WHERE expires_at <= ?;")
.bind(now)
.execute(database)
.await?;
debug!(
event = "db_http_01_token_cleanup_completed",
outcome = "success",
rows_removed = result.rows_affected(),
cutoff = now,
);
Ok(result.rows_affected())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn a_published_token_is_looked_up_until_retracted() {
let database = Database::connect_for_test().await.unwrap();
assert_eq!(
Http01Token::lookup("tok", 10, &database).await.unwrap(),
None
);
Http01Token::publish("tok", "tok.one", 10, 100, &database)
.await
.unwrap();
assert_eq!(
Http01Token::lookup("tok", 10, &database).await.unwrap(),
Some("tok.one".to_string())
);
Http01Token::retract("tok", &database).await.unwrap();
Http01Token::retract("tok", &database).await.unwrap();
assert_eq!(
Http01Token::lookup("tok", 10, &database).await.unwrap(),
None
);
}
#[tokio::test]
async fn publishing_again_replaces_the_row() {
let database = Database::connect_for_test().await.unwrap();
Http01Token::publish("tok", "tok.one", 10, 20, &database)
.await
.unwrap();
Http01Token::publish("tok", "tok.two", 15, 200, &database)
.await
.unwrap();
assert_eq!(
Http01Token::lookup("tok", 100, &database).await.unwrap(),
Some("tok.two".to_string())
);
}
#[tokio::test]
async fn an_expired_token_is_not_served_and_is_swept() {
let database = Database::connect_for_test().await.unwrap();
Http01Token::publish("old", "old.ka", 0, 50, &database)
.await
.unwrap();
Http01Token::publish("live", "live.ka", 0, 500, &database)
.await
.unwrap();
assert_eq!(
Http01Token::lookup("old", 50, &database).await.unwrap(),
None
);
assert_eq!(Http01Token::cleanup(50, &database).await.unwrap(), 1);
assert_eq!(
Http01Token::lookup("live", 50, &database).await.unwrap(),
Some("live.ka".to_string())
);
}
}