nomilo/src/errors.rs

390 lines
14 KiB
Rust
Raw Normal View History

2024-12-15 20:21:03 +00:00
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};
2024-12-22 21:36:26 +00:00
use crate::dns::DnsDriverError;
use crate::ressouces::record::{RecordError, RecordParseError};
2024-12-15 20:21:03 +00:00
use crate::ressouces::zone::ZoneError;
use crate::validation::{DomainValidationError, TxtParseError};
2024-12-22 21:36:26 +00:00
use crate::template::TemplateError;
2024-12-15 20:21:03 +00:00
#[derive(Debug, Serialize)]
pub struct Error {
#[serde(skip)]
cause: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", serialize_with = "serialize_status")]
status: Option<StatusCode>,
code: String,
description: String,
#[serde(skip_serializing_if = "Option::is_none")]
details: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
errors: Option<Vec<Error>>,
}
pub fn serialize_status<S>(status: &Option<StatusCode>, serializer: S) -> Result<S::Ok, S::Error>
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<T: Serialize> (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<Error>) -> 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<bb8::RunError<rusqlite::Error>> for Error {
fn from(value: bb8::RunError<rusqlite::Error>) -> Self {
Error::new("db:pool", "Failed to get database connection from pool")
.with_cause(&value.to_string())
}
}
impl From<rusqlite::Error> for Error {
fn from(value: rusqlite::Error) -> Self {
Error::new("db:sqlite", "Sqlite failure")
.with_cause(&format!("{:?}", value))
}
}
impl From<ZoneError> 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<DomainValidationError> 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<TxtParseError> 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<DnsDriverError> 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<RecordParseError> 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
}))
}
}
}
}
2024-12-22 21:36:26 +00:00
impl From<TemplateError> for Error {
fn from(value: TemplateError) -> Self {
match value {
TemplateError::RenderError { name, reason } => {
Error::new("template:render", "Failed to render the template")
.with_details(json!({
"name": name
}))
.with_cause(&reason.to_string())
},
TemplateError::SerializationError { reason } => {
Error::new("template:serialization", "Failed to serialize context")
.with_cause(&reason.to_string())
}
}
}
}
2024-12-15 20:21:03 +00:00
impl From<RecordError > 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)
}
}
}
}