first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+29
View File
@@ -0,0 +1,29 @@
/// The response header set by GCP Identity-Aware Proxy on its generated responses.
pub const IAP_GENERATED_RESPONSE_HEADER: &str = "x-goog-iap-generated-response";
/// HTTP header used to attach the IAP bearer token to outbound requests.
pub const IAP_PROXY_AUTH_HEADER: &str = "Proxy-Authorization";
pub fn proxy_auth_header(token: &str) -> (&'static str, String) {
(IAP_PROXY_AUTH_HEADER, format!("Bearer {token}"))
}
/// Returns `true` if the given status + headers appear to be an IAP-generated
/// challenge (302, 401, or 403 with the IAP response header present). Useful
/// for detecting stale credentials and triggering a re-fetch.
pub fn is_iap_challenge(status: reqwest::StatusCode, headers: &http::HeaderMap) -> bool {
let is_challenge_status = status == reqwest::StatusCode::FOUND
|| status == reqwest::StatusCode::UNAUTHORIZED
|| status == reqwest::StatusCode::FORBIDDEN;
is_challenge_status && headers.get(IAP_GENERATED_RESPONSE_HEADER).is_some()
}
/// Source of the current IAP bearer token.
pub trait IapTokenProvider: Send + Sync {
fn cached_token(&self) -> Option<String>;
}
#[cfg(test)]
#[path = "iap_tests.rs"]
mod tests;
+42
View File
@@ -0,0 +1,42 @@
use http::{HeaderMap, HeaderValue};
use reqwest::StatusCode;
use super::*;
fn headers_with_iap() -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(
IAP_GENERATED_RESPONSE_HEADER,
HeaderValue::from_static("true"),
);
headers
}
#[test]
fn challenge_statuses_with_iap_header_are_challenges() {
for status in [
StatusCode::FOUND,
StatusCode::UNAUTHORIZED,
StatusCode::FORBIDDEN,
] {
assert!(is_iap_challenge(status, &headers_with_iap()));
}
}
#[test]
fn challenge_status_without_iap_header_is_not_a_challenge() {
assert!(!is_iap_challenge(StatusCode::FORBIDDEN, &HeaderMap::new()));
assert!(!is_iap_challenge(
StatusCode::UNAUTHORIZED,
&HeaderMap::new()
));
}
#[test]
fn non_challenge_status_with_iap_header_is_not_a_challenge() {
assert!(!is_iap_challenge(StatusCode::OK, &headers_with_iap()));
assert!(!is_iap_challenge(
StatusCode::INTERNAL_SERVER_ERROR,
&headers_with_iap()
));
}
+142 -41
View File
@@ -1,5 +1,8 @@
pub mod iap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use std::{fmt, future};
@@ -8,20 +11,20 @@ use async_compat::{Compat, CompatExt};
use async_stream::stream;
use bytes::Bytes;
use futures::{Stream, StreamExt};
use galaxy_core::{
channel::{Channel, ChannelState},
execution_mode,
operating_system_info::OperatingSystemInfo,
report_error,
};
use galaxy_core::channel::{Channel, ChannelState};
use galaxy_core::operating_system_info::OperatingSystemInfo;
use galaxy_core::{execution_mode, report_error};
use http::HeaderValue;
pub use http::header::AUTHORIZATION;
use http::header::HeaderName;
pub use http::{HeaderMap, StatusCode, header::AUTHORIZATION};
pub use http::{HeaderMap, StatusCode};
use reqwest::IntoUrl;
use reqwest_eventsource::RequestBuilderExt;
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::iap::{IapTokenProvider, proxy_auth_header};
pub mod headers {
/// Custom Warp header indicating the version of the Warp app.
pub const CLIENT_RELEASE_VERSION_HEADER_KEY: &str = "X-Warp-Client-Version";
@@ -62,6 +65,11 @@ pub struct Client {
/// A callback that is executed on after each response is received.
after_response_received: Option<ResponseHookFn>,
/// If set, provides IAP bearer tokens to attach as `Proxy-Authorization`
/// headers on outbound requests to the Warp staging server. Wired in by
/// the app layer on IAP-enabled builds (staging).
iap_token_provider: Option<Arc<dyn IapTokenProvider>>,
}
/// Type for 'hook' functions to be executed prior to sending a request. A reference to the
@@ -145,9 +153,7 @@ impl Client {
let client_builder = reqwest::ClientBuilder::new()
// Don't load any SSL/TLS certificates, as doing so can be slow and we should
// never be making real requests in tests.
.tls_built_in_native_certs(false)
.tls_built_in_root_certs(false)
.tls_built_in_webpki_certs(false)
.tls_certs_only([])
// Disable proxy usage in tests, as loading system proxy configuration can be
// slow.
.no_proxy();
@@ -159,6 +165,7 @@ impl Client {
wrapped: client,
before_request_sent: None,
after_response_received: None,
iap_token_provider: None,
})
}
@@ -170,10 +177,15 @@ impl Client {
self.after_response_received = Some(hook_fn);
}
pub fn set_iap_token_provider(&mut self, provider: Arc<dyn IapTokenProvider>) {
self.iap_token_provider = Some(provider);
}
fn builder(
&self,
wrapped: reqwest::RequestBuilder,
include_warp_headers: bool,
iap_token: Option<String>,
) -> RequestBuilder<'_> {
let mut builder = RequestBuilder {
wrapped,
@@ -186,42 +198,53 @@ impl Client {
builder = Self::add_warp_http_headers(builder);
}
if let Some(token) = iap_token {
let (name, value) = proxy_auth_header(&token);
builder = builder.header(name, value);
}
builder
}
pub fn get<U: IntoUrl + Clone>(&self, url: U) -> RequestBuilder<'_> {
self.builder(
self.wrapped.get(url.clone()),
Self::include_warp_http_headers(url),
)
let include_warp_headers = Self::include_warp_http_headers(url.clone());
let iap_token = self.iap_token_for(url.clone());
self.builder(self.wrapped.get(url), include_warp_headers, iap_token)
}
pub fn post<U: IntoUrl + Clone>(&self, url: U) -> RequestBuilder<'_> {
self.builder(
self.wrapped.post(url.clone()),
Self::include_warp_http_headers(url),
)
}
pub fn patch<U: IntoUrl + Clone>(&self, url: U) -> RequestBuilder<'_> {
self.builder(
self.wrapped.patch(url.clone()),
Self::include_warp_http_headers(url),
)
let include_warp_headers = Self::include_warp_http_headers(url.clone());
let iap_token = self.iap_token_for(url.clone());
self.builder(self.wrapped.post(url), include_warp_headers, iap_token)
}
pub fn put<U: IntoUrl + Clone>(&self, url: U) -> RequestBuilder<'_> {
self.builder(
self.wrapped.put(url.clone()),
Self::include_warp_http_headers(url),
)
let include_warp_headers = Self::include_warp_http_headers(url.clone());
let iap_token = self.iap_token_for(url.clone());
self.builder(self.wrapped.put(url), include_warp_headers, iap_token)
}
pub fn patch<U: IntoUrl + Clone>(&self, url: U) -> RequestBuilder<'_> {
let include_warp_headers = Self::include_warp_http_headers(url.clone());
let iap_token = self.iap_token_for(url.clone());
self.builder(self.wrapped.patch(url), include_warp_headers, iap_token)
}
pub fn delete<U: IntoUrl + Clone>(&self, url: U) -> RequestBuilder<'_> {
self.builder(
self.wrapped.delete(url.clone()),
Self::include_warp_http_headers(url),
)
let include_warp_headers = Self::include_warp_http_headers(url.clone());
let iap_token = self.iap_token_for(url.clone());
self.builder(self.wrapped.delete(url), include_warp_headers, iap_token)
}
/// Returns the IAP bearer token to attach to a request targeting
/// `url`, scoped to the Warp server's origin.
fn iap_token_for<U: IntoUrl>(&self, url: U) -> Option<String> {
let provider = self.iap_token_provider.as_ref()?;
let url = url.into_url().ok()?;
if !is_warp_server_origin(&url) {
return None;
}
provider.cached_token()
}
/// Helper method to determine if the request should include warp-specific headers. The only case
@@ -326,6 +349,11 @@ impl Client {
}
pub async fn execute(&self, request: Request) -> reqwest::Result<Response> {
self.execute_inner(request).await
}
/// Core request execution logic shared by all platforms.
async fn execute_inner(&self, request: Request) -> reqwest::Result<Response> {
let Request {
wrapped: request,
serialized_payload,
@@ -358,6 +386,16 @@ impl Client {
}
}
fn is_warp_server_origin(url: &reqwest::Url) -> bool {
[
ChannelState::server_root_url(),
ChannelState::rtc_http_url(),
]
.iter()
.filter_map(|candidate| reqwest::Url::parse(candidate.as_ref()).ok())
.any(|candidate| candidate.origin() == url.origin())
}
impl<'a> RequestBuilder<'a> {
pub fn build(self) -> reqwest::Result<Request> {
self.build_split().1
@@ -555,6 +593,16 @@ impl<'a> RequestBuilder<'a> {
}
}
/// Attach a `multipart/form-data` body.
/// Not available on wasm because reqwest's multipart builder API is native-only.
#[cfg(not(target_family = "wasm"))]
pub fn multipart(self, form: reqwest::multipart::Form) -> RequestBuilder<'a> {
Self {
wrapped: self.wrapped.multipart(form),
..self
}
}
/// Prevents the system from sleeping due to idle while this request is in progress.
///
/// The provided reason will be used in user-visible logging, so make sure it is
@@ -567,12 +615,14 @@ impl<'a> RequestBuilder<'a> {
}
}
/// An error returned from `Response::error_for_status` that includes response headers.
/// This allows callers to inspect headers (like X-Warp-Error-Code) when handling errors.
/// An error returned from `Response::error_for_status` that includes response metadata.
/// This allows callers to inspect headers (like X-Warp-Error-Code) and the response body when
/// handling errors.
#[derive(Debug)]
pub struct ResponseError {
pub source: reqwest::Error,
pub headers: HeaderMap,
pub headers: Box<HeaderMap>,
pub body: Option<String>,
}
impl std::fmt::Display for ResponseError {
@@ -619,7 +669,28 @@ impl Response {
let headers = self.0.headers().clone();
match self.0.error_for_status() {
Ok(response) => Ok(Self(response)),
Err(source) => Err(ResponseError { source, headers }),
Err(source) => Err(ResponseError {
source,
headers: Box::new(headers),
body: None,
}),
}
}
/// Checks the response status and returns an error if it's not successful.
/// Unlike `error_for_status`, this also reads and preserves the response body on errors.
pub async fn error_for_status_with_body(self) -> Result<Self, ResponseError> {
let headers = self.0.headers().clone();
match self.0.error_for_status_ref() {
Ok(_) => Ok(self),
Err(source) => {
let body = self.text().await.ok();
Err(ResponseError {
source,
headers: Box::new(headers),
body,
})
}
}
}
@@ -629,7 +700,11 @@ impl Response {
let headers = self.0.headers().clone();
match self.0.error_for_status_ref() {
Ok(response) => Ok(response),
Err(source) => Err(ResponseError { source, headers }),
Err(source) => Err(ResponseError {
source,
headers: Box::new(headers),
body: None,
}),
}
}
@@ -659,18 +734,20 @@ impl<'c> oauth2::AsyncHttpClient<'c> for Client {
type Future = Pin<Box<dyn Future<Output = Result<oauth2::HttpResponse, Self::Error>> + 'c>>;
#[cfg(not(target_arch = "wasm32"))]
type Future =
Pin<Box<dyn Future<Output = Result<oauth2::HttpResponse, Self::Error>> + Send + Sync + 'c>>;
Pin<Box<dyn Future<Output = Result<oauth2::HttpResponse, Self::Error>> + Send + 'c>>;
fn call(&'c self, request: oauth2::HttpRequest) -> Self::Future {
Box::pin(async move {
let include_warp_headers = Self::include_warp_http_headers(request.uri().to_string());
let uri = request.uri().to_string();
let include_warp_headers = Self::include_warp_http_headers(uri.clone());
let iap_token = self.iap_token_for(uri);
let builder = reqwest::RequestBuilder::from_parts(
self.wrapped.clone(),
request.try_into().map_err(Box::new)?,
);
let response = self
.builder(builder, include_warp_headers)
.builder(builder, include_warp_headers, iap_token)
.send()
.await
.map_err(Box::new)?;
@@ -693,3 +770,27 @@ impl<'c> oauth2::AsyncHttpClient<'c> for Client {
})
}
}
#[cfg(test)]
mod origin_tests {
use super::*;
#[test]
fn server_and_rtc_origins_match() {
// Derive the expected origins from `ChannelState` so the assertion holds
// regardless of which channel config the test build resolves to.
let server = reqwest::Url::parse(ChannelState::server_root_url().as_ref()).unwrap();
assert!(is_warp_server_origin(&server.join("/graphql/v2").unwrap()));
let rtc = reqwest::Url::parse(ChannelState::rtc_http_url().as_ref()).unwrap();
assert!(is_warp_server_origin(
&rtc.join("/api/v1/agent/events/stream").unwrap()
));
}
#[test]
fn third_party_origin_does_not_match() {
let url = reqwest::Url::parse("https://evil.example.com/graphql/v2").unwrap();
assert!(!is_warp_server_origin(&url));
}
}