Skip to main content

tungstenite/
client.rs

1//! Methods to connect to a WebSocket as a client.
2
3use std::{
4    io::{Read, Write},
5    net::{SocketAddr, TcpStream, ToSocketAddrs},
6    result::Result as StdResult,
7};
8
9use http::{request::Parts, HeaderName, Uri};
10use log::*;
11
12use crate::{
13    handshake::client::{generate_key, Request, Response},
14    protocol::WebSocketConfig,
15    stream::MaybeTlsStream,
16};
17
18#[cfg(feature = "proxy")]
19use crate::proxy;
20use crate::{
21    error::{Error, Result, UrlError},
22    handshake::{client::ClientHandshake, HandshakeError},
23    protocol::WebSocket,
24    stream::{Mode, NoDelay},
25};
26
27/// Connect to the given WebSocket in blocking mode.
28///
29/// Uses a websocket configuration passed as an argument to the function. Calling it with `None` is
30/// equal to calling `connect()` function.
31///
32/// The URL may be either ws:// or wss://.
33/// To support wss:// URLs, you must activate the TLS feature on the crate level. Please refer to the
34/// project's [README][readme] for more information on available features.
35///
36/// When the `proxy` feature is enabled, this function honors `HTTP_PROXY`, `HTTPS_PROXY`,
37/// `ALL_PROXY`, and `NO_PROXY` (case-insensitive) for client connections.
38///
39/// This function "just works" for those who wants a simple blocking solution
40/// similar to `std::net::TcpStream`. If you want a non-blocking or other
41/// custom stream, call `client` instead.
42///
43/// This function uses `native_tls` or `rustls` to do TLS depending on the feature flags enabled. If
44/// you want to use other TLS libraries, use `client` instead. There is no need to enable any of
45/// the `*-tls` features if you don't call `connect` since it's the only function that uses them.
46///
47/// [readme]: https://github.com/snapview/tungstenite-rs/#features
48pub fn connect_with_config<Req: IntoClientRequest>(
49    request: Req,
50    config: Option<WebSocketConfig>,
51    max_redirects: u8,
52) -> Result<(WebSocket<MaybeTlsStream<TcpStream>>, Response)> {
53    fn try_client_handshake(
54        request: Request,
55        config: Option<WebSocketConfig>,
56    ) -> Result<(WebSocket<MaybeTlsStream<TcpStream>>, Response)> {
57        let uri = request.uri();
58        let mode = uri_mode(uri)?;
59
60        #[cfg(not(any(feature = "native-tls", feature = "__rustls-tls")))]
61        if let Mode::Tls = mode {
62            return Err(Error::Url(UrlError::TlsFeatureNotEnabled));
63        }
64
65        let host = request.uri().host().ok_or(Error::Url(UrlError::NoHostName))?;
66        let host = if host.starts_with('[') { &host[1..host.len() - 1] } else { host };
67        let port = uri.port_u16().unwrap_or(match mode {
68            Mode::Plain => 80,
69            Mode::Tls => 443,
70        });
71        let mut stream = connect_stream(request.uri(), host, port)?;
72        NoDelay::set_nodelay(&mut stream, true)?;
73
74        #[cfg(not(any(feature = "native-tls", feature = "__rustls-tls")))]
75        let client = client_with_config(request, MaybeTlsStream::Plain(stream), config);
76        #[cfg(any(feature = "native-tls", feature = "__rustls-tls"))]
77        let client = crate::tls::client_tls_with_config(request, stream, config, None);
78
79        client.map_err(|e| match e {
80            HandshakeError::Failure(f) => f,
81            HandshakeError::Interrupted(_) => panic!("Bug: blocking handshake not blocked"),
82        })
83    }
84
85    fn create_request(parts: &Parts, uri: &Uri) -> Request {
86        let mut builder =
87            Request::builder().uri(uri.clone()).method(parts.method.clone()).version(parts.version);
88        *builder.headers_mut().expect("Failed to create `Request`") = parts.headers.clone();
89        builder.body(()).expect("Failed to create `Request`")
90    }
91
92    let (parts, _) = request.into_client_request()?.into_parts();
93    let mut uri = parts.uri.clone();
94
95    for attempt in 0..=max_redirects {
96        let request = create_request(&parts, &uri);
97
98        match try_client_handshake(request, config) {
99            Err(Error::Http(res)) if res.status().is_redirection() && attempt < max_redirects => {
100                if let Some(location) = res.headers().get("Location") {
101                    uri = location.to_str()?.parse::<Uri>()?;
102                    debug!("Redirecting to {uri:?}");
103                    continue;
104                } else {
105                    warn!("No `Location` found in redirect");
106                    return Err(Error::Http(res));
107                }
108            }
109            other => return other,
110        }
111    }
112
113    unreachable!("Bug in a redirect handling logic")
114}
115
116/// Connect to the given WebSocket in blocking mode.
117///
118/// The URL may be either ws:// or wss://.
119/// To support wss:// URLs, feature `native-tls` or `rustls-tls` must be turned on.
120///
121/// This function "just works" for those who wants a simple blocking solution
122/// similar to `std::net::TcpStream`. If you want a non-blocking or other
123/// custom stream, call `client` instead.
124///
125/// This function uses `native_tls` or `rustls` to do TLS depending on the feature flags enabled. If
126/// you want to use other TLS libraries, use `client` instead. There is no need to enable any of
127/// the `*-tls` features if you don't call `connect` since it's the only function that uses them.
128pub fn connect<Req: IntoClientRequest>(
129    request: Req,
130) -> Result<(WebSocket<MaybeTlsStream<TcpStream>>, Response)> {
131    connect_with_config(request, None, 3)
132}
133
134fn connect_to_some(addrs: &[SocketAddr], uri: &Uri) -> Result<TcpStream> {
135    for addr in addrs {
136        debug!("Trying to contact {uri} at {addr}...");
137        if let Ok(stream) = TcpStream::connect(addr) {
138            return Ok(stream);
139        }
140    }
141    Err(Error::Url(UrlError::UnableToConnect(uri.to_string())))
142}
143
144fn connect_stream(uri: &Uri, host: &str, port: u16) -> Result<TcpStream> {
145    #[cfg(feature = "proxy")]
146    if let Some(stream) = proxy::connect_proxy_stream(uri, host, port)? {
147        return Ok(stream);
148    }
149
150    let addrs = (host, port).to_socket_addrs()?;
151    connect_to_some(addrs.as_slice(), uri)
152}
153
154/// Get the mode of the given URL.
155///
156/// This function may be used to ease the creation of custom TLS streams
157/// in non-blocking algorithms or for use with TLS libraries other than `native_tls` or `rustls`.
158pub fn uri_mode(uri: &Uri) -> Result<Mode> {
159    match uri.scheme_str() {
160        Some("ws") => Ok(Mode::Plain),
161        Some("wss") => Ok(Mode::Tls),
162        _ => Err(Error::Url(UrlError::UnsupportedUrlScheme)),
163    }
164}
165
166/// Do the client handshake over the given stream given a web socket configuration. Passing `None`
167/// as configuration is equal to calling `client()` function.
168///
169/// Use this function if you need a nonblocking handshake support or if you
170/// want to use a custom stream like `mio::net::TcpStream` or `openssl::ssl::SslStream`.
171/// Any stream supporting `Read + Write` will do.
172pub fn client_with_config<Stream, Req>(
173    request: Req,
174    stream: Stream,
175    config: Option<WebSocketConfig>,
176) -> StdResult<(WebSocket<Stream>, Response), HandshakeError<ClientHandshake<Stream>>>
177where
178    Stream: Read + Write,
179    Req: IntoClientRequest,
180{
181    ClientHandshake::start(stream, request.into_client_request()?, config)?.handshake()
182}
183
184/// Do the client handshake over the given stream.
185///
186/// Use this function if you need a nonblocking handshake support or if you
187/// want to use a custom stream like `mio::net::TcpStream` or `openssl::ssl::SslStream`.
188/// Any stream supporting `Read + Write` will do.
189pub fn client<Stream, Req>(
190    request: Req,
191    stream: Stream,
192) -> StdResult<(WebSocket<Stream>, Response), HandshakeError<ClientHandshake<Stream>>>
193where
194    Stream: Read + Write,
195    Req: IntoClientRequest,
196{
197    client_with_config(request, stream, None)
198}
199
200/// Trait for converting various types into HTTP requests used for a client connection.
201///
202/// This trait is implemented by default for string slices, strings, `http::Uri` and
203/// `http::Request<()>`. Note that the implementation for `http::Request<()>` is trivial and will
204/// simply take your request and pass it as is further without altering any headers or URLs, so
205/// be aware of this. If you just want to connect to the endpoint with a certain URL, better pass
206/// a regular string containing the URL in which case `tungstenite-rs` will take care for generating
207/// the proper `http::Request<()>` for you.
208pub trait IntoClientRequest {
209    /// Convert into a `Request` that can be used for a client connection.
210    fn into_client_request(self) -> Result<Request>;
211}
212
213impl IntoClientRequest for &str {
214    fn into_client_request(self) -> Result<Request> {
215        self.parse::<Uri>()?.into_client_request()
216    }
217}
218
219impl IntoClientRequest for &String {
220    fn into_client_request(self) -> Result<Request> {
221        <&str as IntoClientRequest>::into_client_request(self)
222    }
223}
224
225impl IntoClientRequest for String {
226    fn into_client_request(self) -> Result<Request> {
227        <&str as IntoClientRequest>::into_client_request(&self)
228    }
229}
230
231impl IntoClientRequest for &Uri {
232    fn into_client_request(self) -> Result<Request> {
233        self.clone().into_client_request()
234    }
235}
236
237impl IntoClientRequest for Uri {
238    fn into_client_request(self) -> Result<Request> {
239        let authority = self.authority().ok_or(Error::Url(UrlError::NoHostName))?.as_str();
240        let host = authority
241            .find('@')
242            .map(|idx| authority.split_at(idx + 1).1)
243            .unwrap_or_else(|| authority);
244
245        if host.is_empty() {
246            return Err(Error::Url(UrlError::EmptyHostName));
247        }
248
249        let req = Request::builder()
250            .method("GET")
251            .header("Host", host)
252            .header("Connection", "Upgrade")
253            .header("Upgrade", "websocket")
254            .header("Sec-WebSocket-Version", "13")
255            .header("Sec-WebSocket-Key", generate_key())
256            .uri(self)
257            .body(())?;
258        Ok(req)
259    }
260}
261
262#[cfg(feature = "url")]
263impl IntoClientRequest for &url::Url {
264    fn into_client_request(self) -> Result<Request> {
265        self.as_str().into_client_request()
266    }
267}
268
269#[cfg(feature = "url")]
270impl IntoClientRequest for url::Url {
271    fn into_client_request(self) -> Result<Request> {
272        self.as_str().into_client_request()
273    }
274}
275
276impl IntoClientRequest for Request {
277    fn into_client_request(self) -> Result<Request> {
278        Ok(self)
279    }
280}
281
282impl IntoClientRequest for httparse::Request<'_, '_> {
283    fn into_client_request(self) -> Result<Request> {
284        use crate::handshake::headers::FromHttparse;
285        Request::from_httparse(self)
286    }
287}
288
289/// Builder for a custom [`IntoClientRequest`] with options to add
290/// custom additional headers and sub protocols.
291///
292/// # Example
293///
294/// ```rust no_run
295/// # use crate::*;
296/// use http::Uri;
297/// use tungstenite::{connect, ClientRequestBuilder};
298///
299/// let uri: Uri = "ws://localhost:3012/socket".parse().unwrap();
300/// let token = "my_jwt_token";
301/// let builder = ClientRequestBuilder::new(uri)
302///     .with_header("Authorization", format!("Bearer {token}"))
303///     .with_sub_protocol("my_sub_protocol");
304/// let socket = connect(builder).unwrap();
305/// ```
306#[derive(Debug, Clone)]
307pub struct ClientRequestBuilder {
308    uri: Uri,
309    /// Additional [`Request`] handshake headers
310    additional_headers: Vec<(String, String)>,
311    /// Handsake subprotocols
312    subprotocols: Vec<String>,
313}
314
315impl ClientRequestBuilder {
316    /// Initializes an empty request builder
317    #[must_use]
318    pub const fn new(uri: Uri) -> Self {
319        Self { uri, additional_headers: Vec::new(), subprotocols: Vec::new() }
320    }
321
322    /// Adds (`key`, `value`) as an additional header to the handshake request
323    pub fn with_header<K, V>(mut self, key: K, value: V) -> Self
324    where
325        K: Into<String>,
326        V: Into<String>,
327    {
328        self.additional_headers.push((key.into(), value.into()));
329        self
330    }
331
332    /// Adds `protocol` to the handshake request subprotocols (`Sec-WebSocket-Protocol`)
333    pub fn with_sub_protocol<P>(mut self, protocol: P) -> Self
334    where
335        P: Into<String>,
336    {
337        self.subprotocols.push(protocol.into());
338        self
339    }
340}
341
342impl IntoClientRequest for ClientRequestBuilder {
343    fn into_client_request(self) -> Result<Request> {
344        let mut request = self.uri.into_client_request()?;
345        let headers = request.headers_mut();
346        for (k, v) in self.additional_headers {
347            let key = HeaderName::try_from(k)?;
348            let value = v.parse()?;
349            headers.append(key, value);
350        }
351        if !self.subprotocols.is_empty() {
352            let protocols = self.subprotocols.join(", ").parse()?;
353            headers.append("Sec-WebSocket-Protocol", protocols);
354        }
355        Ok(request)
356    }
357}