fix: fix connection errors when using custom system certificates and an http proxy. (#807)

When trying to use the Railway CLI behind an HTTP proxy that requires
a custom certificate ( such as some VPNs ), the CLI would previously
fail with a certificate error. This adds the `rustls-tls-native-roots`
feature reqwest to trust the native platform's system certificate store to
fix that.

Railway commands that require a websocket connection were also
failing to go through the proxy, because while `reqwest` automatically
sends requests through the proxy `async-tungstenite` would not. This
removes `async-tungstenite` in favor of `reqwest-websocket` which is
simpler and uses `reqwest` to properly send the initial HTTP request
through the proxy before the websocket upgrade.
This commit is contained in:
Zicklag 2026-03-12 16:01:23 +00:00 committed by GitHub
parent 6b9b52eb17
commit d41b2e4dfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 222 additions and 147 deletions

205
Cargo.lock generated
View file

@ -132,9 +132,9 @@ dependencies = [
[[package]]
name = "async-tungstenite"
version = "0.28.2"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c348fb0b6d132c596eca3dcd941df48fb597aafcb07a738ec41c004b087dc99"
checksum = "ee88b4c88ac8c9ea446ad43498955750a4bbe64c4392f21ccfe5d952865e318f"
dependencies = [
"atomic-waker",
"futures-core",
@ -143,10 +143,6 @@ dependencies = [
"futures-util",
"log",
"pin-project-lite",
"rustls-native-certs",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tungstenite",
]
@ -241,9 +237,9 @@ dependencies = [
[[package]]
name = "bytes"
version = "1.9.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cassowary"
@ -385,7 +381,7 @@ version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes 1.9.0",
"bytes 1.11.1",
"memchr",
]
@ -486,9 +482,9 @@ dependencies = [
[[package]]
name = "cpufeatures"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
@ -606,9 +602,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
@ -661,9 +657,9 @@ dependencies = [
[[package]]
name = "data-encoding"
version = "2.6.0"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
[[package]]
name = "deranged"
@ -1202,7 +1198,6 @@ dependencies = [
"serde",
"serde_json",
"thiserror 1.0.69",
"tungstenite",
]
[[package]]
@ -1251,7 +1246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c65d1899521a11810501b50b898464d133e1afc96703cff57726964cfa7baf"
dependencies = [
"byteorder",
"bytes 1.9.0",
"bytes 1.11.1",
"core_affinity",
"flate2",
"flume",
@ -1334,7 +1329,7 @@ version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes 1.9.0",
"bytes 1.11.1",
"fnv",
"itoa",
]
@ -1345,7 +1340,7 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
dependencies = [
"bytes 1.9.0",
"bytes 1.11.1",
"fnv",
"itoa",
]
@ -1356,7 +1351,7 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes 1.9.0",
"bytes 1.11.1",
"http 1.2.0",
]
@ -1366,7 +1361,7 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes 1.9.0",
"bytes 1.11.1",
"futures-util",
"http 1.2.0",
"http-body",
@ -1387,19 +1382,21 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.5.2"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
dependencies = [
"bytes 1.9.0",
"atomic-waker",
"bytes 1.11.1",
"futures-channel",
"futures-util",
"futures-core",
"http 1.2.0",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec",
"tokio",
"want",
@ -1416,6 +1413,7 @@ dependencies = [
"hyper",
"hyper-util",
"rustls",
"rustls-native-certs",
"rustls-pki-types",
"tokio",
"tokio-rustls",
@ -1425,18 +1423,19 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.10"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"bytes 1.9.0",
"bytes 1.11.1",
"futures-channel",
"futures-util",
"http 1.2.0",
"http-body",
"hyper",
"libc",
"pin-project-lite",
"socket2 0.5.8",
"socket2 0.6.1",
"tokio",
"tower-service",
"tracing",
@ -1796,9 +1795,9 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "js-sys"
version = "0.3.76"
version = "0.3.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
dependencies = [
"once_cell",
"wasm-bindgen",
@ -2298,7 +2297,7 @@ version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef"
dependencies = [
"bytes 1.9.0",
"bytes 1.11.1",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
@ -2316,9 +2315,9 @@ version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d"
dependencies = [
"bytes 1.9.0",
"bytes 1.11.1",
"getrandom 0.2.15",
"rand",
"rand 0.8.5",
"ring",
"rustc-hash",
"rustls",
@ -2366,7 +2365,6 @@ dependencies = [
"anyhow",
"arboard",
"async-trait",
"async-tungstenite",
"base64",
"box_drawing",
"chrono",
@ -2405,9 +2403,10 @@ dependencies = [
"open",
"pastey",
"pathdiff",
"rand",
"rand 0.8.5",
"ratatui",
"reqwest",
"reqwest-websocket",
"schemars",
"scopeguard",
"serde",
@ -2437,8 +2436,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
]
[[package]]
@ -2448,7 +2457,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.5",
]
[[package]]
@ -2460,6 +2479,15 @@ dependencies = [
"getrandom 0.2.15",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "ratatui"
version = "0.29.0"
@ -2537,7 +2565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
dependencies = [
"base64",
"bytes 1.9.0",
"bytes 1.11.1",
"futures-core",
"futures-util",
"http 1.2.0",
@ -2555,6 +2583,7 @@ dependencies = [
"pin-project-lite",
"quinn",
"rustls",
"rustls-native-certs",
"rustls-pemfile",
"rustls-pki-types",
"serde",
@ -2573,6 +2602,24 @@ dependencies = [
"windows-registry",
]
[[package]]
name = "reqwest-websocket"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd5f79b25f7f17a62cc9337108974431a66ae5a723ac0d9fe78ac1cce2027720"
dependencies = [
"async-tungstenite",
"bytes 1.11.1",
"futures-util",
"reqwest",
"thiserror 2.0.9",
"tokio",
"tokio-util",
"tracing",
"tungstenite",
"web-sys",
]
[[package]]
name = "ring"
version = "0.17.14"
@ -3288,7 +3335,7 @@ version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [
"bytes 1.9.0",
"bytes 1.11.1",
"libc",
"mio 1.0.3",
"parking_lot",
@ -3337,8 +3384,9 @@ version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
dependencies = [
"bytes 1.9.0",
"bytes 1.11.1",
"futures-core",
"futures-io",
"futures-sink",
"pin-project-lite",
"tokio",
@ -3378,9 +3426,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
]
[[package]]
name = "tracing-core"
version = "0.1.33"
@ -3398,29 +3458,26 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.24.0"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d"
dependencies = [
"byteorder",
"bytes 1.9.0",
"bytes 1.11.1",
"data-encoding",
"http 1.2.0",
"httparse",
"log",
"rand",
"rustls",
"rustls-pki-types",
"rand 0.9.2",
"sha1",
"thiserror 1.0.69",
"thiserror 2.0.9",
"utf-8",
]
[[package]]
name = "typenum"
version = "1.17.0"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
@ -3574,36 +3631,25 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.99"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn 2.0.93",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.49"
version = "0.4.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8"
dependencies = [
"cfg-if",
"futures-util",
"js-sys",
"once_cell",
"wasm-bindgen",
@ -3612,9 +3658,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.99"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -3622,28 +3668,31 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.99"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.93",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.99"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.76"
version = "0.3.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
dependencies = [
"js-sys",
"wasm-bindgen",

View file

@ -27,8 +27,10 @@ serde_json = "1.0.134"
serde_yaml = "0.9"
reqwest = { version = "0.12.12", default-features = false, features = [
"rustls-tls",
"rustls-tls-native-roots",
"json",
] }
reqwest-websocket = "0.5.1"
chrono = { version = "0.4.39", features = [
"serde",
"clock",
@ -63,14 +65,7 @@ futures = { version = "0.3.31", default-features = false, features = [
"executor"
] }
futures-util = "0.3"
graphql-ws-client = { version = "0.11.1", features = [
"client-graphql-client",
"tungstenite",
] }
async-tungstenite = { version = "0.28.2", features = [
"tokio-runtime",
"tokio-rustls-native-certs",
] }
graphql-ws-client = { version = "0.11.1", features = ["client-graphql-client"] }
crossterm = { version = "0.27.0", features = ["event-stream"] }
http = "0.2"
is-terminal = "0.4.13"

View file

@ -1,8 +1,8 @@
use anyhow::{Result, bail};
use async_tungstenite::WebSocketStream;
use async_tungstenite::tungstenite::Message;
use futures::SinkExt;
use futures_util::stream::StreamExt;
use indicatif::ProgressBar;
use reqwest_websocket::{Message, WebSocket};
use std::io::Write;
use tokio::sync::mpsc;
use tokio::time::{Duration, interval, timeout};
@ -14,7 +14,7 @@ use super::connection::{SSHConnectParams, establish_connection};
use super::messages::{ClientMessage, ClientPayload, DataPayload, ServerMessage};
pub struct TerminalClient {
ws_stream: WebSocketStream<async_tungstenite::tokio::ConnectStream>,
ws_stream: WebSocket,
initialized: bool,
ready: bool,
in_command_progress: bool,
@ -232,7 +232,7 @@ impl TerminalClient {
/// Sends a ping message to keep the connection alive
async fn send_ping(&mut self) -> Result<()> {
self.send_message(Message::Ping(vec![]))
self.send_message(Message::Ping(Default::default()))
.await
.map_err(|e| anyhow::anyhow!("Failed to send ping: {}", e))?;
Ok(())
@ -337,19 +337,13 @@ impl TerminalClient {
}
}
}
Message::Close(frame) => {
Message::Close { code, reason } => {
if let Some(tx) = &self.ready_tx {
let _ = tx.send(false).await;
}
if let Some(frame) = frame {
bail!(
"WebSocket closed with code {}: {}",
frame.code,
frame.reason
);
} else {
bail!("WebSocket closed unexpectedly");
}
bail!(
"WebSocket closed with code {code}: {reason}",
);
}
Message::Ping(data) => {
self.send_message(Message::Pong(data)).await?;
@ -360,9 +354,6 @@ impl TerminalClient {
Message::Binary(_) => {
writeln!(writer, "Warning: Unexpected binary message received")?;
}
Message::Frame(_) => {
writeln!(writer, "Warning: Unexpected raw frame received")?;
}
}
},
None => {
@ -435,12 +426,8 @@ impl TerminalClient {
Message::Ping(data) => {
self.send_message(Message::Pong(data)).await?;
}
Message::Close(frame) => {
if let Some(frame) = frame {
bail!("WebSocket closed with code {}: {}", frame.code, frame.reason);
} else {
bail!("WebSocket closed unexpectedly");
}
Message::Close { code, reason } => {
bail!("WebSocket closed with code {code}: {reason}");
}
// Ignore other message types
_ => {}

View file

@ -1,9 +1,6 @@
use anyhow::{Result, bail};
use async_tungstenite::WebSocketStream;
use async_tungstenite::tungstenite::handshake::client::generate_key;
use async_tungstenite::tungstenite::http::Request;
use indicatif::ProgressBar;
use tokio::time::{Duration, sleep, timeout};
use tokio::time::{Duration, sleep};
use url::Url;
use crate::commands::ssh::{
@ -26,7 +23,7 @@ pub async fn establish_connection(
params: &SSHConnectParams,
spinner: &mut ProgressBar,
max_attempts: Option<u32>,
) -> Result<WebSocketStream<async_tungstenite::tokio::ConnectStream>> {
) -> Result<reqwest_websocket::WebSocket> {
let url = Url::parse(url)?;
let max_attempts = max_attempts.unwrap_or(SSH_MAX_CONNECT_ATTEMPTS);
@ -62,20 +59,16 @@ pub async fn attempt_connection(
url: &Url,
token: AuthKind,
params: &SSHConnectParams,
) -> Result<WebSocketStream<async_tungstenite::tokio::ConnectStream>> {
let key = generate_key();
) -> Result<reqwest_websocket::WebSocket> {
use reqwest_websocket::RequestBuilderExt;
let mut request = Request::builder()
.uri(url.as_str())
.header("Sec-WebSocket-Key", key)
.header("Upgrade", "websocket")
.header("Connection", "Upgrade")
.header("Sec-WebSocket-Version", "13")
.header("Host", url.host_str().unwrap_or(""))
let mut request = reqwest::Client::default()
.get(url.as_str())
.header("X-Source", get_user_agent())
.header("X-Railway-Project-Id", params.project_id.clone())
.header("X-Railway-Service-Id", params.service_id.clone())
.header("X-Railway-Environment-Id", params.environment_id.clone());
.header("X-Railway-Environment-Id", params.environment_id.clone())
.timeout(Duration::from_secs(SSH_CONNECTION_TIMEOUT_SECS));
if let Some(instance_id) = params.deployment_instance_id.as_ref() {
request = request.header("X-Railway-Deployment-Instance-Id", instance_id);
@ -89,16 +82,10 @@ pub async fn attempt_connection(
}
}
let request = request.body(())?;
let (ws_stream, response) = timeout(
Duration::from_secs(SSH_CONNECTION_TIMEOUT_SECS),
async_tungstenite::tokio::connect_async_with_config(request, None),
)
.await??;
let response = request.upgrade().send().await?;
if response.status().as_u16() == 101 {
Ok(ws_stream)
Ok(response.into_websocket().await?)
} else {
bail!(
"Server did not upgrade to WebSocket. Status: {}",

View file

@ -1,8 +1,11 @@
use std::time::Duration;
use crate::commands::Configs;
use anyhow::{Result, bail};
use async_tungstenite::tungstenite::{client::IntoClientRequest, http::HeaderValue};
use futures::{SinkExt, StreamExt};
use graphql_client::GraphQLQuery;
use graphql_ws_client::{Client, Subscription, graphql::StreamingOperation};
use reqwest_websocket::{RequestBuilderExt, WebSocket};
pub async fn subscribe_graphql<T: GraphQLQuery + Send + Sync + Unpin + 'static>(
variables: T::Variables,
@ -13,26 +16,80 @@ where
{
let configs = Configs::new()?;
let hostname = configs.get_host();
let mut request = format!("wss://backboard.{hostname}/graphql/v2").into_client_request()?;
let headers = request.headers_mut();
let client = reqwest::Client::default();
let mut request = client
.get(format!("wss://backboard.{hostname}/graphql/v2"))
.timeout(Duration::from_secs(1));
if let Some(token) = &Configs::get_railway_token() {
headers.insert("project-access-token", HeaderValue::from_str(token)?);
request = request.header("project-access-token", token);
} else if let Some(token) = configs.get_railway_auth_token() {
headers.insert(
"authorization",
HeaderValue::from_str(&format!("Bearer {token}"))?,
);
request = request.header("authorization", format!("Bearer {token}"));
} else {
bail!("Not authorized");
}
headers.insert(
"Sec-WebSocket-Protocol",
HeaderValue::from_str("graphql-transport-ws").unwrap(),
);
};
let (connection, _) = async_tungstenite::tokio::connect_async(request).await?;
let resp = request
.upgrade()
.protocols(["graphql-transport-ws"])
.send()
.await?;
resp.error_for_status_ref()?;
let web_socket = resp.into_websocket().await?;
Ok(Client::build(connection)
Ok(Client::build(GraphQLWebSocket(web_socket))
.subscribe(StreamingOperation::<T>::new(variables))
.await?)
}
struct GraphQLWebSocket(WebSocket);
impl graphql_ws_client::Connection for GraphQLWebSocket {
fn receive(&mut self) -> impl Future<Output = Option<graphql_ws_client::Message>> + Send {
use graphql_ws_client::Message as M2;
use reqwest_websocket::Message as M1;
async {
let message = self.0.next().await?.ok()?;
Some(match message {
M1::Text(t) => M2::Text(t),
M1::Binary(_) => None?,
M1::Ping(_) => M2::Ping,
M1::Pong(_) => M2::Pong,
M1::Close { code, reason } => M2::Close {
code: Some(code.into()),
reason: Some(reason),
},
})
}
}
fn send(
&mut self,
message: graphql_ws_client::Message,
) -> impl Future<Output = std::result::Result<(), graphql_ws_client::Error>> + Send {
use graphql_ws_client::{Error as E2, Message as M2};
use reqwest_websocket::{Error as E1, Message as M1};
async {
let message = match message {
M2::Text(t) => M1::Text(t),
M2::Close { code, reason } => M1::Close {
code: code.unwrap_or(0).into(),
reason: reason.unwrap_or_default(),
},
M2::Ping => M1::Ping(Default::default()),
M2::Pong => M1::Pong(Default::default()),
};
self.0.send(message).await.map_err(|e| match e {
E1::Handshake(handshake_error) => {
E2::Custom("Handshake Error".into(), handshake_error.to_string())
}
E1::Reqwest(error) => E2::Custom("Reqwest Error".into(), error.to_string()),
E1::Tungstenite(error) => E2::Custom("Tungstenite Error".into(), error.to_string()),
e => E2::Send(e.to_string()),
})?;
Ok(())
}
}
}