194 lines
6.8 KiB
Rust
194 lines
6.8 KiB
Rust
|
|
//! The failure type embedders match on, and the status-code mapping it is
|
||
|
|
//! built from.
|
||
|
|
|
||
|
|
use std::fmt;
|
||
|
|
|
||
|
|
use crate::crypto::{b32, KEY_LEN};
|
||
|
|
use crate::transport::MAX_CHAIN;
|
||
|
|
|
||
|
|
/// Every failure the library reports, distinguished the way a caller needs
|
||
|
|
/// to decide what to do — no prose parsing. Each variant documents the
|
||
|
|
/// decision it supports; `Display` still produces a user-readable message.
|
||
|
|
#[derive(Debug)]
|
||
|
|
pub enum Error {
|
||
|
|
/// The wire status codes of SPEC.md sec 12, one variant each, carrying
|
||
|
|
/// what was being attempted. `UnknownStatus` is an unassigned code.
|
||
|
|
Malformed(String),
|
||
|
|
BadVersion(String),
|
||
|
|
UnknownUser(String),
|
||
|
|
AuthRequired(String),
|
||
|
|
AuthFailed(String),
|
||
|
|
QuotaExceeded(String),
|
||
|
|
TooLarge(String),
|
||
|
|
RateLimited(String),
|
||
|
|
NotPermitted(String),
|
||
|
|
InternalError(String),
|
||
|
|
UnknownStatus(u8, String),
|
||
|
|
/// The host has no pinned server key, so the session cannot be
|
||
|
|
/// authenticated (sec 4). A GUI routes this to pinning; an auth failure
|
||
|
|
/// is terminal.
|
||
|
|
NotPinned { host: String },
|
||
|
|
/// A pinned server presented a different key (sec 4): a hard abort, the
|
||
|
|
/// pin is the entire trust model.
|
||
|
|
PinMismatch {
|
||
|
|
host: String,
|
||
|
|
pinned: [u8; KEY_LEN],
|
||
|
|
presented: [u8; KEY_LEN],
|
||
|
|
},
|
||
|
|
/// A contact's key changed with no valid rotation chain (sec 7): the
|
||
|
|
/// user confirms out of band and imports, rather than clicking through.
|
||
|
|
KeyChanged {
|
||
|
|
address: String,
|
||
|
|
known: [u8; KEY_LEN],
|
||
|
|
offered: [u8; KEY_LEN],
|
||
|
|
},
|
||
|
|
/// The store has no account yet; registering or restoring creates one.
|
||
|
|
NotRegistered,
|
||
|
|
/// The rotation index would exceed the chain limit (sec 2).
|
||
|
|
ChainLimit { index: u32 },
|
||
|
|
/// The store was written by a schema this build cannot read; a host
|
||
|
|
/// decides when to migrate, not the library.
|
||
|
|
SchemaVersion { found: i32, expected: i32 },
|
||
|
|
/// The server could not be reached at all.
|
||
|
|
Unreachable {
|
||
|
|
host: String,
|
||
|
|
port: u16,
|
||
|
|
source: std::io::Error,
|
||
|
|
},
|
||
|
|
Storage(rusqlite::Error),
|
||
|
|
Noise(snow::Error),
|
||
|
|
Io(std::io::Error),
|
||
|
|
/// Validation prose without a decision-relevant variant of its own.
|
||
|
|
Other(String),
|
||
|
|
}
|
||
|
|
|
||
|
|
impl fmt::Display for Error {
|
||
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
|
match self {
|
||
|
|
Self::Malformed(what) => write!(f, "{what} failed: malformed (1)"),
|
||
|
|
Self::BadVersion(what) => write!(f, "{what} failed: bad version (2)"),
|
||
|
|
Self::UnknownUser(what) => write!(f, "{what} failed: unknown user (3)"),
|
||
|
|
Self::AuthRequired(what) => write!(f, "{what} failed: auth required (4)"),
|
||
|
|
Self::AuthFailed(what) => write!(f, "{what} failed: auth failed (5)"),
|
||
|
|
Self::QuotaExceeded(what) => write!(f, "{what} failed: quota exceeded (6)"),
|
||
|
|
Self::TooLarge(what) => write!(f, "{what} failed: too large (7)"),
|
||
|
|
Self::RateLimited(what) => write!(f, "{what} failed: rate limited (8)"),
|
||
|
|
Self::NotPermitted(what) => write!(f, "{what} failed: not permitted (9)"),
|
||
|
|
Self::InternalError(what) => write!(f, "{what} failed: internal error (10)"),
|
||
|
|
Self::UnknownStatus(status, what) => {
|
||
|
|
write!(f, "{what} failed: unknown status {status}")
|
||
|
|
}
|
||
|
|
Self::NotPinned { host } => write!(f, "no pinned key for {host}"),
|
||
|
|
Self::PinMismatch {
|
||
|
|
host,
|
||
|
|
pinned,
|
||
|
|
presented,
|
||
|
|
} => write!(
|
||
|
|
f,
|
||
|
|
"{host} presented a different key than the one pinned\n pinned: {}\n presented: {}",
|
||
|
|
b32(pinned),
|
||
|
|
b32(presented)
|
||
|
|
),
|
||
|
|
Self::KeyChanged {
|
||
|
|
address,
|
||
|
|
known,
|
||
|
|
offered,
|
||
|
|
} => write!(
|
||
|
|
f,
|
||
|
|
"{address} presents a different key with no valid rotation chain.\n known: {}\n offered: {}",
|
||
|
|
b32(known),
|
||
|
|
b32(offered)
|
||
|
|
),
|
||
|
|
Self::NotRegistered => write!(f, "not registered"),
|
||
|
|
Self::ChainLimit { index } => write!(
|
||
|
|
f,
|
||
|
|
"rotation index {index} exceeds the chain limit of {MAX_CHAIN}"
|
||
|
|
),
|
||
|
|
Self::SchemaVersion { found, expected } => write!(
|
||
|
|
f,
|
||
|
|
"store schema version {found} is not this build's {expected}"
|
||
|
|
),
|
||
|
|
Self::Unreachable { host, port, source } => {
|
||
|
|
write!(f, "cannot reach {host}:{port}: {source}")
|
||
|
|
}
|
||
|
|
Self::Storage(e) => write!(f, "storage error: {e}"),
|
||
|
|
Self::Noise(e) => write!(f, "noise error: {e}"),
|
||
|
|
Self::Io(e) => write!(f, "{e}"),
|
||
|
|
Self::Other(msg) => write!(f, "{msg}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl std::error::Error for Error {
|
||
|
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||
|
|
match self {
|
||
|
|
Self::Unreachable { source, .. } => Some(source),
|
||
|
|
Self::Storage(e) => Some(e),
|
||
|
|
Self::Noise(e) => Some(e),
|
||
|
|
Self::Io(e) => Some(e),
|
||
|
|
_ => None,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<rusqlite::Error> for Error {
|
||
|
|
fn from(e: rusqlite::Error) -> Self {
|
||
|
|
Error::Storage(e)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<std::io::Error> for Error {
|
||
|
|
fn from(e: std::io::Error) -> Self {
|
||
|
|
Error::Io(e)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<snow::Error> for Error {
|
||
|
|
fn from(e: snow::Error) -> Self {
|
||
|
|
Error::Noise(e)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Status codes from SPEC.md sec 12. Unassigned numbers map to None, so a
|
||
|
|
/// caller cannot accidentally name one that does not exist.
|
||
|
|
pub fn status_name(status: u8) -> Option<&'static str> {
|
||
|
|
Some(match status {
|
||
|
|
0 => "ok",
|
||
|
|
1 => "malformed",
|
||
|
|
2 => "bad version",
|
||
|
|
3 => "unknown user",
|
||
|
|
4 => "auth required",
|
||
|
|
5 => "auth failed",
|
||
|
|
6 => "quota exceeded",
|
||
|
|
7 => "too large",
|
||
|
|
8 => "rate limited",
|
||
|
|
9 => "not permitted",
|
||
|
|
10 => "internal error",
|
||
|
|
_ => return None,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Turns a non-OK response into an error carrying the status code. The
|
||
|
|
/// optional reason string in the response body is never parsed (SPEC.md sec
|
||
|
|
/// 12); an unassigned code is surfaced by number and treated as a plain
|
||
|
|
/// failure.
|
||
|
|
pub fn expect_ok(status: u8, what: &str) -> Result<(), Error> {
|
||
|
|
if status == 0 {
|
||
|
|
return Ok(());
|
||
|
|
}
|
||
|
|
let what = what.to_string();
|
||
|
|
Err(match status {
|
||
|
|
1 => Error::Malformed(what),
|
||
|
|
2 => Error::BadVersion(what),
|
||
|
|
3 => Error::UnknownUser(what),
|
||
|
|
4 => Error::AuthRequired(what),
|
||
|
|
5 => Error::AuthFailed(what),
|
||
|
|
6 => Error::QuotaExceeded(what),
|
||
|
|
7 => Error::TooLarge(what),
|
||
|
|
8 => Error::RateLimited(what),
|
||
|
|
9 => Error::NotPermitted(what),
|
||
|
|
10 => Error::InternalError(what),
|
||
|
|
status => Error::UnknownStatus(status, what),
|
||
|
|
})
|
||
|
|
}
|