Skip to content

Commit 8b02e0a

Browse files
committed
feat!: support passwords in urls.
It's now possible to use urls like `https://user:pass@host/repo` without loosing the password portion of the URL. We also change the `from_parts()` method to take all parts needed to describe a URL, which is a breaking change.
1 parent d137a8c commit 8b02e0a

File tree

5 files changed

+82
-28
lines changed

5 files changed

+82
-28
lines changed

gix-url/src/impls.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ impl Default for Url {
1313
serialize_alternative_form: false,
1414
scheme: Scheme::Ssh,
1515
user: None,
16+
password: None,
1617
host: None,
1718
port: None,
1819
path: bstr::BString::default(),

gix-url/src/lib.rs

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ pub struct Url {
3838
pub scheme: Scheme,
3939
/// The user to impersonate on the remote.
4040
user: Option<String>,
41+
/// The password associated with a user.
42+
password: Option<String>,
4143
/// The host to which to connect. Localhost is implied if `None`.
4244
host: Option<String>,
4345
/// When serializing, use the alternative forms as it was parsed as such.
@@ -50,44 +52,25 @@ pub struct Url {
5052

5153
/// Instantiation
5254
impl Url {
53-
/// Create a new instance from the given parts, which will be validated by parsing them back.
55+
/// Create a new instance from the given parts, including a password, which will be validated by parsing them back.
5456
pub fn from_parts(
5557
scheme: Scheme,
5658
user: Option<String>,
59+
password: Option<String>,
5760
host: Option<String>,
5861
port: Option<u16>,
5962
path: BString,
63+
serialize_alternative_form: bool,
6064
) -> Result<Self, parse::Error> {
6165
parse(
6266
Url {
6367
scheme,
6468
user,
69+
password,
6570
host,
6671
port,
6772
path,
68-
serialize_alternative_form: false,
69-
}
70-
.to_bstring()
71-
.as_ref(),
72-
)
73-
}
74-
75-
/// Create a new instance from the given parts, which will be validated by parsing them back from its alternative form.
76-
pub fn from_parts_as_alternative_form(
77-
scheme: Scheme,
78-
user: Option<String>,
79-
host: Option<String>,
80-
port: Option<u16>,
81-
path: BString,
82-
) -> Result<Self, parse::Error> {
83-
parse(
84-
Url {
85-
scheme,
86-
user,
87-
host,
88-
port,
89-
path,
90-
serialize_alternative_form: true,
73+
serialize_alternative_form,
9174
}
9275
.to_bstring()
9376
.as_ref(),
@@ -178,6 +161,10 @@ impl Url {
178161
match (&self.user, &self.host) {
179162
(Some(user), Some(host)) => {
180163
out.write_all(user.as_bytes())?;
164+
if let Some(password) = &self.password {
165+
out.write_all(&[b':'])?;
166+
out.write_all(password.as_bytes())?;
167+
}
181168
out.write_all(&[b'@'])?;
182169
out.write_all(host.as_bytes())?;
183170
}

gix-url/src/parse.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,12 @@ fn has_no_explicit_protocol(url: &[u8]) -> bool {
6666
}
6767

6868
fn to_owned_url(url: url::Url) -> Result<crate::Url, Error> {
69+
let password = url.password();
6970
Ok(crate::Url {
7071
serialize_alternative_form: false,
7172
scheme: str_to_protocol(url.scheme()),
72-
user: if url.username().is_empty() {
73+
password: password.map(ToOwned::to_owned),
74+
user: if url.username().is_empty() && password.is_none() {
7375
None
7476
} else {
7577
Some(url.username().into())

gix-url/tests/parse/mod.rs

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,31 @@ fn url<'a, 'b>(
3939
gix_url::Url::from_parts(
4040
protocol,
4141
user.into().map(Into::into),
42+
None,
4243
host.into().map(Into::into),
4344
port.into(),
4445
path.into(),
46+
false,
47+
)
48+
.unwrap_or_else(|err| panic!("'{}' failed: {err:?}", path.as_bstr()))
49+
}
50+
51+
fn url_with_pass<'a, 'b>(
52+
protocol: Scheme,
53+
user: impl Into<Option<&'a str>>,
54+
password: impl Into<String>,
55+
host: impl Into<Option<&'b str>>,
56+
port: impl Into<Option<u16>>,
57+
path: &[u8],
58+
) -> gix_url::Url {
59+
gix_url::Url::from_parts(
60+
protocol,
61+
user.into().map(Into::into),
62+
Some(password.into()),
63+
host.into().map(Into::into),
64+
port.into(),
65+
path.into(),
66+
false,
4567
)
4668
.unwrap_or_else(|err| panic!("'{}' failed: {err:?}", path.as_bstr()))
4769
}
@@ -53,12 +75,14 @@ fn url_alternate<'a, 'b>(
5375
port: impl Into<Option<u16>>,
5476
path: &[u8],
5577
) -> gix_url::Url {
56-
let url = gix_url::Url::from_parts_as_alternative_form(
78+
let url = gix_url::Url::from_parts(
5779
protocol.clone(),
5880
user.into().map(Into::into),
81+
None,
5982
host.into().map(Into::into),
6083
port.into(),
6184
path.into(),
85+
true,
6286
)
6387
.expect("valid");
6488
assert_eq!(url.scheme, protocol);
@@ -92,7 +116,7 @@ mod radicle {
92116
mod http {
93117
use gix_url::Scheme;
94118

95-
use crate::parse::{assert_url, assert_url_roundtrip, url};
119+
use crate::parse::{assert_url, assert_url_roundtrip, url, url_with_pass};
96120

97121
#[test]
98122
fn username_expansion_is_unsupported() -> crate::Result {
@@ -102,6 +126,44 @@ mod http {
102126
)
103127
}
104128
#[test]
129+
fn empty_user_cannot_roundtrip() -> crate::Result {
130+
let actual = gix_url::parse("http://@example.com/~byron/hello".into())?;
131+
let expected = url(Scheme::Http, "", "example.com", None, b"/~byron/hello");
132+
assert_eq!(actual, expected);
133+
assert_eq!(
134+
actual.to_bstring(),
135+
"http://example.com/~byron/hello",
136+
"we cannot differentiate between empty user and no user"
137+
);
138+
Ok(())
139+
}
140+
#[test]
141+
fn username_and_password() -> crate::Result {
142+
assert_url_roundtrip(
143+
"http://user:[email protected]/~byron/hello",
144+
url_with_pass(Scheme::Http, "user", "password", "example.com", None, b"/~byron/hello"),
145+
)
146+
}
147+
#[test]
148+
fn only_password() -> crate::Result {
149+
assert_url_roundtrip(
150+
"http://:[email protected]/~byron/hello",
151+
url_with_pass(Scheme::Http, "", "password", "example.com", None, b"/~byron/hello"),
152+
)
153+
}
154+
#[test]
155+
fn username_and_empty_password() -> crate::Result {
156+
let actual = gix_url::parse("http://user:@example.com/~byron/hello".into())?;
157+
let expected = url_with_pass(Scheme::Http, "user", "", "example.com", None, b"/~byron/hello");
158+
assert_eq!(actual, expected);
159+
assert_eq!(
160+
actual.to_bstring(),
161+
"http://[email protected]/~byron/hello",
162+
"an empty password appears like no password to us - fair enough"
163+
);
164+
Ok(())
165+
}
166+
#[test]
105167
fn secure() -> crate::Result {
106168
assert_url_roundtrip(
107169
"https://github.com/byron/gitoxide",

gix-url/tests/parse/ssh.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,14 @@ fn with_user_and_port_and_absolute_path() -> crate::Result {
6666

6767
#[test]
6868
fn ssh_alias_needs_username_to_not_be_considered_a_filepath() {
69-
let url = gix_url::Url::from_parts_as_alternative_form(
69+
let url = gix_url::Url::from_parts(
7070
Scheme::Ssh,
7171
None,
72+
None,
7273
"alias".to_string().into(),
7374
None,
7475
b"path/to/git".as_bstr().into(),
76+
true,
7577
)
7678
.expect("valid");
7779
assert_eq!(

0 commit comments

Comments
 (0)