Skip to content

Commit 85f8b28

Browse files
committed
Merge branch 'password-in-urls'
2 parents d137a8c + 7830f1e commit 85f8b28

File tree

14 files changed

+173
-70
lines changed

14 files changed

+173
-70
lines changed

gix-transport/src/client/blocking_io/connect.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub(crate) mod function {
2323
Ok(match url.scheme {
2424
gix_url::Scheme::Ext(_) => return Err(Error::UnsupportedScheme(url.scheme)),
2525
gix_url::Scheme::File => {
26-
if url.user().is_some() || url.host().is_some() || url.port.is_some() {
26+
if url.user().is_some() || url.password().is_some() || url.host().is_some() || url.port.is_some() {
2727
return Err(Error::UnsupportedUrlTokens {
2828
url: url.to_bstring(),
2929
scheme: url.scheme,
@@ -59,10 +59,9 @@ pub(crate) mod function {
5959
#[cfg(not(any(feature = "http-client-curl", feature = "http-client-reqwest")))]
6060
gix_url::Scheme::Https | gix_url::Scheme::Http => return Err(Error::CompiledWithoutHttp(url.scheme)),
6161
#[cfg(any(feature = "http-client-curl", feature = "http-client-reqwest"))]
62-
gix_url::Scheme::Https | gix_url::Scheme::Http => Box::new(crate::client::http::connect(
63-
&url.to_bstring().to_string(),
64-
options.version,
65-
)),
62+
gix_url::Scheme::Https | gix_url::Scheme::Http => {
63+
Box::new(crate::client::http::connect(url, options.version))
64+
}
6665
})
6766
}
6867
}

gix-transport/src/client/blocking_io/file.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ impl SpawnProcessOnDemand {
7171
}
7272
fn new_local(path: BString, version: Protocol) -> SpawnProcessOnDemand {
7373
SpawnProcessOnDemand {
74-
url: gix_url::Url::from_parts_as_alternative_form(gix_url::Scheme::File, None, None, None, path.clone())
74+
url: gix_url::Url::from_parts(gix_url::Scheme::File, None, None, None, None, path.clone(), true)
7575
.expect("valid url"),
7676
path,
7777
ssh_cmd: None,

gix-transport/src/client/blocking_io/http/mod.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,26 +217,40 @@ pub struct Transport<H: Http> {
217217
impl<H: Http> Transport<H> {
218218
/// Create a new instance with `http` as implementation to communicate to `url` using the given `desired_version`.
219219
/// Note that we will always fallback to other versions as supported by the server.
220-
pub fn new_http(http: H, url: &str, desired_version: Protocol) -> Self {
220+
pub fn new_http(http: H, url: gix_url::Url, desired_version: Protocol) -> Self {
221+
let identity = url
222+
.user()
223+
.zip(url.password())
224+
.map(|(user, pass)| gix_sec::identity::Account {
225+
username: user.to_string(),
226+
password: pass.to_string(),
227+
});
221228
Transport {
222-
url: url.to_owned(),
229+
url: url.to_bstring().to_string(),
223230
user_agent_header: concat!("User-Agent: git/oxide-", env!("CARGO_PKG_VERSION")),
224231
desired_version,
225232
actual_version: Default::default(),
226233
service: None,
227234
http,
228235
line_provider: None,
229-
identity: None,
236+
identity,
230237
}
231238
}
232239
}
233240

241+
impl<H: Http> Transport<H> {
242+
/// Returns the identity that the transport uses when connecting to the remote.
243+
pub fn identity(&self) -> Option<&gix_sec::identity::Account> {
244+
self.identity.as_ref()
245+
}
246+
}
247+
234248
#[cfg(any(feature = "http-client-curl", feature = "http-client-reqwest"))]
235249
impl Transport<Impl> {
236250
/// Create a new instance to communicate to `url` using the given `desired_version` of the `git` protocol.
237251
///
238252
/// Note that the actual implementation depends on feature toggles.
239-
pub fn new(url: &str, desired_version: Protocol) -> Self {
253+
pub fn new(url: gix_url::Url, desired_version: Protocol) -> Self {
240254
Self::new_http(Impl::default(), url, desired_version)
241255
}
242256
}
@@ -497,13 +511,13 @@ impl<H: Http, B: ExtendedBufRead + Unpin> ExtendedBufRead for HeadersThenBody<H,
497511

498512
/// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol, with `http` as implementation.
499513
#[cfg(all(feature = "http-client", not(feature = "http-client-curl")))]
500-
pub fn connect_http<H: Http>(http: H, url: &str, desired_version: Protocol) -> Transport<H> {
514+
pub fn connect_http<H: Http>(http: H, url: gix_url::Url, desired_version: Protocol) -> Transport<H> {
501515
Transport::new_http(http, url, desired_version)
502516
}
503517

504518
/// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol.
505519
#[cfg(any(feature = "http-client-curl", feature = "http-client-reqwest"))]
506-
pub fn connect(url: &str, desired_version: Protocol) -> Transport<Impl> {
520+
pub fn connect(url: gix_url::Url, desired_version: Protocol) -> Transport<Impl> {
507521
Transport::new(url, desired_version)
508522
}
509523

gix-transport/src/client/blocking_io/ssh/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ pub mod connect {
8686
///
8787
/// The `desired_version` is the preferred protocol version when establishing the connection, but note that it can be
8888
/// downgraded by servers not supporting it.
89+
#[allow(clippy::result_large_err)]
8990
pub fn connect(
9091
url: gix_url::Url,
9192
desired_version: Protocol,

gix-transport/src/client/blocking_io/ssh/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ mod program_kind {
192192
}
193193

194194
fn joined(input: &[&str]) -> String {
195-
input.iter().copied().collect::<Vec<_>>().join(" ")
195+
input.to_vec().join(" ")
196196
}
197197
fn try_call(
198198
kind: ProgramKind,

gix-transport/tests/client/blocking_io/http/mock.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,13 @@ pub fn serve_and_connect(
9999
version: Protocol,
100100
) -> Result<(Server, http::Transport<http::Impl>), crate::Error> {
101101
let server = serve_once(name);
102-
let url = format!(
102+
let url_str = format!(
103103
"http://{}:{}/{}",
104104
&server.addr.ip().to_string(),
105105
&server.addr.port(),
106106
path
107107
);
108-
let client = gix_transport::client::http::connect(&url, version);
109-
assert_eq!(url, client.to_url().as_ref());
108+
let client = gix_transport::client::http::connect(url_str.as_str().try_into()?, version);
109+
assert_eq!(url_str, client.to_url().as_ref());
110110
Ok((server, client))
111111
}

gix-transport/tests/client/blocking_io/http/mod.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ fn http_status_500_is_communicated_via_special_io_error() -> crate::Result {
4646
Ok(())
4747
}
4848

49+
#[test]
50+
fn http_identity_is_picked_up_from_url() -> crate::Result {
51+
let transport =
52+
gix_transport::client::http::connect("https://user:pass@example.com/repo".try_into()?, Protocol::V2);
53+
assert_eq!(transport.to_url().as_ref(), "https://user:pass@example.com/repo");
54+
assert_eq!(
55+
transport.identity(),
56+
Some(&gix_sec::identity::Account {
57+
username: "user".into(),
58+
password: "pass".into()
59+
})
60+
);
61+
Ok(())
62+
}
63+
4964
// based on a test in cargo
5065
#[test]
5166
fn http_will_use_pipelining() {
@@ -108,7 +123,8 @@ fn http_will_use_pipelining() {
108123
});
109124

110125
let url = format!("http://{}:{}/reponame", &addr.ip().to_string(), &addr.port(),);
111-
let mut client = gix_transport::client::http::connect(&url, gix_transport::Protocol::V2);
126+
let mut client =
127+
gix_transport::client::http::connect(url.try_into().expect("valid url"), gix_transport::Protocol::V2);
112128
match client.handshake(gix_transport::Service::UploadPack, &[]) {
113129
Ok(_) => unreachable!("expecting permission denied to be detected"),
114130
Err(gix_transport::client::Error::Io(err)) if err.kind() == std::io::ErrorKind::PermissionDenied => {}

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+
}

0 commit comments

Comments
 (0)