moirai_http/websocket/
close.rs1use std::fmt;
4use std::io;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct WebSocketPeerClosed {
13 code: Option<u16>,
14 reason: String,
15}
16
17impl WebSocketPeerClosed {
18 pub const fn code(&self) -> Option<u16> {
20 self.code
21 }
22
23 pub fn reason(&self) -> &str {
25 &self.reason
26 }
27}
28
29impl fmt::Display for WebSocketPeerClosed {
30 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31 formatter.write_str("WebSocket peer closed the connection")
33 }
34}
35
36impl std::error::Error for WebSocketPeerClosed {}
37
38pub(super) struct ClosePayload<'a> {
41 code: Option<u16>,
42 reason: &'a str,
43}
44
45impl From<ClosePayload<'_>> for WebSocketPeerClosed {
46 fn from(payload: ClosePayload<'_>) -> Self {
47 Self {
48 code: payload.code,
49 reason: payload.reason.to_owned(),
51 }
52 }
53}
54
55pub(super) fn parse_close_payload(payload: &[u8]) -> io::Result<ClosePayload<'_>> {
56 match payload {
57 [] => Ok(ClosePayload {
58 code: None,
59 reason: "",
60 }),
61 [_] => Err(protocol_error("WebSocket close payload has one byte")),
62 [high, low, reason @ ..] => {
63 let code = u16::from_be_bytes([*high, *low]);
64 let valid_range = (1000..=2999).contains(&code) || (3000..=4999).contains(&code);
65 if !valid_range || matches!(code, 1004 | 1005 | 1006 | 1015) {
66 return Err(protocol_error("WebSocket close code is reserved"));
67 }
68 let reason = std::str::from_utf8(reason)
69 .map_err(|_| protocol_error("WebSocket close reason is not UTF-8"))?;
70 Ok(ClosePayload {
71 code: Some(code),
72 reason,
73 })
74 }
75 }
76}
77
78pub(super) fn protocol_error(message: &str) -> io::Error {
79 io::Error::new(io::ErrorKind::InvalidData, message)
80}