1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
//! Support for client-side WebSockets
use crate::{Conn, WebSocketConfig, WebSocketConn};
use std::{
borrow::Cow,
error::Error,
fmt::{self, Display},
ops::{Deref, DerefMut},
};
use trillium_http::{
KnownHeaderName::{
Connection, SecWebsocketAccept, SecWebsocketKey, SecWebsocketVersion,
Upgrade as UpgradeHeader,
},
Method, Status, Upgrade, Version,
};
pub use trillium_websockets::Message;
use trillium_websockets::{Role, websocket_accept_hash, websocket_key};
impl Conn {
fn set_websocket_upgrade_headers_h1(&mut self) {
let headers = self.request_headers_mut();
headers.try_insert(UpgradeHeader, "websocket");
headers.try_insert(Connection, "upgrade");
headers.try_insert(SecWebsocketVersion, "13");
headers.try_insert(SecWebsocketKey, websocket_key());
}
/// Attempt to transform this `Conn` into a [`WebSocketConn`].
///
/// This is an *execution* method: calling it on a conn that has already been awaited
/// returns [`ErrorKind::AlreadyExecuted`]. Build the conn, then call this — don't await
/// it yourself first.
///
/// Protocol selection follows the conn's [`http_version`][Conn::http_version] hint:
/// `Http2` uses the extended-CONNECT bootstrap (RFC 8441); the default uses an h1
/// `Upgrade` handshake (RFC 6455). If the peer is h2 but doesn't advertise
/// `SETTINGS_ENABLE_CONNECT_PROTOCOL`, the upgrade hard-errors — there is no silent
/// fallback to h1 from a non-capable h2 peer.
///
/// HTTP/3 (RFC 9220) extended CONNECT is not yet supported on the client. The h3 transport
/// would need to wrap post-upgrade bytes in h3 DATA frames on both client and server before
/// the byte channel will round-trip, and that framing layer doesn't exist yet. A `Http3`
/// hint here surfaces as `ErrorKind::ExtendedConnectUnsupported`.
pub async fn into_websocket(self) -> Result<WebSocketConn, WebSocketUpgradeError> {
self.into_websocket_with_config(WebSocketConfig::default())
.await
}
/// Like [`Conn::into_websocket`] but with a caller-supplied [`WebSocketConfig`].
pub async fn into_websocket_with_config(
self,
config: WebSocketConfig,
) -> Result<WebSocketConn, WebSocketUpgradeError> {
if self.status().is_some() {
return Err(WebSocketUpgradeError::new(self, ErrorKind::AlreadyExecuted));
}
match self.http_version() {
Version::Http2 => self.into_websocket_extended_connect(config).await,
Version::Http3 => Err(WebSocketUpgradeError::new(
self,
ErrorKind::ExtendedConnectUnsupported,
)),
_ => self.into_websocket_h1(config).await,
}
}
async fn into_websocket_h1(
mut self,
config: WebSocketConfig,
) -> Result<WebSocketConn, WebSocketUpgradeError> {
self.set_websocket_upgrade_headers_h1();
if let Err(e) = (&mut self).await {
return Err(WebSocketUpgradeError::new(self, e.into()));
}
let status = self.status().expect("Response did not include status");
if status != Status::SwitchingProtocols {
return Err(WebSocketUpgradeError::new(self, ErrorKind::Status(status)));
}
let key = self
.request_headers()
.get_str(SecWebsocketKey)
.expect("Request did not include Sec-WebSocket-Key");
let accept_key = websocket_accept_hash(key);
if self.response_headers().get_str(SecWebsocketAccept) != Some(&accept_key) {
return Err(WebSocketUpgradeError::new(self, ErrorKind::InvalidAccept));
}
let peer_ip = self.peer_addr().map(|addr| addr.ip());
let mut conn = WebSocketConn::new(Upgrade::from(self), Some(config), Role::Client).await;
conn.set_peer_ip(peer_ip);
Ok(conn)
}
async fn into_websocket_extended_connect(
mut self,
config: WebSocketConfig,
) -> Result<WebSocketConn, WebSocketUpgradeError> {
// RFC 8441 §4 / RFC 9220 §3: the upgrade carries `Sec-WebSocket-Version: 13` and the
// optional `Sec-WebSocket-Protocol`, but skips the `Sec-WebSocket-Key` /
// `Sec-WebSocket-Accept` SHA1 dance — those are h1-only artifacts. The
// `Connection: upgrade` / `Upgrade: websocket` headers are likewise h1-only and would
// be stripped by `finalize_headers_h2` / `_h3` even if we set them.
self.request_headers_mut()
.try_insert(SecWebsocketVersion, "13");
self.set_method(Method::Connect);
self.protocol = Some(Cow::Borrowed("websocket"));
// The peer-capability gate (RFC 8441 §3 — server must have advertised
// `SETTINGS_ENABLE_CONNECT_PROTOCOL` before the client may send a `:protocol`
// HEADERS) lives inside the h2 client send path, where it can park on the peer's
// first SETTINGS *before* putting any HEADERS on the wire. A "not supported"
// outcome surfaces here as `Error::ExtendedConnectUnsupported`.
if let Err(e) = (&mut self).await {
let kind = match e {
trillium_http::Error::ExtendedConnectUnsupported => {
ErrorKind::ExtendedConnectUnsupported
}
other => other.into(),
};
return Err(WebSocketUpgradeError::new(self, kind));
}
let status = self.status().expect("Response did not include status");
if status != Status::Ok {
return Err(WebSocketUpgradeError::new(self, ErrorKind::Status(status)));
}
let peer_ip = self.peer_addr().map(|addr| addr.ip());
let mut conn = WebSocketConn::new(Upgrade::from(self), Some(config), Role::Client).await;
conn.set_peer_ip(peer_ip);
Ok(conn)
}
}
/// The kind of error that occurred when attempting a websocket upgrade
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum ErrorKind {
/// an HTTP error attempting to make the request
#[error(transparent)]
Http(#[from] trillium_http::Error),
/// Response didn't have the expected status (101 Switching Protocols for h1, 200 OK for
/// h2/h3 extended CONNECT).
#[error("Unexpected response status {0} for websocket upgrade")]
Status(Status),
/// Response Sec-WebSocket-Accept was missing or invalid; generally a server bug
#[error("Response Sec-WebSocket-Accept was missing or invalid")]
InvalidAccept,
/// `into_websocket` was called on a `Conn` that had already been executed (its status is
/// already set). The websocket upgrade *is* the execution; build the conn and call
/// `into_websocket` directly without awaiting first.
#[error(
"Conn::into_websocket called after execution — build the conn and await into_websocket \
instead of awaiting the conn separately"
)]
AlreadyExecuted,
/// h2 peer did not advertise `SETTINGS_ENABLE_CONNECT_PROTOCOL = 1`, so the extended-CONNECT
/// bootstrap (RFC 8441) is not available on this connection.
///
/// Also surfaced when the conn was hinted as `Version::Http3`: client-side WebSocket-over-h3
/// (RFC 9220) requires h3 DATA-frame wrapping for the post-upgrade byte channel and that
/// framing layer doesn't exist yet.
#[error("peer does not support extended CONNECT, or h3 client websocket framing is missing")]
ExtendedConnectUnsupported,
}
/// An attempted upgrade to a WebSocket failed.
///
/// You can transform this back into the Conn with [`From::from`]/[`Into::into`], if you need to
/// look at the server response.
#[derive(Debug)]
pub struct WebSocketUpgradeError {
/// The kind of error that occurred
pub kind: ErrorKind,
conn: Box<Conn>,
}
impl WebSocketUpgradeError {
fn new(conn: Conn, kind: ErrorKind) -> Self {
let conn = Box::new(conn);
Self { conn, kind }
}
}
impl From<WebSocketUpgradeError> for Conn {
fn from(value: WebSocketUpgradeError) -> Self {
*value.conn
}
}
impl Deref for WebSocketUpgradeError {
type Target = Conn;
fn deref(&self) -> &Self::Target {
&self.conn
}
}
impl DerefMut for WebSocketUpgradeError {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.conn
}
}
impl Error for WebSocketUpgradeError {}
impl Display for WebSocketUpgradeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.kind.fmt(f)
}
}