icewind1991 / steam-vent

Interact with the Steam network via rust
19 stars 11 forks source link

[Feature Request] Support socks5 and http proxies #26

Open johnpyp opened 3 weeks ago

johnpyp commented 3 weeks ago

Supporting proxies for the websocket connection would be very helpful for multi-user bots. node-steam-user currently supports this, and its one of the remaining features I'm missing from steam-vent.

I've prototyped this successfully by using reqwest-websocket instead of tungstenite-tokio directly, which allows easily upgrading a reqwest connection into a websocket one. Reqwest natively supports socks5/http/https proxies, so it makes the integration very easy. Here's the proof-of-concept replacement for the current transport/websocket.rs, which generally seems to work:

use crate::message::flatten_multi;
use crate::net::{NetworkError, RawNetMessage};
use crate::transport::assert_can_unsplit;
use futures_util::{Sink, SinkExt, Stream, StreamExt, TryStreamExt};
use reqwest::{Client, Proxy};
use reqwest_websocket::{Message as WsMessage, RequestBuilderExt};
use std::future::ready;
use tracing::{debug, instrument};

type Result<T, E = NetworkError> = std::result::Result<T, E>;

#[instrument]
pub async fn connect(
    addr: &str,
) -> Result<(
    impl Stream<Item = Result<RawNetMessage>>,
    impl Sink<RawNetMessage, Error = NetworkError>,
)> {
    connect_with_proxy(addr, None).await
}

#[instrument]
pub async fn connect_with_proxy(
    addr: &str,
    proxy_url: Option<String>,
) -> Result<(
    impl Stream<Item = Result<RawNetMessage>>,
    impl Sink<RawNetMessage, Error = NetworkError>,
)> {
    let mut client_builder = Client::builder();

    // Configure proxy if provided
    if let Some(proxy) = proxy_url {
        client_builder = client_builder.proxy(Proxy::all(&proxy)?);
    }

    let client = client_builder.build()?;

    // Connect to websocket using the upgrade flow
    let response = client.get(addr).upgrade().send().await?;

    let websocket = response.into_websocket().await?;

    debug!("connected to websocket server");
    let (raw_write, raw_read) = websocket.split();

    Ok((
        flatten_multi(
            raw_read
                .map_err(NetworkError::from)
                .map_ok(|msg| match msg {
                    WsMessage::Binary(data) => data,
                    _ => vec![], // Handle other message types as needed
                })
                .map_ok(|vec| vec.into_iter().collect())
                .map(|res| res.and_then(RawNetMessage::read)),
        ),
        raw_write.with(|msg: RawNetMessage| {
            let mut body = msg.header_buffer;
            assert_can_unsplit(&body, &msg.data);
            body.unsplit(msg.data);
            ready(Ok(WsMessage::Binary(body.to_vec())))
        }),
    ))
}

The other way to do this is with tokio-tungstenite connections manually, but it seems that it's significantly more work than this approach. Let me know if this current approach looks good and I can open a PR.

icewind1991 commented 3 weeks ago

Approach looks good.

Additionally, since I foresee that even with builtin proxy support there will still be people that need to further tweak the connection behavior. I implemented the ability for users to bring their own customized transport with 4f16b82