Skip to content

Commit c6897d9

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 c6897d9

File tree

6 files changed

+124
-53
lines changed

6 files changed

+124
-53
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: 15 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(),
@@ -133,6 +116,10 @@ impl Url {
133116
pub fn user(&self) -> Option<&str> {
134117
self.user.as_deref()
135118
}
119+
/// Returns the password mentioned in the url, if present.
120+
pub fn password(&self) -> Option<&str> {
121+
self.password.as_deref()
122+
}
136123
/// Returns the host mentioned in the url, if present.
137124
pub fn host(&self) -> Option<&str> {
138125
self.host.as_deref()
@@ -178,6 +165,10 @@ impl Url {
178165
match (&self.user, &self.host) {
179166
(Some(user), Some(host)) => {
180167
out.write_all(user.as_bytes())?;
168+
if let Some(password) = &self.password {
169+
out.write_all(&[b':'])?;
170+
out.write_all(password.as_bytes())?;
171+
}
181172
out.write_all(&[b'@'])?;
182173
out.write_all(host.as_bytes())?;
183174
}

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/http.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use gix_url::Scheme;
2+
3+
use crate::parse::{assert_url, assert_url_roundtrip, url, url_with_pass};
4+
5+
#[test]
6+
fn username_expansion_is_unsupported() -> crate::Result {
7+
assert_url_roundtrip(
8+
"http://example.com/~byron/hello",
9+
url(Scheme::Http, None, "example.com", None, b"/~byron/hello"),
10+
)
11+
}
12+
13+
#[test]
14+
fn empty_user_cannot_roundtrip() -> crate::Result {
15+
let actual = gix_url::parse("http://@example.com/~byron/hello".into())?;
16+
let expected = url(Scheme::Http, "", "example.com", None, b"/~byron/hello");
17+
assert_eq!(actual, expected);
18+
assert_eq!(
19+
actual.to_bstring(),
20+
"http://example.com/~byron/hello",
21+
"we cannot differentiate between empty user and no user"
22+
);
23+
Ok(())
24+
}
25+
26+
#[test]
27+
fn username_and_password() -> crate::Result {
28+
assert_url_roundtrip(
29+
"http://user:password@example.com/~byron/hello",
30+
url_with_pass(Scheme::Http, "user", "password", "example.com", None, b"/~byron/hello"),
31+
)
32+
}
33+
34+
#[test]
35+
fn username_and_password_and_port() -> crate::Result {
36+
assert_url_roundtrip(
37+
"http://user:password@example.com:8080/~byron/hello",
38+
url_with_pass(Scheme::Http, "user", "password", "example.com", 8080, b"/~byron/hello"),
39+
)
40+
}
41+
42+
#[test]
43+
fn only_password() -> crate::Result {
44+
assert_url_roundtrip(
45+
"http://:password@example.com/~byron/hello",
46+
url_with_pass(Scheme::Http, "", "password", "example.com", None, b"/~byron/hello"),
47+
)
48+
}
49+
50+
#[test]
51+
fn username_and_empty_password() -> crate::Result {
52+
let actual = gix_url::parse("http://user:@example.com/~byron/hello".into())?;
53+
let expected = url_with_pass(Scheme::Http, "user", "", "example.com", None, b"/~byron/hello");
54+
assert_eq!(actual, expected);
55+
assert_eq!(
56+
actual.to_bstring(),
57+
"http://user@example.com/~byron/hello",
58+
"an empty password appears like no password to us - fair enough"
59+
);
60+
Ok(())
61+
}
62+
63+
#[test]
64+
fn secure() -> crate::Result {
65+
assert_url_roundtrip(
66+
"https://github.com/byron/gitoxide",
67+
url(Scheme::Https, None, "github.com", None, b"/byron/gitoxide"),
68+
)
69+
}
70+
71+
#[test]
72+
fn http_missing_path() -> crate::Result {
73+
assert_url_roundtrip("http://host.xz/", url(Scheme::Http, None, "host.xz", None, b"/"))?;
74+
assert_url("http://host.xz", url(Scheme::Http, None, "host.xz", None, b"/"))?;
75+
Ok(())
76+
}

gix-url/tests/parse/mod.rs

Lines changed: 26 additions & 27 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);
@@ -89,33 +113,8 @@ mod radicle {
89113
}
90114
}
91115

92-
mod http {
93-
use gix_url::Scheme;
94-
95-
use crate::parse::{assert_url, assert_url_roundtrip, url};
116+
mod http;
96117

97-
#[test]
98-
fn username_expansion_is_unsupported() -> crate::Result {
99-
assert_url_roundtrip(
100-
"http://example.com/~byron/hello",
101-
url(Scheme::Http, None, "example.com", None, b"/~byron/hello"),
102-
)
103-
}
104-
#[test]
105-
fn secure() -> crate::Result {
106-
assert_url_roundtrip(
107-
"https://github.com/byron/gitoxide",
108-
url(Scheme::Https, None, "github.com", None, b"/byron/gitoxide"),
109-
)
110-
}
111-
112-
#[test]
113-
fn http_missing_path() -> crate::Result {
114-
assert_url_roundtrip("http://host.xz/", url(Scheme::Http, None, "host.xz", None, b"/"))?;
115-
assert_url("http://host.xz", url(Scheme::Http, None, "host.xz", None, b"/"))?;
116-
Ok(())
117-
}
118-
}
119118
mod git {
120119
use gix_url::Scheme;
121120

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)