371 lines
13 KiB
Rust
371 lines
13 KiB
Rust
|
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<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
|
||
|
}))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|