1use std::collections::HashSet;
32use std::io::{Cursor, Read};
33use std::path::Path;
34
35use crate::error::{Error, RemoteUnreadableReason, Result};
36use crate::fetch::Progress;
37use crate::objects::ObjectId;
38use crate::pkt_line;
39use crate::transfer::{FetchOptions, FetchOutcome};
40use crate::transport::stateless_http::StatelessHttpConnection;
41use crate::transport::{ConnectOptions, Connection, Service, Transport};
42
43#[cfg(feature = "http-ureq")]
44pub mod ureq_client;
45
46pub trait HttpClient: Send + Sync {
58 fn get(&self, url: &str, git_protocol: Option<&str>) -> Result<Vec<u8>>;
64
65 fn post(
72 &self,
73 url: &str,
74 content_type: &str,
75 accept: &str,
76 body: &[u8],
77 git_protocol: Option<&str>,
78 ) -> Result<Vec<u8>>;
79
80 fn post_into_reader(
89 &self,
90 url: &str,
91 content_type: &str,
92 accept: &str,
93 body: &[u8],
94 git_protocol: Option<&str>,
95 ) -> Result<Box<dyn Read + Send>> {
96 Ok(Box::new(Cursor::new(self.post(
97 url,
98 content_type,
99 accept,
100 body,
101 git_protocol,
102 )?)))
103 }
104
105 fn post_streaming(
124 &self,
125 url: &str,
126 content_type: &str,
127 accept: &str,
128 body: &mut dyn Read,
129 git_protocol: Option<&str>,
130 ) -> Result<Box<dyn Read + Send>> {
131 let mut buffered = Vec::new();
132 body.read_to_end(&mut buffered).map_err(Error::Io)?;
133 self.post_into_reader(url, content_type, accept, &buffered, git_protocol)
134 }
135
136 fn get_with_final_url(
152 &self,
153 url: &str,
154 git_protocol: Option<&str>,
155 ) -> Result<(Vec<u8>, Option<String>)> {
156 Ok((self.get(url, git_protocol)?, None))
157 }
158
159 fn get_with_final_url_exact(
162 &self,
163 url: &str,
164 git_protocol: Option<&str>,
165 ) -> Result<(Vec<u8>, Option<String>)> {
166 self.get_with_final_url(url, git_protocol)
167 }
168
169 fn git_protocol_header(&self) -> Option<&str> {
172 None
173 }
174
175 fn smart_http_enabled(&self) -> bool {
178 true
179 }
180
181 fn reset_auth_after_redirect_rebase(&self) {}
184
185 fn send(&self, request: &HttpRequest<'_>) -> Result<HttpResponse> {
203 Err(Error::Message(format!(
204 "this HTTP client cannot send {} requests",
205 request.method.as_str()
206 )))
207 }
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub enum HttpMethod {
213 Get,
215 Head,
217 Post,
219 Patch,
221 Put,
223 Delete,
225}
226
227impl HttpMethod {
228 #[must_use]
230 pub fn as_str(self) -> &'static str {
231 match self {
232 Self::Get => "GET",
233 Self::Head => "HEAD",
234 Self::Post => "POST",
235 Self::Patch => "PATCH",
236 Self::Put => "PUT",
237 Self::Delete => "DELETE",
238 }
239 }
240}
241
242#[derive(Debug, Clone)]
244pub struct HttpRequest<'a> {
245 pub method: HttpMethod,
247 pub url: &'a str,
249 pub headers: Vec<(&'static str, String)>,
251 pub body: &'a [u8],
253 pub long_running: bool,
256}
257
258impl<'a> HttpRequest<'a> {
259 #[must_use]
261 pub fn new(method: HttpMethod, url: &'a str) -> Self {
262 Self {
263 method,
264 url,
265 headers: Vec::new(),
266 body: &[],
267 long_running: false,
268 }
269 }
270
271 #[must_use]
273 pub fn header(mut self, name: &'static str, value: impl Into<String>) -> Self {
274 self.headers.push((name, value.into()));
275 self
276 }
277
278 #[must_use]
280 pub fn body(mut self, body: &'a [u8]) -> Self {
281 self.body = body;
282 self
283 }
284
285 #[must_use]
287 pub fn long_running(mut self) -> Self {
288 self.long_running = true;
289 self
290 }
291}
292
293pub struct HttpResponse {
295 pub status: u16,
297 pub headers: Vec<(String, String)>,
299 pub body: Box<dyn Read + Send>,
301}
302
303impl HttpResponse {
304 #[must_use]
306 pub fn header(&self, name: &str) -> Option<&str> {
307 self.headers
308 .iter()
309 .find(|(n, _)| n.eq_ignore_ascii_case(name))
310 .map(|(_, v)| v.as_str())
311 }
312
313 #[must_use]
315 pub fn is_success(&self) -> bool {
316 (200..300).contains(&self.status)
317 }
318
319 pub fn into_bytes(mut self) -> Result<Vec<u8>> {
325 let mut out = Vec::new();
326 self.body.read_to_end(&mut out).map_err(Error::Io)?;
327 Ok(out)
328 }
329}
330
331impl std::fmt::Debug for HttpResponse {
332 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333 f.debug_struct("HttpResponse")
334 .field("status", &self.status)
335 .field("headers", &self.headers)
336 .finish_non_exhaustive()
337 }
338}
339
340impl HttpClient for std::sync::Arc<dyn HttpClient> {
343 fn get(&self, url: &str, git_protocol: Option<&str>) -> Result<Vec<u8>> {
344 (**self).get(url, git_protocol)
345 }
346
347 fn post(
348 &self,
349 url: &str,
350 content_type: &str,
351 accept: &str,
352 body: &[u8],
353 git_protocol: Option<&str>,
354 ) -> Result<Vec<u8>> {
355 (**self).post(url, content_type, accept, body, git_protocol)
356 }
357
358 fn post_into_reader(
359 &self,
360 url: &str,
361 content_type: &str,
362 accept: &str,
363 body: &[u8],
364 git_protocol: Option<&str>,
365 ) -> Result<Box<dyn Read + Send>> {
366 (**self).post_into_reader(url, content_type, accept, body, git_protocol)
367 }
368
369 fn post_streaming(
370 &self,
371 url: &str,
372 content_type: &str,
373 accept: &str,
374 body: &mut dyn Read,
375 git_protocol: Option<&str>,
376 ) -> Result<Box<dyn Read + Send>> {
377 (**self).post_streaming(url, content_type, accept, body, git_protocol)
378 }
379
380 fn get_with_final_url(
381 &self,
382 url: &str,
383 git_protocol: Option<&str>,
384 ) -> Result<(Vec<u8>, Option<String>)> {
385 (**self).get_with_final_url(url, git_protocol)
386 }
387
388 fn get_with_final_url_exact(
389 &self,
390 url: &str,
391 git_protocol: Option<&str>,
392 ) -> Result<(Vec<u8>, Option<String>)> {
393 (**self).get_with_final_url_exact(url, git_protocol)
394 }
395
396 fn git_protocol_header(&self) -> Option<&str> {
397 (**self).git_protocol_header()
398 }
399
400 fn smart_http_enabled(&self) -> bool {
401 (**self).smart_http_enabled()
402 }
403
404 fn reset_auth_after_redirect_rebase(&self) {
405 (**self).reset_auth_after_redirect_rebase();
406 }
407
408 fn send(&self, request: &HttpRequest<'_>) -> Result<HttpResponse> {
409 (**self).send(request)
410 }
411}
412
413impl<C: HttpClient> HttpClient for std::sync::Arc<C> {
414 fn get(&self, url: &str, git_protocol: Option<&str>) -> Result<Vec<u8>> {
415 (**self).get(url, git_protocol)
416 }
417
418 fn post(
419 &self,
420 url: &str,
421 content_type: &str,
422 accept: &str,
423 body: &[u8],
424 git_protocol: Option<&str>,
425 ) -> Result<Vec<u8>> {
426 (**self).post(url, content_type, accept, body, git_protocol)
427 }
428
429 fn post_into_reader(
430 &self,
431 url: &str,
432 content_type: &str,
433 accept: &str,
434 body: &[u8],
435 git_protocol: Option<&str>,
436 ) -> Result<Box<dyn Read + Send>> {
437 (**self).post_into_reader(url, content_type, accept, body, git_protocol)
438 }
439
440 fn post_streaming(
441 &self,
442 url: &str,
443 content_type: &str,
444 accept: &str,
445 body: &mut dyn Read,
446 git_protocol: Option<&str>,
447 ) -> Result<Box<dyn Read + Send>> {
448 (**self).post_streaming(url, content_type, accept, body, git_protocol)
449 }
450
451 fn get_with_final_url(
452 &self,
453 url: &str,
454 git_protocol: Option<&str>,
455 ) -> Result<(Vec<u8>, Option<String>)> {
456 (**self).get_with_final_url(url, git_protocol)
457 }
458
459 fn get_with_final_url_exact(
460 &self,
461 url: &str,
462 git_protocol: Option<&str>,
463 ) -> Result<(Vec<u8>, Option<String>)> {
464 (**self).get_with_final_url_exact(url, git_protocol)
465 }
466
467 fn git_protocol_header(&self) -> Option<&str> {
468 (**self).git_protocol_header()
469 }
470
471 fn smart_http_enabled(&self) -> bool {
472 (**self).smart_http_enabled()
473 }
474
475 fn reset_auth_after_redirect_rebase(&self) {
476 (**self).reset_auth_after_redirect_rebase();
477 }
478
479 fn send(&self, request: &HttpRequest<'_>) -> Result<HttpResponse> {
480 (**self).send(request)
481 }
482}
483
484const UPLOAD_PACK: &str = "git-upload-pack";
485
486#[derive(Clone, Debug)]
488pub(crate) struct InfoRefsDiscoveryHttp {
489 pub url: String,
490 pub status: u16,
491 pub content_type: Option<String>,
492}
493
494#[cfg(feature = "http-ureq")]
496#[must_use]
497pub(crate) fn is_info_refs_discovery_url(url: &str) -> bool {
498 url.contains("/info/refs") && url.contains("service=git-")
499}
500
501#[must_use]
503pub(crate) fn http_content_type_media_type(raw: &str) -> String {
504 raw.split(';').next().unwrap_or(raw).trim().to_owned()
505}
506
507#[must_use]
509pub(crate) fn body_looks_like_non_git_server(body: &[u8], content_type: Option<&str>) -> bool {
510 if content_type
511 .is_some_and(|ct| http_content_type_media_type(ct).eq_ignore_ascii_case("text/html"))
512 {
513 return true;
514 }
515 let body = body.strip_prefix(b"\xef\xbb\xbf").unwrap_or(body);
516 body.starts_with(b"<!")
517 || body
518 .get(..4)
519 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"<htm"))
520}
521
522pub(crate) fn not_git_server_error(
523 url: Option<String>,
524 status: u16,
525 content_type: Option<String>,
526) -> Error {
527 Error::RemoteUnreadable {
528 url,
529 reason: RemoteUnreadableReason::NotGitServer {
530 status,
531 content_type,
532 },
533 }
534}
535
536fn reject_non_git_discovery_http(http: &InfoRefsDiscoveryHttp, body: &[u8]) -> Result<()> {
537 if body_looks_like_non_git_server(body, http.content_type.as_deref()) {
538 return Err(not_git_server_error(
539 Some(http.url.clone()),
540 http.status,
541 http.content_type.clone(),
542 ));
543 }
544 Ok(())
545}
546
547fn strip_service_advertisement_for_discovery<'a>(
548 body: &'a [u8],
549 http: Option<&InfoRefsDiscoveryHttp>,
550) -> Result<&'a [u8]> {
551 if let Some(ctx) = http {
552 reject_non_git_discovery_http(ctx, body)?;
553 } else if body_looks_like_non_git_server(body, None) {
554 return Err(not_git_server_error(None, 200, None));
555 }
556 match strip_service_advertisement(body) {
557 Ok(stripped) => Ok(stripped),
558 Err(Error::Io(e)) if e.kind() == std::io::ErrorKind::InvalidData => {
559 if let Some(ctx) = http {
560 Err(not_git_server_error(
561 Some(ctx.url.clone()),
562 ctx.status,
563 ctx.content_type.clone(),
564 ))
565 } else {
566 Err(not_git_server_error(None, 200, None))
567 }
568 }
569 Err(e) => Err(e),
570 }
571}
572
573fn strip_service_advertisement(body: &[u8]) -> Result<&[u8]> {
582 let mut cur = Cursor::new(body);
583 let start = cur.position();
584 match pkt_line::read_packet(&mut cur)? {
585 Some(pkt_line::Packet::Data(line)) if line.starts_with("# service=") => {
586 match pkt_line::read_packet(&mut cur)? {
588 Some(pkt_line::Packet::Flush) | None => {}
589 _ => {
590 return Ok(body);
592 }
593 }
594 let pos = cur.position() as usize;
595 Ok(&body[pos..])
596 }
597 _ => {
598 cur.set_position(start);
599 Ok(body)
600 }
601 }
602}
603
604#[derive(Clone, Debug)]
606struct AdvRef {
607 name: String,
608 oid: ObjectId,
609}
610
611struct Discovery {
614 protocol_version: u8,
615 refs: Vec<AdvRef>,
616 caps: HashSet<String>,
617 head_symref: Option<String>,
618 object_format: String,
619}
620
621fn parse_advertisement(body: &[u8]) -> Result<Discovery> {
629 let mut cur = Cursor::new(body);
630
631 let first = match pkt_line::read_packet(&mut cur)? {
633 None | Some(pkt_line::Packet::Flush) => {
634 return Ok(Discovery {
636 protocol_version: 0,
637 refs: Vec::new(),
638 caps: HashSet::new(),
639 head_symref: None,
640 object_format: "sha1".to_owned(),
641 });
642 }
643 Some(pkt_line::Packet::Data(s)) => s,
644 Some(other) => {
645 return Err(Error::Message(format!(
646 "unexpected first advertisement packet: {other:?}"
647 )))
648 }
649 };
650 if first.trim_end() == "version 2" {
651 let mut caps = HashSet::new();
653 loop {
654 match pkt_line::read_packet(&mut cur)? {
655 None | Some(pkt_line::Packet::Flush) => break,
656 Some(pkt_line::Packet::Data(s)) => {
657 caps.insert(s.trim_end().to_owned());
658 }
659 Some(_) => break,
660 }
661 }
662 let object_format = caps
663 .iter()
664 .find_map(|c| c.strip_prefix("object-format="))
665 .unwrap_or("sha1")
666 .to_owned();
667 return Ok(Discovery {
668 protocol_version: 2,
669 refs: Vec::new(),
670 caps,
671 head_symref: None,
672 object_format,
673 });
674 }
675
676 cur.set_position(0);
678 let mut refs = Vec::new();
679 let mut caps: HashSet<String> = HashSet::new();
680 let mut head_symref = None;
681 let mut first_ref_line = true;
682 loop {
683 match pkt_line::read_packet(&mut cur)? {
684 None | Some(pkt_line::Packet::Flush) => break,
685 Some(pkt_line::Packet::Data(line)) => {
686 let line = line.trim_end_matches('\n');
687 if line.starts_with("version ") {
688 continue;
689 }
690 if line.starts_with("shallow ") || line.starts_with("unshallow ") {
691 continue;
692 }
693 let (payload, cap_part) = match line.split_once('\0') {
694 Some((p, c)) => (p.trim(), Some(c)),
695 None => (line.trim(), None),
696 };
697 let Some((oid_hex, refname)) =
698 payload.split_once('\t').or_else(|| payload.split_once(' '))
699 else {
700 continue;
701 };
702 let oid_hex = oid_hex.trim();
703 let refname = refname.trim();
704 if first_ref_line {
705 if let Some(raw_caps) = cap_part {
706 for cap in raw_caps.split_whitespace() {
707 if let Some(target) = cap.strip_prefix("symref=HEAD:") {
708 if crate::refs::is_valid_advertised_symref_target(target) {
709 head_symref = Some(target.to_owned());
710 }
711 }
712 caps.insert(cap.to_owned());
713 }
714 }
715 first_ref_line = false;
716 }
717 if refname.is_empty() {
718 continue;
719 }
720 if oid_hex.bytes().all(|b| b == b'0') {
722 continue;
723 }
724 let oid = ObjectId::from_hex(oid_hex).map_err(|e| {
725 Error::Message(format!("bad oid in advertisement: {oid_hex}: {e}"))
726 })?;
727 let valid_ref = refname == "HEAD"
728 || crate::refs::is_valid_fetch_advertised_ref(refname)
729 || refname
730 .strip_suffix("^{}")
731 .is_some_and(crate::refs::is_valid_fetch_advertised_ref);
732 if valid_ref {
733 refs.push(AdvRef {
734 name: refname.to_owned(),
735 oid,
736 });
737 }
738 }
739 Some(other) => {
740 return Err(Error::Message(format!(
741 "unexpected packet in advertisement: {other:?}"
742 )))
743 }
744 }
745 }
746 let object_format = caps
747 .iter()
748 .find_map(|c| c.strip_prefix("object-format="))
749 .unwrap_or("sha1")
750 .to_owned();
751 Ok(Discovery {
752 protocol_version: if caps.contains("version 1") { 1 } else { 0 },
753 refs,
754 caps,
755 head_symref,
756 object_format,
757 })
758}
759
760fn info_refs_url(repo_url: &str) -> String {
762 smart_info_refs_discovery_url(repo_url)
763}
764
765pub type UploadPackDiscovery = (u8, Vec<(String, ObjectId)>, Vec<String>, Option<String>);
767
768fn upload_pack_discovery_from_body(body: &[u8]) -> Result<UploadPackDiscovery> {
769 upload_pack_discovery_from_body_with_http(body, None)
770}
771
772fn upload_pack_discovery_from_body_with_http(
773 body: &[u8],
774 http: Option<&InfoRefsDiscoveryHttp>,
775) -> Result<UploadPackDiscovery> {
776 let stripped = strip_service_advertisement_for_discovery(body, http)?;
777 let disc = parse_advertisement(stripped)?;
778 let refs: Vec<(String, ObjectId)> = disc
779 .refs
780 .iter()
781 .filter(|r| r.name != "HEAD" && !r.name.ends_with("^{}"))
782 .map(|r| (r.name.clone(), r.oid))
783 .collect();
784 let caps: Vec<String> = disc.caps.iter().cloned().collect();
785 Ok((disc.protocol_version, refs, caps, disc.head_symref))
786}
787
788pub fn discover_upload_pack_from_body(body: &[u8]) -> Result<UploadPackDiscovery> {
790 upload_pack_discovery_from_body(body)
791}
792
793pub fn discover_upload_pack_for_list_refs_from_body(body: &[u8]) -> Result<UploadPackDiscovery> {
796 let stripped = strip_service_advertisement_for_discovery(body, None)?;
797 let disc = parse_advertisement(stripped)?;
798 let refs: Vec<(String, ObjectId)> = disc
799 .refs
800 .iter()
801 .filter(|r| r.name != "HEAD")
802 .map(|r| (r.name.clone(), r.oid))
803 .collect();
804 let caps: Vec<String> = disc.caps.iter().cloned().collect();
805 Ok((disc.protocol_version, refs, caps, disc.head_symref))
806}
807
808pub fn discover_upload_pack_for_list_refs(
810 client: &dyn HttpClient,
811 repo_url: &str,
812 git_protocol: Option<&str>,
813) -> Result<UploadPackDiscovery> {
814 let url = info_refs_url(repo_url);
815 let (body, _) = client.get_with_final_url_exact(&url, git_protocol)?;
816 discover_upload_pack_for_list_refs_from_body(&body)
817}
818
819pub fn discover_upload_pack(
823 client: &dyn HttpClient,
824 repo_url: &str,
825 git_protocol: Option<&str>,
826) -> Result<UploadPackDiscovery> {
827 let url = info_refs_url(repo_url);
828 let gp = git_protocol.or_else(|| client.git_protocol_header());
829 let body = client.get(&url, gp)?;
830 upload_pack_discovery_from_body(&body)
831}
832
833pub fn discover_upload_pack_exact(
835 client: &dyn HttpClient,
836 repo_url: &str,
837 git_protocol: Option<&str>,
838) -> Result<UploadPackDiscovery> {
839 let url = info_refs_url(repo_url);
840 let (body, _) = client.get_with_final_url_exact(&url, git_protocol)?;
841 upload_pack_discovery_from_body(&body)
842}
843
844#[must_use]
846pub fn smart_info_refs_discovery_url(repo_base: &str) -> String {
847 let base = repo_base.trim_end_matches('/');
848 let mut url = format!("{base}/info/refs");
849 url.push_str(if url.contains('?') { "&" } else { "?" });
850 url.push_str("service=");
851 url.push_str(UPLOAD_PACK);
852 url
853}
854
855#[must_use]
857pub fn repo_base_from_info_refs_url(info_refs_url: &str) -> Option<String> {
858 let path = info_refs_url.split('?').next()?;
859 let base = path.strip_suffix("/info/refs")?.trim_end_matches('/');
860 (!base.is_empty()).then(|| base.to_string())
861}
862
863#[must_use]
867pub fn effective_info_refs_url_after_redirect(
868 requested_info_refs_url: &str,
869 final_url: Option<&str>,
870) -> String {
871 let Some(original_base) = repo_base_from_info_refs_url(requested_info_refs_url) else {
872 return requested_info_refs_url.to_string();
873 };
874 match rebased_base_from_redirect(&original_base, final_url) {
875 Some(new_base) => smart_info_refs_discovery_url(&new_base),
876 None => requested_info_refs_url.to_string(),
877 }
878}
879
880#[must_use]
894pub fn rebased_base_from_redirect(original_base: &str, final_url: Option<&str>) -> Option<String> {
895 let final_url = final_url?;
896 let final_path = final_url.split('?').next().unwrap_or(final_url);
897 let new_base = final_path.strip_suffix("/info/refs")?.trim_end_matches('/');
898 if new_base.is_empty() || new_base == original_base.trim_end_matches('/') {
899 return None;
900 }
901 Some(new_base.to_owned())
902}
903
904#[must_use]
911pub fn http_origin_key(url: &str) -> Option<String> {
912 let parsed = url::Url::parse(url).ok()?;
913 let scheme = parsed.scheme().to_ascii_lowercase();
914 let host = parsed.host_str()?.to_ascii_lowercase();
915 let port = parsed.port_or_known_default()?;
916 Some(format!("{scheme}://{host}:{port}"))
917}
918
919#[must_use]
921pub fn http_origins_match(a: &str, b: &str) -> bool {
922 match (http_origin_key(a), http_origin_key(b)) {
923 (Some(left), Some(right)) => left == right,
924 _ => false,
925 }
926}
927
928pub struct SmartHttpTransport<C: HttpClient> {
935 client: C,
936}
937
938impl<C: HttpClient + Clone + 'static> SmartHttpTransport<C> {
939 pub fn new(client: C) -> Self {
941 Self { client }
942 }
943
944 pub fn client(&self) -> &C {
946 &self.client
947 }
948
949 pub fn push(
966 &self,
967 local_git_dir: &Path,
968 repo_url: &str,
969 refs: &[crate::transfer::PushRefSpec],
970 opts: &crate::transfer::PushOptions,
971 progress: &mut dyn Progress,
972 cancel: &dyn crate::progress::Cancel,
973 ) -> Result<crate::transfer::PushOutcome> {
974 use crate::push::push_remote;
975 use crate::transport::{ConnectOptions, Transport};
976 let connect = ConnectOptions {
977 protocol_version: 0,
978 diagnostics: opts.diagnostics.clone(),
979 network_trace: opts.network_trace,
980 ..Default::default()
981 };
982 let mut conn = self.connect(repo_url, Service::ReceivePack, &connect)?;
983 push_remote(local_git_dir, &mut *conn, refs, opts, progress, cancel)
984 }
985
986 fn discover_with_redirect(
994 &self,
995 repo_url: &str,
996 service: Service,
997 git_protocol: Option<&str>,
998 ) -> Result<(String, Discovery)> {
999 let info_url = info_refs_url_for_service(repo_url, service);
1000 let gp = git_protocol.or_else(|| self.client.git_protocol_header());
1001 let (body, final_url) = self.client.get_with_final_url(&info_url, gp)?;
1002 if let Some(new_base) = rebased_base_from_redirect(repo_url, final_url.as_deref()) {
1003 let _redirect = format!("http(s): redirected base {repo_url} -> {new_base}");
1004 self.client.reset_auth_after_redirect_rebase();
1005 let url = info_refs_url_for_service(&new_base, service);
1006 let body = self.client.get(&url, gp)?;
1007 let stripped = strip_service_advertisement_for_discovery(&body, None)?;
1008 let disc = parse_advertisement(stripped)?;
1009 return Ok((new_base, disc));
1010 }
1011 let stripped = strip_service_advertisement_for_discovery(&body, None)?;
1012 let disc = parse_advertisement(stripped)?;
1013 Ok((repo_url.to_owned(), disc))
1014 }
1015}
1016
1017fn info_refs_url_for_service(repo_url: &str, service: Service) -> String {
1018 let base = repo_url.trim_end_matches('/');
1019 let mut url = format!("{base}/info/refs");
1020 url.push_str(if url.contains('?') { "&" } else { "?" });
1021 url.push_str("service=");
1022 url.push_str(service.wire_name());
1023 url
1024}
1025
1026fn git_protocol_for_version(version: u8) -> Option<String> {
1029 if version >= 1 {
1030 Some(format!("version={version}"))
1031 } else {
1032 None
1033 }
1034}
1035
1036impl<C: HttpClient + Clone + 'static> Transport for SmartHttpTransport<C> {
1037 fn connect(
1038 &self,
1039 url: &str,
1040 service: Service,
1041 opts: &ConnectOptions,
1042 ) -> Result<Box<dyn Connection>> {
1043 crate::net_trace::net_trace!(
1044 opts.network_trace,
1045 opts.diagnostics.as_ref(),
1046 "http(s) discover {url} (service={}, request protocol v{})",
1047 service.wire_name(),
1048 opts.protocol_version
1049 );
1050 let gp = git_protocol_for_version(opts.protocol_version);
1051 let (repo_url, disc) = self.discover_with_redirect(url, service, gp.as_deref())?;
1052 let adv_refs: Vec<(String, ObjectId)> = disc
1053 .refs
1054 .iter()
1055 .filter(|r| r.name != "HEAD" && !r.name.ends_with("^{}"))
1056 .map(|r| (r.name.clone(), r.oid))
1057 .collect();
1058 let caps: Vec<String> = disc.caps.iter().cloned().collect();
1059 crate::net_trace::net_trace!(
1060 opts.network_trace,
1061 opts.diagnostics.as_ref(),
1062 "http(s) discovered: protocol v{}, {} ref(s) advertised",
1063 disc.protocol_version,
1064 adv_refs.len()
1065 );
1066 Ok(Box::new(StatelessHttpConnection::new(
1067 self.client.clone(),
1068 service,
1069 repo_url,
1070 disc.protocol_version,
1071 adv_refs,
1072 caps,
1073 disc.head_symref,
1074 disc.object_format,
1075 gp,
1076 )))
1077 }
1078}
1079
1080pub fn http_client_arc(client: impl HttpClient + 'static) -> std::sync::Arc<dyn HttpClient> {
1082 let boxed: Box<dyn HttpClient> = Box::new(client);
1083 std::sync::Arc::from(boxed)
1084}
1085
1086pub fn http_fetch(
1095 client: std::sync::Arc<dyn HttpClient>,
1096 local_git_dir: &Path,
1097 repo_url: &str,
1098 opts: &mut FetchOptions,
1099 progress: &mut dyn Progress,
1100) -> Result<FetchOutcome> {
1101 use crate::fetch::fetch_remote;
1102 use crate::transport::Transport;
1103 let protocol_version = client
1104 .git_protocol_header()
1105 .and_then(|h| h.strip_prefix("version="))
1106 .and_then(|v| v.parse::<u8>().ok())
1107 .unwrap_or(0);
1108 let connect = ConnectOptions {
1109 protocol_version,
1110 diagnostics: opts.diagnostics.clone(),
1111 network_trace: opts.network_trace,
1112 ..Default::default()
1113 };
1114 let transport = SmartHttpTransport::new(client);
1115 let mut conn = transport.connect(repo_url, Service::UploadPack, &connect)?;
1116 fetch_remote(
1117 local_git_dir,
1118 &mut *conn,
1119 opts,
1120 progress,
1121 &crate::progress::NeverCancel,
1122 )
1123}
1124
1125#[cfg(test)]
1126#[path = "http_discovery_tests.rs"]
1127mod discovery_tests;