diff options
| author | Michelle Tilley <michelle@michelletilley.net> | 2025-05-21 17:36:23 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-05-21 17:36:23 -0700 |
| commit | 87a963600cb6bea8f594d53f3a19920af7560169 (patch) | |
| tree | 99fcec203cfd1a405dc3528e07255352da307cee /crates | |
| parent | Formatting (diff) | |
| download | atuin-87a963600cb6bea8f594d53f3a19920af7560169.zip | |
fix(api): Allow trailing slashes in sync_address (#2760)
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/atuin-client/src/api_client.rs | 76 |
1 files changed, 50 insertions, 26 deletions
diff --git a/crates/atuin-client/src/api_client.rs b/crates/atuin-client/src/api_client.rs index 0bd16c50..c2bdfadc 100644 --- a/crates/atuin-client/src/api_client.rs +++ b/crates/atuin-client/src/api_client.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::env; use std::time::Duration; -use eyre::{Result, bail}; +use eyre::{Result, bail, eyre}; use reqwest::{ Response, StatusCode, Url, header::{AUTHORIZATION, HeaderMap, USER_AGENT}, @@ -35,6 +35,25 @@ pub struct Client<'a> { client: reqwest::Client, } +fn make_url(address: &str, path: &str) -> Result<String> { + // `join()` expects a trailing `/` in order to join paths + // e.g. it treats `http://host:port/subdir` as a file called `subdir` + let address = if address.ends_with("/") { + address + } else { + &format!("{}/", address) + }; + + // passing a path with a leading `/` will cause `join()` to replace the entire URL path + let path = path.strip_prefix("/").unwrap_or(path); + + let url = Url::parse(address) + .map(|url| url.join(path))? + .map_err(|_| eyre!("invalid address"))?; + + Ok(url.to_string()) +} + pub async fn register( address: &str, username: &str, @@ -46,14 +65,14 @@ pub async fn register( map.insert("email", email); map.insert("password", password); - let url = format!("{address}/user/{username}"); + let url = make_url(address, &format!("/user/{username}"))?; let resp = reqwest::get(url).await?; if resp.status().is_success() { bail!("username already in use"); } - let url = format!("{address}/register"); + let url = make_url(address, "/register")?; let client = reqwest::Client::new(); let resp = client .post(url) @@ -73,7 +92,7 @@ pub async fn register( } pub async fn login(address: &str, req: LoginRequest) -> Result<LoginResponse> { - let url = format!("{address}/login"); + let url = make_url(address, "/login")?; let client = reqwest::Client::new(); let resp = client @@ -197,7 +216,7 @@ impl<'a> Client<'a> { } pub async fn count(&self) -> Result<i64> { - let url = format!("{}/sync/count", self.sync_addr); + let url = make_url(self.sync_addr, "/sync/count")?; let url = Url::parse(url.as_str())?; let resp = self.client.get(url).send().await?; @@ -217,7 +236,7 @@ impl<'a> Client<'a> { } pub async fn status(&self) -> Result<StatusResponse> { - let url = format!("{}/sync/status", self.sync_addr); + let url = make_url(self.sync_addr, "/sync/status")?; let url = Url::parse(url.as_str())?; let resp = self.client.get(url).send().await?; @@ -233,7 +252,7 @@ impl<'a> Client<'a> { } pub async fn me(&self) -> Result<MeResponse> { - let url = format!("{}/api/v0/me", self.sync_addr); + let url = make_url(self.sync_addr, "/api/v0/me")?; let url = Url::parse(url.as_str())?; let resp = self.client.get(url).send().await?; @@ -252,13 +271,15 @@ impl<'a> Client<'a> { ) -> Result<SyncHistoryResponse> { let host = host.unwrap_or_else(|| hash_str(&get_host_user())); - let url = format!( - "{}/sync/history?sync_ts={}&history_ts={}&host={}", + let url = make_url( self.sync_addr, - urlencoding::encode(sync_ts.format(&Rfc3339)?.as_str()), - urlencoding::encode(history_ts.format(&Rfc3339)?.as_str()), - host, - ); + &format!( + "/sync/history?sync_ts={}&history_ts={}&host={}", + urlencoding::encode(sync_ts.format(&Rfc3339)?.as_str()), + urlencoding::encode(history_ts.format(&Rfc3339)?.as_str()), + host, + ), + )?; let resp = self.client.get(url).send().await?; let resp = handle_resp_error(resp).await?; @@ -268,7 +289,7 @@ impl<'a> Client<'a> { } pub async fn post_history(&self, history: &[AddHistoryRequest]) -> Result<()> { - let url = format!("{}/history", self.sync_addr); + let url = make_url(self.sync_addr, "/history")?; let url = Url::parse(url.as_str())?; let resp = self.client.post(url).json(history).send().await?; @@ -278,7 +299,7 @@ impl<'a> Client<'a> { } pub async fn delete_history(&self, h: History) -> Result<()> { - let url = format!("{}/history", self.sync_addr); + let url = make_url(self.sync_addr, "/history")?; let url = Url::parse(url.as_str())?; let resp = self @@ -296,7 +317,7 @@ impl<'a> Client<'a> { } pub async fn delete_store(&self) -> Result<()> { - let url = format!("{}/api/v0/store", self.sync_addr); + let url = make_url(self.sync_addr, "/api/v0/store")?; let url = Url::parse(url.as_str())?; let resp = self.client.delete(url).send().await?; @@ -307,7 +328,7 @@ impl<'a> Client<'a> { } pub async fn post_records(&self, records: &[Record<EncryptedData>]) -> Result<()> { - let url = format!("{}/api/v0/record", self.sync_addr); + let url = make_url(self.sync_addr, "/api/v0/record")?; let url = Url::parse(url.as_str())?; debug!("uploading {} records to {url}", records.len()); @@ -332,10 +353,13 @@ impl<'a> Client<'a> { start ); - let url = format!( - "{}/api/v0/record/next?host={}&tag={}&count={}&start={}", - self.sync_addr, host.0, tag, count, start - ); + let url = make_url( + self.sync_addr, + &format!( + "/api/v0/record/next?host={}&tag={}&count={}&start={}", + host.0, tag, count, start + ), + )?; let url = Url::parse(url.as_str())?; @@ -348,7 +372,7 @@ impl<'a> Client<'a> { } pub async fn record_status(&self) -> Result<RecordStatus> { - let url = format!("{}/api/v0/record", self.sync_addr); + let url = make_url(self.sync_addr, "/api/v0/record")?; let url = Url::parse(url.as_str())?; let resp = self.client.get(url).send().await?; @@ -366,7 +390,7 @@ impl<'a> Client<'a> { } pub async fn delete(&self) -> Result<()> { - let url = format!("{}/account", self.sync_addr); + let url = make_url(self.sync_addr, "/account")?; let url = Url::parse(url.as_str())?; let resp = self.client.delete(url).send().await?; @@ -385,7 +409,7 @@ impl<'a> Client<'a> { current_password: String, new_password: String, ) -> Result<()> { - let url = format!("{}/account/password", self.sync_addr); + let url = make_url(self.sync_addr, "/account/password")?; let url = Url::parse(url.as_str())?; let resp = self @@ -413,7 +437,7 @@ impl<'a> Client<'a> { pub async fn verify(&self, token: Option<String>) -> Result<(bool, bool)> { // could dedupe this a bit, but it's simple at the moment let (email_sent, verified) = if let Some(token) = token { - let url = format!("{}/api/v0/account/verify", self.sync_addr); + let url = make_url(self.sync_addr, "/api/v0/account/verify")?; let url = Url::parse(url.as_str())?; let resp = self @@ -427,7 +451,7 @@ impl<'a> Client<'a> { (false, resp.verified) } else { - let url = format!("{}/api/v0/account/send-verification", self.sync_addr); + let url = make_url(self.sync_addr, "/api/v0/account/send-verification")?; let url = Url::parse(url.as_str())?; let resp = self.client.post(url).send().await?; |
