use std::fmt; use axum::http::{self, StatusCode}; use axum::response::{AppendHeaders, IntoResponse, Response}; use axum::Json; use serde::{Serialize, Serializer}; use serde_json::{Value, json}; use crate::dns::{DnsDriverError, ZoneDriver}; use crate::dns::record::{RecordError, RecordParseError}; use crate::ressouces::zone::ZoneError; use crate::validation::{DomainValidationError, TxtParseError}; #[derive(Debug, Serialize)] pub struct Error { #[serde(skip)] cause: Option, #[serde(skip_serializing_if = "Option::is_none", serialize_with = "serialize_status")] status: Option, code: String, description: String, #[serde(skip_serializing_if = "Option::is_none")] details: Option, #[serde(skip_serializing_if = "Option::is_none")] path: Option, #[serde(skip_serializing_if = "Option::is_none")] errors: Option>, } pub fn serialize_status(status: &Option, serializer: S) -> Result where S: Serializer { if let Some(status) = status { serializer.serialize_u16(status.as_u16()) } else { serializer.serialize_unit() } } impl Error { pub fn new(code: &str, description: &str) -> Self { Error { cause: None, status: None, code: code.into(), description: description.into(), details: None, path: None, errors: None } } pub fn with_cause(self, cause: &str) -> Self { Self { cause: Some(cause.into()), ..self } } pub fn with_status(self, status: StatusCode) -> Self { Self { status: Some(status), ..self } } pub fn with_path(self, path: &str) -> Self { if let Some(current_path) = self.path { Self { path: Some(format!("{path}{current_path}")), ..self } } else { Self { path: Some(path.into()), ..self } } } pub fn with_details (self, details: T) -> Self { let mut new_details = serde_json::to_value(details).expect("failed to convert details to serde_json::Value"); let details = self.details; // append new details to existing details if let Some(mut details) = details { if let Some(object) = details.as_object_mut() { if let Some(new_object) = new_details.as_object_mut() { object.append(new_object); return Self { details: Some(details), ..self } } } } Self { details: Some(new_details), ..self } } pub fn with_suberrors(self, mut errors: Vec) -> Self { for error in &mut errors { error.status = None; } Self { errors: Some(errors), ..self } } pub fn override_status(self, status: StatusCode) -> Self { if self.status.is_some() { self.with_status(status) } else { self } } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.description)?; if let Some(cause) = &self.cause { write!(f, ": {}", cause)?; } if self.status.is_some() || self.details.is_some() { write!(f, " (")?; } if let Some(status) = &self.status { write!(f, "status = {}", status)?; } if let Some(details) = &self.details { if self.status.is_some() { write!(f, ", ")?; } write!(f, "details = {}", serde_json::to_string(details).expect("Failed to serialize error details"))?; } if self.status.is_some() || self.details.is_some() { write!(f, ")")?; } Ok(()) } } impl IntoResponse for Error { fn into_response(self) -> Response { if let Some(status) = self.status { (status, Json(self)).into_response() } else { eprintln!("{}", self); ( StatusCode::INTERNAL_SERVER_ERROR, AppendHeaders([ (http::header::CONTENT_TYPE, "application/json") ]), r#"{"status": 500,"description":"Internal server error","code":"internal"}"# ).into_response() } } } impl From> for Error { fn from(value: bb8::RunError) -> Self { Error::new("db:pool", "Failed to get database connection from pool") .with_cause(&value.to_string()) } } impl From for Error { fn from(value: rusqlite::Error) -> Self { Error::new("db:sqlite", "Sqlite failure") .with_cause(&format!("{:?}", value)) } } impl From for Error { fn from(value: ZoneError) -> Self { match value { ZoneError::ZoneConflict { name } => { Error::new("zone:conflict", "Zone {zone_name} already exists") .with_details(json!({ "zone_name": name })) .with_status(StatusCode::CONFLICT) }, ZoneError::NotFound { name } => { Error::new("zone:not_found", "The zone {zone_name} could not be found") .with_details(json!({ "zone_name": name })) .with_status(StatusCode::NOT_FOUND) }, ZoneError::Validation { suberrors } => { Error::new("zone:validation", "Error while validating zone input data") .with_suberrors(suberrors) .with_status(StatusCode::BAD_REQUEST) }, ZoneError::NotExistsNs { name } => { Error::new("zone:not_exists_ns", "The zone {zone_name} does not exist on the name server") .with_details(json!({ "zone_name": name })) .with_status(StatusCode::BAD_REQUEST) } } } } impl From for Error { fn from(value: DomainValidationError) -> Self { match value { DomainValidationError::CharactersNotPermitted { label } => { Error::new("domain:characters_not_permitted", "Domain name label {label} contains characters not permitted. The allowed characters are lowercase alphanumeric characters (a-z and 0-9), the dash ('-'), the underscore ('_') and the forward slash ('/').") .with_details(json!({ "label": label })) }, DomainValidationError::EmptyDomain => { Error::new("domain:empty_domain", "Domain name can not be empty or the root domain ('.')") }, DomainValidationError::EmptyLabel => { Error::new("domain:empty_label", "Domain name contains empty labels (repeated dots)") }, DomainValidationError::DomainTooLong { length } => { Error::new("domain:domain_too_long", "Domain name too long ({length} characters), the maximum length is 255 characters") .with_details(json!({ "length": length })) }, DomainValidationError::LabelToolLong { length, label } => { Error::new("domain:label_too_long", "Domain name label {label} is too long ({label_length} characters), the maximum length is 63 characters") .with_details(json!({ "label": label, "length": length, })) }, } } } impl From for Error { fn from(value: TxtParseError) -> Self { match value { TxtParseError::BadEscapeDigitIndexTooHigh { sequence } => { Error::new("record:txt:parse:escape_decimal_index_too_high", "Octect escape sequence should be between 000 and 255. Offending escape sequence: \\{sequence}") .with_details(json!({ "sequence": sequence })) }, TxtParseError::BadEscapeDigitsNotDigits { sequence } => { Error::new("record:txt:parse:escape_decimal_not_digits", "Expected an octect escape sequence due to the presence of a back slash (\\) followed by a digit but found non digit characters. Offending escape sequence: \\{sequence}") .with_details(json!({ "sequence": sequence })) }, TxtParseError::BadEscapeDigitsTooShort { sequence } => { Error::new("record:txt:parse:escape_decimal_too_short", "Expected an octect escape sequence due to the presence of a back slash (\\) followed by a digit but found found {sequence_lenght} characters instead of three. Offending escape sequence: \\{sequence}") .with_details(json!({ "sequence": sequence, "sequence_lenght": sequence.len() })) }, TxtParseError::MissingEscape => { Error::new("record:txt:parse:escape_missing", "Expected an escape sequence due to the presence of a back slash (\\) at the end of the input but found nothing") }, TxtParseError::NonAscii { character } => { Error::new("record:txt:parse:non_ascii", "Found a non ASCII character ({character}). Only printable ASCII characters are allowed.") .with_details(json!({ "character": character })) } } } } impl From for Error { fn from(value: DnsDriverError) -> Self { match value { DnsDriverError::ConnectionError { reason } => { Error::new("dns:connection", "Error while connecting to the name server") .with_cause(&reason.to_string()) }, DnsDriverError::OperationError { reason } => { Error::new("dns:operation", "DNS operation error") .with_cause(&reason.to_string()) }, DnsDriverError::ServerError { rcode, name, qtype } => { Error::new("dns:server", "Unexpected response to query") .with_details(json!({ "rcode": rcode, "name": name, "qtype": qtype, })) }, DnsDriverError::ZoneNotFound { name } => { Error::new("dns:zone_not_found", "The zone {zone_name} does not exist on the name server") .with_details(json!({ "zone_name": name })) } } } } impl From for Error { fn from(value: RecordParseError) -> Self { match value { RecordParseError::Ip4Address { input } => { Error::new("record:parse:ip4", "The following IPv4 address {input} is invalid. IPv4 addresses should have four numbers, each between 0 and 255, separated by dots.") .with_details(json!({ "input": input })) }, RecordParseError::Ip6Address { input } => { Error::new("record:parse:ip6", "The following IPv4 address {input} is invalid. IPv6 addresses should have eight groups of four hexadecimal digit separated by colons. Leftmost zeros in a group can be omitted, sequence of zeros can be shorted by a double colons.") .with_details(json!({ "input": input })) }, RecordParseError::RDataUnknown { input, field, rtype } => { Error::new("record:parse:rdata_unknown", "Unknown error while parsing record rdata field") .with_details(json!({ "input": input, "field": field, "rtype": rtype, })) }, RecordParseError::NameUnknown { input } => { Error::new("record:parse:name_unknown", "Unknown error while parsing record name") .with_details(json!({ "input": input })) }, RecordParseError::NotInZone { name, zone } => { Error::new("record:parse:not_in_zone", "The domain name {name} is not in the current zone ({zone})") .with_details(json!({ "name": name, "zone": zone })) } } } } impl From for Error { fn from(value: RecordError) -> Self { match value { RecordError::Validation { suberrors } => { Error::new("record:validation", "Error while validating input records") .with_suberrors(suberrors) .with_status(StatusCode::BAD_REQUEST) } } } }