Skip to content

Commit

Permalink
Merge pull request snapview#168 from WiredSound/master
Browse files Browse the repository at this point in the history
Create specific error types for protocol, URL, and capacity errors
  • Loading branch information
strohel committed Jan 11, 2021
2 parents 36f617a + 79dcf9f commit 5586d0a
Show file tree
Hide file tree
Showing 14 changed files with 240 additions and 141 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,8 @@
# 0.13.0

- Add `CapacityError`, `UrlError`, and `ProtocolError` types to represent the different types of capacity, URL, and protocol errors respectively.
- Modify variants `Error::Capacity`, `Error::Url`, and `Error::Protocol` to hold the above errors types instead of string error messages.

# 0.12.0

- Add facilities to allow clients to follow HTTP 3XX redirects.
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Expand Up @@ -9,7 +9,7 @@ readme = "README.md"
homepage = "https://github.com/snapview/tungstenite-rs"
documentation = "https://docs.rs/tungstenite/0.12.0"
repository = "https://github.com/snapview/tungstenite-rs"
version = "0.12.0"
version = "0.13.0"
edition = "2018"

[features]
Expand All @@ -29,6 +29,7 @@ rand = "0.8.0"
sha-1 = "0.9"
url = "2.1.0"
utf-8 = "0.7.5"
thiserror = "1.0.23"

