Skip to content

Commit 0a2154e

Browse files
fix: 405 method not allowed errors caused by trailing forward-slashes (#1348)
## 🎟️ Tracking https://bitwarden.atlassian.net/browse/SM-1162 ## 📔 Objective Trim trailing forward-slashes on server URLs to avoid an ambiguous `405 method not allowed` error in our CLI. All methods of input should be handled: - using the `--server-url` arg - using the `BWS_SERVER_URL` environment variable - reading from the `server-{base,api,identity}` keys in the config file - _writing_ to the config file with `bws config set server-{base,api,identity}` In any of those cases, the changes here should drop any trailing forward-slashes successfully. ## ⏰ Reminders before review - Contributor guidelines followed - All formatters and local linters executed and passed - Written new unit and / or integration tests where applicable - Protected functional changes with optionality (feature flags) - Used internationalization (i18n) for all UI strings - CI builds passed - Communicated to DevOps any deployment requirements - Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team ## 🦮 Reviewer guidelines <!-- Suggested interactions but feel free to use (or not) as you desire! --> - 👍 (`:+1:`) or similar for great changes - 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info - ❓ (`:question:`) for questions - 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - 🎨 (`:art:`) for suggestions / improvements - ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention - 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt - ⛏ (`:pick:`) for minor or nitpick changes
1 parent d79ff05 commit 0a2154e

File tree

2 files changed

+97
-3
lines changed

2 files changed

+97
-3
lines changed

crates/bws/src/cli.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::path::PathBuf;
22

33
use bitwarden_cli::Color;
4-
use clap::{ArgGroup, Parser, Subcommand, ValueEnum};
4+
use clap::{builder::ValueParser, ArgGroup, Parser, Subcommand, ValueEnum};
55
use clap_complete::Shell;
66
use uuid::Uuid;
77

@@ -63,10 +63,18 @@ pub(crate) struct Cli {
6363
#[arg(short = 'p', long, global = true, env = PROFILE_KEY_VAR_NAME, help="Profile to use from the config file")]
6464
pub(crate) profile: Option<String>,
6565

66-
#[arg(short = 'u', long, global = true, env = SERVER_URL_KEY_VAR_NAME, help="Override the server URL from the config file")]
66+
#[arg(short = 'u', long, global = true, env = SERVER_URL_KEY_VAR_NAME, help="Override the server URL from the config file", value_parser = ValueParser::new(url_parser) )]
6767
pub(crate) server_url: Option<String>,
6868
}
6969

70+
fn url_parser(value: &str) -> Result<String, String> {
71+
if value.starts_with("http://") || value.starts_with("https://") {
72+
Ok(value.trim_end_matches('/').into())
73+
} else {
74+
Err(format!("'{value}' is not a valid URL"))
75+
}
76+
}
77+
7078
#[derive(Subcommand, Debug)]
7179
pub(crate) enum Commands {
7280
#[command(long_about = "Configure the CLI", arg_required_else_help(true))]

crates/bws/src/config.rs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,35 @@ pub(crate) struct Config {
1717

1818
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1919
pub(crate) struct Profile {
20+
#[serde(deserialize_with = "deserialize_trimmed_url", default)]
2021
pub server_base: Option<String>,
22+
#[serde(deserialize_with = "deserialize_trimmed_url", default)]
2123
pub server_api: Option<String>,
24+
#[serde(deserialize_with = "deserialize_trimmed_url", default)]
2225
pub server_identity: Option<String>,
2326
pub state_dir: Option<String>,
2427
pub state_opt_out: Option<String>,
2528
}
2629

30+
fn deserialize_trimmed_url<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
31+
where
32+
D: serde::Deserializer<'de>,
33+
{
34+
let opt_string: Option<String> = Option::deserialize(deserializer)?;
35+
Ok(opt_string.map(|s| s.trim_end_matches('/').to_string()))
36+
}
37+
2738
impl ProfileKey {
2839
fn update_profile_value(&self, p: &mut Profile, value: String) {
40+
let value = if matches!(
41+
self,
42+
ProfileKey::server_base | ProfileKey::server_api | ProfileKey::server_identity
43+
) {
44+
value.trim_end_matches('/').to_string()
45+
} else {
46+
value
47+
};
48+
2949
match self {
3050
ProfileKey::server_base => p.server_base = Some(value),
3151
ProfileKey::server_api => p.server_api = Some(value),
@@ -90,7 +110,12 @@ pub(crate) fn update_profile(
90110
let mut config = load_config(config_file, false)?;
91111

92112
let p = config.profiles.entry(profile).or_default();
93-
name.update_profile_value(p, value);
113+
114+
if value.starts_with("http://") || value.starts_with("https://") {
115+
name.update_profile_value(p, value.trim_end_matches('/').to_string());
116+
} else {
117+
name.update_profile_value(p, value);
118+
}
94119

95120
write_config(config, config_file)?;
96121
Ok(())
@@ -123,6 +148,7 @@ impl Profile {
123148
state_opt_out: None,
124149
})
125150
}
151+
126152
pub(crate) fn api_url(&self) -> Result<String> {
127153
if let Some(api) = &self.server_api {
128154
return Ok(api.clone());
@@ -214,4 +240,64 @@ mod tests {
214240
c.unwrap().profiles["default"].server_base.as_ref().unwrap()
215241
);
216242
}
243+
244+
#[test]
245+
fn config_trims_trailing_forward_slashes_in_urls() {
246+
let tmpfile = NamedTempFile::new().unwrap();
247+
write!(tmpfile.as_file(), "[profiles.default]").unwrap();
248+
249+
let _ = update_profile(
250+
Some(tmpfile.as_ref()),
251+
"default".to_owned(),
252+
ProfileKey::server_base,
253+
"https://vault.bitwarden.com//////".to_owned(),
254+
);
255+
256+
let _ = update_profile(
257+
Some(tmpfile.as_ref()),
258+
"default".to_owned(),
259+
ProfileKey::server_api,
260+
"https://api.bitwarden.com/".to_owned(),
261+
);
262+
263+
let _ = update_profile(
264+
Some(tmpfile.as_ref()),
265+
"default".to_owned(),
266+
ProfileKey::server_identity,
267+
"https://identity.bitwarden.com/".to_owned(),
268+
);
269+
270+
let c = load_config(Some(Path::new(tmpfile.as_ref())), true).unwrap();
271+
assert_eq!(
272+
"https://vault.bitwarden.com",
273+
c.profiles["default"].server_base.as_ref().unwrap()
274+
);
275+
assert_eq!(
276+
"https://api.bitwarden.com",
277+
c.profiles["default"].server_api.as_ref().unwrap()
278+
);
279+
assert_eq!(
280+
"https://identity.bitwarden.com",
281+
c.profiles["default"].server_identity.as_ref().unwrap()
282+
);
283+
}
284+
285+
#[test]
286+
fn config_does_not_trim_forward_slashes_in_non_url_values() {
287+
let tmpfile = NamedTempFile::new().unwrap();
288+
write!(tmpfile.as_file(), "[profiles.default]").unwrap();
289+
290+
let _ = update_profile(
291+
Some(tmpfile.as_ref()),
292+
"default".to_owned(),
293+
ProfileKey::state_dir,
294+
"/dev/null/".to_owned(),
295+
);
296+
297+
let c = load_config(Some(Path::new(tmpfile.as_ref())), true).unwrap();
298+
assert_eq!(
299+
"/dev/null/",
300+
c.profiles["default"].state_dir.as_ref().unwrap()
301+
);
302+
}
217303
}

0 commit comments

Comments
 (0)