Skip to content

Commit

Permalink
Add Options::additional_headers and subprotocols (#27)
Browse files Browse the repository at this point in the history
Hello,

In the native app, the Websocket protocol can lead to error because it
doesn't act like the web interface.

When we are using the WebSocket API (in the browser), it add
automatically some headers - like the `Origin` header.

In fact this header is mandatory, as explained in the RFC :

- <https://datatracker.ietf.org/doc/html/rfc6455>

```text
8.   The request MUST include a header field with the name |Origin|
        [[RFC6454](https://datatracker.ietf.org/doc/html/rfc6454)] if the request is coming from a browser client.  If
        the connection is from a non-browser client, the request MAY
        include this header field if the semantics of that client match
        the use-case described here for browser clients.  The value of
        this header field is the ASCII serialization of origin of the
        context in which the code establishing the connection is
        running.  See [[RFC6454](https://datatracker.ietf.org/doc/html/rfc6454)] for the details of how this header field
        value is constructed.
```


However, if we don't set the `Origin` header in the native app, the app
"could" not act the same as in the browser


In this PR, I had the Origin header to the client.

Note that
- the PR to add a header in tungstenite is quite new :
snapview/tungstenite-rs#400
- a new version of tungstenite has not been released yet (so this PR is
kind of a draft)

Thanks

---------

Co-authored-by: Emil Ernerfeldt <[email protected]>
  • Loading branch information
Its-Just-Nans and emilk authored Oct 10, 2024
1 parent f396ba0 commit 50c510e
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 24 deletions.
19 changes: 17 additions & 2 deletions ewebsock/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ pub type Result<T> = std::result::Result<T, Error>;
pub(crate) type EventHandler = Box<dyn Send + Fn(WsEvent) -> ControlFlow<()>>;

/// Options for a connection.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Options {
/// The maximum size of a single incoming message frame, in bytes.
///
Expand All @@ -134,6 +134,19 @@ pub struct Options {
/// Ignored on Web.
pub max_incoming_frame_size: usize,

/// Additional Request headers.
///
/// Currently only supported on native.
pub additional_headers: Vec<(String, String)>,

/// Additional subprotocols.
///
/// <https://www.iana.org/assignments/websocket/websocket.xml>
/// <https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#miscellaneous>
///
/// Currently only supported on native.
pub subprotocols: Vec<String>,

/// Delay blocking in ms - default 10ms
pub delay_blocking: std::time::Duration,
}
Expand All @@ -142,7 +155,9 @@ impl Default for Options {
fn default() -> Self {
Self {
max_incoming_frame_size: 64 * 1024 * 1024,
delay_blocking: std::time::Duration::from_millis(10),
additional_headers: vec![],
subprotocols: vec![],
delay_blocking: std::time::Duration::from_millis(10), // default value 10ms,
}
}
}
Expand Down
57 changes: 39 additions & 18 deletions ewebsock/src/native_tungstenite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::{
sync::mpsc::{Receiver, TryRecvError},
};

use crate::tungstenite_common::into_requester;
use crate::{EventHandler, Options, Result, WsEvent, WsMessage};

/// This is how you send [`WsMessage`]s to the server.
Expand Down Expand Up @@ -64,21 +65,27 @@ pub(crate) fn ws_receive_impl(url: String, options: Options, on_event: EventHand

/// Connect and call the given event handler on each received event.
///
/// Blocking version of [`ws_receive`], only available on native.
/// Blocking version of [`crate::ws_receive`], only available on native.
///
/// # Errors
/// All errors are returned to the caller, and NOT reported via `on_event`.
pub fn ws_receiver_blocking(url: &str, options: Options, on_event: &EventHandler) -> Result<()> {
let config = tungstenite::protocol::WebSocketConfig::from(options);
let uri: tungstenite::http::Uri = url
.parse()
.map_err(|err| format!("Failed to parse URL {url:?}: {err}"))?;
let config = tungstenite::protocol::WebSocketConfig::from(options.clone());
let max_redirects = 3; // tungstenite default

let (mut socket, response) =
match tungstenite::client::connect_with_config(url, Some(config), max_redirects) {
Ok(result) => result,
Err(err) => {
return Err(format!("Connect: {err}"));
}
};
let (mut socket, response) = match tungstenite::client::connect_with_config(
into_requester(uri, options),
Some(config),
max_redirects,
) {
Ok(result) => result,
Err(err) => {
return Err(format!("Connect: {err}"));
}
};

log::debug!("WebSocket HTTP response code: {}", response.status());
log::trace!(
Expand Down Expand Up @@ -166,15 +173,21 @@ pub fn ws_connect_blocking(
rx: &Receiver<WsMessage>,
) -> Result<()> {
let delay = options.delay_blocking;
let config = tungstenite::protocol::WebSocketConfig::from(options);
let config = tungstenite::protocol::WebSocketConfig::from(options.clone());
let max_redirects = 3; // tungstenite default
let (mut socket, response) =
match tungstenite::client::connect_with_config(url, Some(config), max_redirects) {
Ok(result) => result,
Err(err) => {
return Err(format!("Connect: {err}"));
}
};
let uri: tungstenite::http::Uri = url
.parse()
.map_err(|err| format!("Failed to parse URL {url:?}: {err}"))?;
let (mut socket, response) = match tungstenite::client::connect_with_config(
into_requester(uri, options),
Some(config),
max_redirects,
) {
Ok(result) => result,
Err(err) => {
return Err(format!("Connect: {err}"));
}
};

log::debug!("WebSocket HTTP response code: {}", response.status());
log::trace!(
Expand Down Expand Up @@ -217,7 +230,7 @@ pub fn ws_connect_blocking(
WsMessage::Pong(data) => tungstenite::protocol::Message::Pong(data),
WsMessage::Unknown(_) => panic!("You cannot send WsMessage::Unknown"),
};
if let Err(err) = socket.write(outgoing_message) {
if let Err(err) = socket.send(outgoing_message) {
socket.close(None).ok();
socket.flush().ok();
return Err(format!("send: {err}"));
Expand Down Expand Up @@ -278,3 +291,11 @@ pub fn ws_connect_blocking(
}
}
}

#[test]
fn test_connect() {
let options = crate::Options::default();
// see documentation for more options
let (mut sender, _receiver) = crate::connect("ws://example.com", options).unwrap();
sender.send(crate::WsMessage::Text("Hello!".into()));
}
30 changes: 27 additions & 3 deletions ewebsock/src/native_tungstenite_tokio.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::ops::ControlFlow;

use crate::tungstenite_common::into_requester;
use crate::{EventHandler, Options, Result, WsEvent, WsMessage};

/// This is how you send [`WsMessage`]s to the server.
Expand Down Expand Up @@ -49,11 +50,19 @@ async fn ws_connect_async(
on_event: EventHandler,
) {
use futures::StreamExt as _;

let config = tungstenite::protocol::WebSocketConfig::from(options);
let uri: tungstenite::http::Uri = match url.parse() {
Ok(uri) => uri,
Err(err) => {
on_event(WsEvent::Error(format!(
"Failed to parse URL {url:?}: {err}"
)));
return;
}
};
let config = tungstenite::protocol::WebSocketConfig::from(options.clone());
let disable_nagle = false; // God damn everyone who adds negations to the names of their variables
let (ws_stream, _response) = match tokio_tungstenite::connect_async_with_config(
url,
into_requester(uri, options),
Some(config),
disable_nagle,
)
Expand Down Expand Up @@ -146,3 +155,18 @@ fn ws_connect_native(url: String, options: Options, on_event: EventHandler) -> W
pub(crate) fn ws_receive_impl(url: String, options: Options, on_event: EventHandler) -> Result<()> {
ws_connect_impl(url, options, on_event).map(|sender| sender.forget())
}

#[cfg(feature = "tokio")]
#[test]
fn test_connect_tokio() {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let options = crate::Options::default();
// see documentation for more options
let (mut sender, _receiver) = crate::connect("ws://example.com", options).unwrap();
sender.send(crate::WsMessage::Text("Hello!".into()));
});
}
15 changes: 15 additions & 0 deletions ewebsock/src/tungstenite_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,18 @@ impl From<crate::Options> for tungstenite::protocol::WebSocketConfig {
}
}
}

/// transform uri and options into a request builder
pub fn into_requester(
uri: tungstenite::http::Uri,
options: crate::Options,
) -> tungstenite::client::ClientRequestBuilder {
let mut client_request = tungstenite::client::ClientRequestBuilder::new(uri);
for (key, value) in options.additional_headers {
client_request = client_request.with_header(key, value);
}
for subprotocol in options.subprotocols {
client_request = client_request.with_sub_protocol(subprotocol);
}
client_request
}
2 changes: 1 addition & 1 deletion example_app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<head>
<!-- change this to your project name -->
<title>example_app websocket template</title>
<title>ewebsock demo</title>

<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
<link data-trunk rel="rust" data-wasm-opt="2" />
Expand Down

0 comments on commit 50c510e

Please sign in to comment.