Skip to content

Commit c22dc58

Browse files
committed
feat: add GWS_API_BASE_URL for custom/mock endpoint support
When GWS_API_BASE_URL is set (e.g., http://localhost:8099), all API requests are redirected to the custom endpoint and OAuth authentication is skipped automatically. The real Discovery Document is still fetched so the CLI command tree remains fully functional — only root_url and base_url are rewritten to point at the custom endpoint. A warning is printed to stderr on first use so the redirect is never silent, mitigating the risk of malicious .env injection. Changes: - discovery.rs: add custom_api_base_url() with LazyLock caching and stderr warning; rewrite Discovery Document URLs when env var is set - executor.rs: add resolve_auth() that skips OAuth for custom endpoints - main.rs, mcp_server.rs: use resolve_auth() for consistent behavior
1 parent 99a6284 commit c22dc58

4 files changed

Lines changed: 60 additions & 10 deletions

File tree

src/discovery.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,35 @@ pub struct JsonSchemaProperty {
183183
pub additional_properties: Option<Box<JsonSchemaProperty>>,
184184
}
185185

186+
/// Cached custom API base URL, read once from the environment.
187+
/// Prints a warning on first access so the redirect is never silent.
188+
static CUSTOM_API_BASE_URL: std::sync::LazyLock<Option<String>> =
189+
std::sync::LazyLock::new(|| {
190+
let url = std::env::var("GWS_API_BASE_URL").ok().filter(|s| !s.is_empty());
191+
if let Some(ref u) = url {
192+
eprintln!("[gws] Custom API endpoint active: {u}");
193+
eprintln!("[gws] Authentication is disabled. Requests will NOT go to Google APIs.");
194+
}
195+
url
196+
});
197+
198+
/// Returns the custom API base URL override, if set.
199+
///
200+
/// When `GWS_API_BASE_URL` is set (e.g., `http://localhost:8099`), all API
201+
/// requests are directed to this endpoint instead of the real Google APIs.
202+
/// Authentication is skipped automatically. This is useful for testing against
203+
/// mock API servers.
204+
pub fn custom_api_base_url() -> Option<&'static str> {
205+
CUSTOM_API_BASE_URL.as_deref()
206+
}
207+
186208
/// Fetches and caches a Google Discovery Document.
209+
///
210+
/// The Discovery Document is always fetched from the real Google APIs so that
211+
/// gws knows the full command structure (resources, methods, parameters). When
212+
/// `GWS_API_BASE_URL` is set, the document's `root_url` and `base_url` are
213+
/// rewritten to point at the custom endpoint — actual API requests then go to
214+
/// the mock server while the CLI command tree remains fully functional.
187215
pub async fn fetch_discovery_document(
188216
service: &str,
189217
version: &str,
@@ -235,7 +263,20 @@ pub async fn fetch_discovery_document(
235263
let _ = e;
236264
}
237265

238-
let doc: RestDescription = serde_json::from_str(&body)?;
266+
let mut doc: RestDescription = serde_json::from_str(&body)?;
267+
268+
// When a custom API base URL is set, rewrite the Discovery Document's
269+
// root_url and base_url so that all HTTP requests go to the custom
270+
// endpoint (e.g., a mock server) instead of the real Google APIs.
271+
if let Some(base) = custom_api_base_url() {
272+
let base_trimmed = base.trim_end_matches('/');
273+
doc.root_url = format!("{base_trimmed}/");
274+
doc.base_url = Some(format!(
275+
"{base_trimmed}/{}/{}/",
276+
doc.name, doc.version
277+
));
278+
}
279+
239280
Ok(doc)
240281
}
241282

src/executor.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ pub enum AuthMethod {
3838
None,
3939
}
4040

41+
/// Resolve authentication, skipping OAuth when a custom API endpoint is set.
42+
///
43+
/// When `GWS_API_BASE_URL` is set, all requests go to a mock server that
44+
/// doesn't need (or support) Google OAuth. This helper centralises that check
45+
/// so every call-site doesn't need to know about the env var.
46+
pub async fn resolve_auth(scopes: &[&str]) -> (Option<String>, AuthMethod) {
47+
if crate::discovery::custom_api_base_url().is_some() {
48+
return (None, AuthMethod::None);
49+
}
50+
match crate::auth::get_token(scopes).await {
51+
Ok(t) => (Some(t), AuthMethod::OAuth),
52+
Err(_) => (None, AuthMethod::None),
53+
}
54+
}
55+
4156
/// Configuration for auto-pagination.
4257
#[derive(Debug, Clone)]
4358
pub struct PaginationConfig {

src/main.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,8 @@ async fn run() -> Result<(), GwsError> {
203203
// Get scopes from the method
204204
let scopes: Vec<&str> = method.scopes.iter().map(|s| s.as_str()).collect();
205205

206-
// Authenticate: try OAuth, otherwise proceed unauthenticated
207-
let (token, auth_method) = match auth::get_token(&scopes).await {
208-
Ok(t) => (Some(t), executor::AuthMethod::OAuth),
209-
Err(_) => (None, executor::AuthMethod::None),
210-
};
206+
// Authenticate: skips OAuth automatically when GWS_API_BASE_URL is set.
207+
let (token, auth_method) = executor::resolve_auth(&scopes).await;
211208

212209
// Execute
213210
executor::execute_method(

src/mcp_server.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -407,10 +407,7 @@ async fn handle_tools_call(params: &Value, config: &ServerConfig) -> Result<Valu
407407
};
408408

409409
let scopes: Vec<&str> = method.scopes.iter().map(|s| s.as_str()).collect();
410-
let (token, auth_method) = match crate::auth::get_token(&scopes).await {
411-
Ok(t) => (Some(t), crate::executor::AuthMethod::OAuth),
412-
Err(_) => (None, crate::executor::AuthMethod::None),
413-
};
410+
let (token, auth_method) = crate::executor::resolve_auth(&scopes).await;
414411

415412
let result = crate::executor::execute_method(
416413
&doc,

0 commit comments

Comments
 (0)