use axum::http::{header, StatusCode};
use axum::response::{IntoResponse, Response};
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AcmeErrorType {
Malformed,
Unauthorized,
BadNonce,
BadSignatureAlgorithm,
BadCsr,
RejectedIdentifier,
ExternalAccountRequired,
AccountDoesNotExist,
ServerInternal,
RateLimited,
OrderNotReady,
}
impl AcmeErrorType {
pub fn urn(self) -> &'static str {
match self {
Self::Malformed => "urn:ietf:params:acme:error:malformed",
Self::Unauthorized => "urn:ietf:params:acme:error:unauthorized",
Self::BadNonce => "urn:ietf:params:acme:error:badNonce",
Self::BadSignatureAlgorithm => "urn:ietf:params:acme:error:badSignatureAlgorithm",
Self::BadCsr => "urn:ietf:params:acme:error:badCSR",
Self::RejectedIdentifier => "urn:ietf:params:acme:error:rejectedIdentifier",
Self::ExternalAccountRequired => "urn:ietf:params:acme:error:externalAccountRequired",
Self::AccountDoesNotExist => "urn:ietf:params:acme:error:accountDoesNotExist",
Self::ServerInternal => "urn:ietf:params:acme:error:serverInternal",
Self::RateLimited => "urn:ietf:params:acme:error:rateLimited",
Self::OrderNotReady => "urn:ietf:params:acme:error:orderNotReady",
}
}
pub fn status(self) -> StatusCode {
match self {
Self::Malformed
| Self::BadNonce
| Self::BadSignatureAlgorithm
| Self::BadCsr
| Self::RejectedIdentifier => StatusCode::BAD_REQUEST,
Self::Unauthorized
| Self::ExternalAccountRequired
| Self::AccountDoesNotExist
| Self::OrderNotReady => StatusCode::FORBIDDEN,
Self::RateLimited => StatusCode::TOO_MANY_REQUESTS,
Self::ServerInternal => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
#[derive(Debug)]
pub struct AcmeProblem {
error_type: AcmeErrorType,
detail: String,
nonce: Option<String>,
}
#[derive(Debug, Serialize)]
struct ProblemBody {
#[serde(rename = "type")]
type_: &'static str,
detail: String,
status: u16,
}
impl AcmeProblem {
pub fn new(error_type: AcmeErrorType, detail: impl Into<String>) -> Self {
Self {
error_type,
detail: detail.into(),
nonce: None,
}
}
pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
self.nonce = Some(nonce.into());
self
}
pub fn error_type(&self) -> AcmeErrorType {
self.error_type
}
}
impl IntoResponse for AcmeProblem {
fn into_response(self) -> Response {
let status = self.error_type.status();
let body = ProblemBody {
type_: self.error_type.urn(),
detail: self.detail,
status: status.as_u16(),
};
let json = serde_json::to_string(&body).unwrap_or_else(|_| {
format!(
"{{\"type\":\"{}\",\"status\":{}}}",
self.error_type.urn(),
status.as_u16()
)
});
let mut resp = Response::builder()
.status(status)
.header(header::CONTENT_TYPE, "application/problem+json");
if let Some(nonce) = self.nonce {
resp = resp.header("Replay-Nonce", nonce);
}
resp.body(json.into())
.unwrap_or_else(|_| status.into_response())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn urns_are_rfc8555_namespaced() {
assert_eq!(
AcmeErrorType::BadNonce.urn(),
"urn:ietf:params:acme:error:badNonce"
);
assert_eq!(
AcmeErrorType::RejectedIdentifier.urn(),
"urn:ietf:params:acme:error:rejectedIdentifier"
);
assert_eq!(
AcmeErrorType::BadCsr.urn(),
"urn:ietf:params:acme:error:badCSR"
);
}
#[test]
fn statuses_match_rfc() {
assert_eq!(AcmeErrorType::BadNonce.status(), StatusCode::BAD_REQUEST);
assert_eq!(
AcmeErrorType::RejectedIdentifier.status(),
StatusCode::BAD_REQUEST
);
assert_eq!(AcmeErrorType::Unauthorized.status(), StatusCode::FORBIDDEN);
assert_eq!(
AcmeErrorType::RateLimited.status(),
StatusCode::TOO_MANY_REQUESTS
);
}
#[test]
fn body_serializes_to_problem_json() {
let body = ProblemBody {
type_: AcmeErrorType::Malformed.urn(),
detail: "bad request".into(),
status: 400,
};
let json = serde_json::to_string(&body).unwrap();
assert!(json.contains("\"type\":\"urn:ietf:params:acme:error:malformed\""));
assert!(json.contains("\"detail\":\"bad request\""));
assert!(json.contains("\"status\":400"));
}
}