[dependencies.native-tls]
optional = true
Expand Down
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -15,7 +15,7 @@ fn main () {
let mut websocket = accept(stream.unwrap()).unwrap();
loop {
let msg = websocket.read_message().unwrap();

// We do not want to send back ping/pong messages.
if msg.is_binary() || msg.is_text() {
websocket.write_message(msg).unwrap();
Expand Down Expand Up @@ -62,7 +62,7 @@ Testing
-------

Tungstenite is thoroughly tested and passes the [Autobahn Test Suite](https://crossbar.io/autobahn/) for
WebSockets. It is also covered by internal unit tests as good as possible.
WebSockets. It is also covered by internal unit tests as well as possible.

Contributing
------------
Expand Down
15 changes: 7 additions & 8 deletions src/client.rs
Expand Up @@ -52,7 +52,7 @@ mod encryption {
use std::net::TcpStream;

use crate::{
error::{Error, Result},
error::{Error, Result, UrlError},
stream::Mode,
};

Expand All @@ -62,7 +62,7 @@ mod encryption {
pub fn wrap_stream(stream: TcpStream, _domain: &str, mode: Mode) -> Result<AutoStream> {
match mode {
Mode::Plain => Ok(stream),
Mode::Tls => Err(Error::Url("TLS support not compiled in.".into())),
Mode::Tls => Err(Error::Url(UrlError::TlsFeatureNotEnabled)),
}
}
}
Expand All @@ -71,7 +71,7 @@ use self::encryption::wrap_stream;
pub use self::encryption::AutoStream;

use crate::{
error::{Error, Result},
error::{Error, Result, UrlError},
handshake::{client::ClientHandshake, HandshakeError},
protocol::WebSocket,
stream::{Mode, NoDelay},
Expand Down Expand Up @@ -103,8 +103,7 @@ pub fn connect_with_config<Req: IntoClientRequest>(
) -> Result<(WebSocket<AutoStream>, Response)> {
let uri = request.uri();
let mode = uri_mode(uri)?;
let host =
request.uri().host().ok_or_else(|| Error::Url("No host name in the URL".into()))?;
let host = request.uri().host().ok_or(Error::Url(UrlError::NoHostName))?;
let port = uri.port_u16().unwrap_or(match mode {
Mode::Plain => 80,
Mode::Tls => 443,
Expand Down Expand Up @@ -166,7 +165,7 @@ pub fn connect<Req: IntoClientRequest>(request: Req) -> Result<(WebSocket<AutoSt
}

fn connect_to_some(addrs: &[SocketAddr], uri: &Uri, mode: Mode) -> Result<AutoStream> {
let domain = uri.host().ok_or_else(|| Error::Url("No host name in the URL".into()))?;
let domain = uri.host().ok_or(Error::Url(UrlError::NoHostName))?;
for addr in addrs {
debug!("Trying to contact {} at {}...", uri, addr);
if let Ok(raw_stream) = TcpStream::connect(addr) {
Expand All @@ -175,7 +174,7 @@ fn connect_to_some(addrs: &[SocketAddr], uri: &Uri, mode: Mode) -> Result<AutoSt
}
}
}
Err(Error::Url(format!("Unable to connect to {}", uri).into()))
Err(Error::Url(UrlError::UnableToConnect(uri.to_string())))
}

/// Get the mode of the given URL.
Expand All @@ -186,7 +185,7 @@ pub fn uri_mode(uri: &Uri) -> Result<Mode> {
match uri.scheme_str() {
Some("ws") => Ok(Mode::Plain),
Some("wss") => Ok(Mode::Tls),
_ => Err(Error::Url("URL scheme not supported".into())),
_ => Err(Error::Url(UrlError::UnsupportedUrlScheme)),
}
}

Expand Down
205 changes: 151 additions & 54 deletions src/error.rs
@@ -1,9 +1,10 @@
//! Error handling.

use std::{borrow::Cow, error::Error as ErrorTrait, fmt, io, result, str, string};
use std::{io, result, str, string};

use crate::protocol::Message;
use crate::protocol::{frame::coding::Data, Message};
use http::Response;
use thiserror::Error;

#[cfg(feature = "tls")]
pub mod tls {
Expand All @@ -14,8 +15,8 @@ pub mod tls {
/// Result type of all Tungstenite library calls.
pub type Result<T> = result::Result<T, Error>;

/// Possible WebSocket errors
#[derive(Debug)]
/// Possible WebSocket errors.
#[derive(Error, Debug)]
pub enum Error {
/// WebSocket connection closed normally. This informs you of the close.
/// It's not an error as such and nothing wrong happened.
Expand All @@ -28,6 +29,7 @@ pub enum Error {
///
/// Receiving this error means that the WebSocket object is not usable anymore and the
/// only meaningful action with it is dropping it.
#[error("Connection closed normally")]
ConnectionClosed,
/// Trying to work with already closed connection.
///
Expand All @@ -36,56 +38,39 @@ pub enum Error {
/// As opposed to `ConnectionClosed`, this indicates your code tries to operate on the
/// connection when it really shouldn't anymore, so this really indicates a programmer
/// error on your part.
#[error("Trying to work with closed connection")]
AlreadyClosed,
/// Input-output error. Apart from WouldBlock, these are generally errors with the
/// underlying connection and you should probably consider them fatal.
Io(io::Error),
#[error("IO error: {0}")]
Io(#[from] io::Error),
/// TLS error.
#[cfg(feature = "tls")]
/// TLS error
Tls(tls::Error),
#[error("TLS error: {0}")]
Tls(#[from] tls::Error),
/// - When reading: buffer capacity exhausted.
/// - When writing: your message is bigger than the configured max message size
/// (64MB by default).
Capacity(Cow<'static, str>),
#[error("Space limit exceeded: {0}")]
Capacity(CapacityError),
/// Protocol violation.
Protocol(Cow<'static, str>),
#[error("WebSocket protocol error: {0}")]
Protocol(ProtocolError),
/// Message send queue full.
#[error("Send queue is full")]
SendQueueFull(Message),
/// UTF coding error
/// UTF coding error.
#[error("UTF-8 encoding error")]
Utf8,
/// Invalid URL.
Url(Cow<'static, str>),
#[error("URL error: {0}")]
Url(UrlError),
/// HTTP error.
#[error("HTTP error: {}", .0.status())]
Http(Response<Option<String>>),
/// HTTP format error.
HttpFormat(http::Error),
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Error::ConnectionClosed => write!(f, "Connection closed normally"),
Error::AlreadyClosed => write!(f, "Trying to work with closed connection"),
Error::Io(ref err) => write!(f, "IO error: {}", err),
#[cfg(feature = "tls")]
Error::Tls(ref err) => write!(f, "TLS error: {}", err),
Error::Capacity(ref msg) => write!(f, "Space limit exceeded: {}", msg),
Error::Protocol(ref msg) => write!(f, "WebSocket protocol error: {}", msg),
Error::SendQueueFull(_) => write!(f, "Send queue is full"),
Error::Utf8 => write!(f, "UTF-8 encoding error"),
Error::Url(ref msg) => write!(f, "URL error: {}", msg),
Error::Http(ref code) => write!(f, "HTTP error: {}", code.status()),
Error::HttpFormat(ref err) => write!(f, "HTTP format error: {}", err),
}
}
}

impl ErrorTrait for Error {}

impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
Error::Io(err)
}
#[error("HTTP format error: {0}")]
HttpFormat(#[from] http::Error),
}

impl From<str::Utf8Error> for Error {
Expand Down Expand Up @@ -130,24 +115,136 @@ impl From<http::status::InvalidStatusCode> for Error {
}
}

impl From<http::Error> for Error {
fn from(err: http::Error) -> Self {
Error::HttpFormat(err)
}
}

#[cfg(feature = "tls")]
impl From<tls::Error> for Error {
fn from(err: tls::Error) -> Self {
Error::Tls(err)
}
}

impl From<httparse::Error> for Error {
fn from(err: httparse::Error) -> Self {
match err {
httparse::Error::TooManyHeaders => Error::Capacity("Too many headers".into()),
e => Error::Protocol(e.to_string().into()),
httparse::Error::TooManyHeaders => Error::Capacity(CapacityError::TooManyHeaders),
e => Error::Protocol(ProtocolError::HttparseError(e)),
}
}
}

/// Indicates the specific type/cause of a capacity error.
#[derive(Error, Debug, PartialEq, Eq, Clone, Copy)]
pub enum CapacityError {
/// Too many headers provided (see [`httparse::Error::TooManyHeaders`]).
#[error("Too many headers")]
TooManyHeaders,
/// Received header is too long.
#[error("Header too long")]
HeaderTooLong,
/// Message is bigger than the maximum allowed size.
#[error("Message too long: {size} > {max_size}")]
MessageTooLong {
/// The size of the message.
size: usize,
/// The maximum allowed message size.
max_size: usize,
},
/// TCP buffer is full.
#[error("Incoming TCP buffer is full")]
TcpBufferFull,
}

/// Indicates the specific type/cause of a protocol error.
#[derive(Error, Debug, PartialEq, Eq, Clone, Copy)]
pub enum ProtocolError {
/// Use of the wrong HTTP method (the WebSocket protocol requires the GET method be used).
#[error("Unsupported HTTP method used - only GET is allowed")]
WrongHttpMethod,
/// Wrong HTTP version used (the WebSocket protocol requires version 1.1 or higher).
#[error("HTTP version must be 1.1 or higher")]
WrongHttpVersion,
/// Missing `Connection: upgrade` HTTP header.
#[error("No \"Connection: upgrade\" header")]
MissingConnectionUpgradeHeader,
/// Missing `Upgrade: websocket` HTTP header.
#[error("No \"Upgrade: websocket\" header")]
MissingUpgradeWebSocketHeader,
/// Missing `Sec-WebSocket-Version: 13` HTTP header.
#[error("No \"Sec-WebSocket-Version: 13\" header")]
MissingSecWebSocketVersionHeader,
/// Missing `Sec-WebSocket-Key` HTTP header.
#[error("No \"Sec-WebSocket-Key\" header")]
MissingSecWebSocketKey,
/// The `Sec-WebSocket-Accept` header is either not present or does not specify the correct key value.
#[error("Key mismatch in \"Sec-WebSocket-Accept\" header")]
SecWebSocketAcceptKeyMismatch,
/// Garbage data encountered after client request.
#[error("Junk after client request")]
JunkAfterRequest,
/// Custom responses must be unsuccessful.
#[error("Custom response must not be successful")]
CustomResponseSuccessful,
/// No more data while still performing handshake.
#[error("Handshake not finished")]
HandshakeIncomplete,
/// Wrapper around a [`httparse::Error`] value.
#[error("httparse error: {0}")]
HttparseError(#[from] httparse::Error),
/// Not allowed to send after having sent a closing frame.
#[error("Sending after closing is not allowed")]
SendAfterClosing,
/// Remote sent data after sending a closing frame.
#[error("Remote sent after having closed")]
ReceivedAfterClosing,
/// Reserved bits in frame header are non-zero.
#[error("Reserved bits are non-zero")]
NonZeroReservedBits,
/// The server must close the connection when an unmasked frame is received.
#[error("Received an unmasked frame from client")]
UnmaskedFrameFromClient,
/// The client must close the connection when a masked frame is received.
#[error("Received a masked frame from server")]
MaskedFrameFromServer,
/// Control frames must not be fragmented.
#[error("Fragmented control frame")]
FragmentedControlFrame,
/// Control frames must have a payload of 125 bytes or less.
#[error("Control frame too big (payload must be 125 bytes or less)")]
ControlFrameTooBig,
/// Type of control frame not recognised.
#[error("Unknown control frame type: {0}")]
UnknownControlFrameType(u8),
/// Type of data frame not recognised.
#[error("Unknown data frame type: {0}")]
UnknownDataFrameType(u8),
/// Received a continue frame despite there being nothing to continue.
#[error("Continue frame but nothing to continue")]
UnexpectedContinueFrame,
/// Received data while waiting for more fragments.
#[error("While waiting for more fragments received: {0}")]
ExpectedFragment(Data),
/// Connection closed without performing the closing handshake.
#[error("Connection reset without closing handshake")]
ResetWithoutClosingHandshake,
/// Encountered an invalid opcode.
#[error("Encountered invalid opcode: {0}")]
InvalidOpcode(u8),
/// The payload for the closing frame is invalid.
#[error("Invalid close sequence")]
InvalidCloseSequence,
}

/// Indicates the specific type/cause of URL error.
#[derive(Error, Debug, PartialEq, Eq)]
pub enum UrlError {
/// TLS is used despite not being compiled with the TLS feature enabled.
#[error("TLS support not compiled in")]
TlsFeatureNotEnabled,
/// The URL does not include a host name.
#[error("No host name in the URL")]
NoHostName,
/// Failed to connect with this URL.
#[error("Unable to connect to {0}")]
UnableToConnect(String),
/// Unsupported URL scheme used (only `ws://` or `wss://` may be used).
#[error("URL scheme not supported")]
UnsupportedUrlScheme,
/// The URL host name, though included, is empty.
#[error("URL contains empty host name")]
EmptyHostName,
/// The URL does not include a path/query.
#[error("No path/query in URL")]
NoPathOrQuery,
}

0 comments on commit 5586d0a

Please sign in to comment.