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 pkt_line::reject_err_line(&first)?;
651 if first.trim_end() == "version 2" {
652 let mut caps = HashSet::new();
654 loop {
655 match pkt_line::read_packet(&mut cur)? {
656 None | Some(pkt_line::Packet::Flush) => break,
657 Some(pkt_line::Packet::Data(s)) => {
658 pkt_line::reject_err_line(&s)?;
659 caps.insert(s.trim_end().to_owned());
660 }
661 Some(_) => break,
662 }
663 }
664 let object_format = caps
665 .iter()
666 .find_map(|c| c.strip_prefix("object-format="))
667 .unwrap_or("sha1")
668 .to_owned();
669 return Ok(Discovery {
670 protocol_version: 2,
671 refs: Vec::new(),
672 caps,
673 head_symref: None,
674 object_format,
675 });
676 }
677
678 cur.set_position(0);
680 let mut refs = Vec::new();
681 let mut caps: HashSet<String> = HashSet::new();
682 let mut head_symref = None;
683 let mut first_ref_line = true;
684 loop {
685 match pkt_line::read_packet(&mut cur)? {
686 None | Some(pkt_line::Packet::Flush) => break,
687 Some(pkt_line::Packet::Data(line)) => {
688 let line = line.trim_end_matches('\n');
689 pkt_line::reject_err_line(line)?;
690 if line.starts_with("version ") {
691 continue;
692 }
693 if line.starts_with("shallow ") || line.starts_with("unshallow ") {
694 continue;
695 }
696 let (payload, cap_part) = match line.split_once('\0') {
697 Some((p, c)) => (p.trim(), Some(c)),
698 None => (line.trim(), None),
699 };
700 let Some((oid_hex, refname)) =
701 payload.split_once('\t').or_else(|| payload.split_once(' '))
702 else {
703 continue;
704 };
705 let oid_hex = oid_hex.trim();
706 let refname = refname.trim();
707 if first_ref_line {
708 if let Some(raw_caps) = cap_part {
709 for cap in raw_caps.split_whitespace() {
710 if let Some(target) = cap.strip_prefix("symref=HEAD:") {
711 if crate::refs::is_valid_advertised_symref_target(target) {
712 head_symref = Some(target.to_owned());
713 }
714 }
715 caps.insert(cap.to_owned());
716 }
717 }
718 first_ref_line = false;
719 }
720 if refname.is_empty() {
721 continue;
722 }
723 if oid_hex.bytes().all(|b| b == b'0') {
725 continue;
726 }
727 let oid = ObjectId::from_hex(oid_hex).map_err(|e| {
728 Error::Message(format!("bad oid in advertisement: {oid_hex}: {e}"))
729 })?;
730 let valid_ref = refname == "HEAD"
731 || crate::refs::is_valid_fetch_advertised_ref(refname)
732 || refname
733 .strip_suffix("^{}")
734 .is_some_and(crate::refs::is_valid_fetch_advertised_ref);
735 if valid_ref {
736 refs.push(AdvRef {
737 name: refname.to_owned(),
738 oid,
739 });
740 }
741 }
742 Some(other) => {
743 return Err(Error::Message(format!(
744 "unexpected packet in advertisement: {other:?}"
745 )))
746 }
747 }
748 }
749 let object_format = caps
750 .iter()
751 .find_map(|c| c.strip_prefix("object-format="))
752 .unwrap_or("sha1")
753 .to_owned();
754 Ok(Discovery {
755 protocol_version: if caps.contains("version 1") { 1 } else { 0 },
756 refs,
757 caps,
758 head_symref,
759 object_format,
760 })
761}
762
763fn info_refs_url(repo_url: &str) -> String {
765 smart_info_refs_discovery_url(repo_url)
766}
767
768pub type UploadPackDiscovery = (u8, Vec<(String, ObjectId)>, Vec<String>, Option<String>);
770
771fn upload_pack_discovery_from_body(body: &[u8]) -> Result<UploadPackDiscovery> {
772 upload_pack_discovery_from_body_with_http(body, None)
773}
774
775fn upload_pack_discovery_from_body_with_http(
776 body: &[u8],
777 http: Option<&InfoRefsDiscoveryHttp>,
778) -> Result<UploadPackDiscovery> {
779 let stripped = strip_service_advertisement_for_discovery(body, http)?;
780 let disc = parse_advertisement(stripped)?;
781 let refs: Vec<(String, ObjectId)> = disc
782 .refs
783 .iter()
784 .filter(|r| r.name != "HEAD" && !r.name.ends_with("^{}"))
785 .map(|r| (r.name.clone(), r.oid))
786 .collect();
787 let caps: Vec<String> = disc.caps.iter().cloned().collect();
788 Ok((disc.protocol_version, refs, caps, disc.head_symref))
789}
790
791pub fn discover_upload_pack_from_body(body: &[u8]) -> Result<UploadPackDiscovery> {
798 upload_pack_discovery_from_body(body)
799}
800
801pub fn discover_upload_pack_for_list_refs_from_body(body: &[u8]) -> Result<UploadPackDiscovery> {
804 let stripped = strip_service_advertisement_for_discovery(body, None)?;
805 let disc = parse_advertisement(stripped)?;
806 let refs: Vec<(String, ObjectId)> = disc
807 .refs
808 .iter()
809 .filter(|r| r.name != "HEAD")
810 .map(|r| (r.name.clone(), r.oid))
811 .collect();
812 let caps: Vec<String> = disc.caps.iter().cloned().collect();
813 Ok((disc.protocol_version, refs, caps, disc.head_symref))
814}
815
816pub fn discover_upload_pack_for_list_refs(
818 client: &dyn HttpClient,
819 repo_url: &str,
820 git_protocol: Option<&str>,
821) -> Result<UploadPackDiscovery> {
822 let url = info_refs_url(repo_url);
823 let (body, _) = client.get_with_final_url_exact(&url, git_protocol)?;
824 discover_upload_pack_for_list_refs_from_body(&body)
825}
826
827pub fn discover_upload_pack(
831 client: &dyn HttpClient,
832 repo_url: &str,
833 git_protocol: Option<&str>,
834) -> Result<UploadPackDiscovery> {
835 let url = info_refs_url(repo_url);
836 let gp = git_protocol.or_else(|| client.git_protocol_header());
837 let body = client.get(&url, gp)?;
838 upload_pack_discovery_from_body(&body)
839}
840
841pub fn discover_upload_pack_exact(
843 client: &dyn HttpClient,
844 repo_url: &str,
845 git_protocol: Option<&str>,
846) -> Result<UploadPackDiscovery> {
847 let url = info_refs_url(repo_url);
848 let (body, _) = client.get_with_final_url_exact(&url, git_protocol)?;
849 upload_pack_discovery_from_body(&body)
850}
851
852#[must_use]
854pub fn smart_info_refs_discovery_url(repo_base: &str) -> String {
855 let base = repo_base.trim_end_matches('/');
856 let mut url = format!("{base}/info/refs");
857 url.push_str(if url.contains('?') { "&" } else { "?" });
858 url.push_str("service=");
859 url.push_str(UPLOAD_PACK);
860 url
861}
862
863#[must_use]
865pub fn repo_base_from_info_refs_url(info_refs_url: &str) -> Option<String> {
866 let path = info_refs_url.split('?').next()?;
867 let base = path.strip_suffix("/info/refs")?.trim_end_matches('/');
868 (!base.is_empty()).then(|| base.to_string())
869}
870
871#[must_use]
875pub fn effective_info_refs_url_after_redirect(
876 requested_info_refs_url: &str,
877 final_url: Option<&str>,
878) -> String {
879 let Some(original_base) = repo_base_from_info_refs_url(requested_info_refs_url) else {
880 return requested_info_refs_url.to_string();
881 };
882 match rebased_base_from_redirect(&original_base, final_url) {
883 Some(new_base) => smart_info_refs_discovery_url(&new_base),
884 None => requested_info_refs_url.to_string(),
885 }
886}
887
888#[must_use]
902pub fn rebased_base_from_redirect(original_base: &str, final_url: Option<&str>) -> Option<String> {
903 let final_url = final_url?;
904 let final_path = final_url.split('?').next().unwrap_or(final_url);
905 let new_base = final_path.strip_suffix("/info/refs")?.trim_end_matches('/');
906 if new_base.is_empty() || new_base == original_base.trim_end_matches('/') {
907 return None;
908 }
909 Some(new_base.to_owned())
910}
911
912#[must_use]
919pub fn http_origin_key(url: &str) -> Option<String> {
920 let parsed = url::Url::parse(url).ok()?;
921 let scheme = parsed.scheme().to_ascii_lowercase();
922 let host = parsed.host_str()?.to_ascii_lowercase();
923 let port = parsed.port_or_known_default()?;
924 Some(format!("{scheme}://{host}:{port}"))
925}
926
927#[must_use]
929pub fn http_origins_match(a: &str, b: &str) -> bool {
930 match (http_origin_key(a), http_origin_key(b)) {
931 (Some(left), Some(right)) => left == right,
932 _ => false,
933 }
934}
935
936pub struct SmartHttpTransport<C: HttpClient> {
943 client: C,
944}
945
946impl<C: HttpClient + Clone + 'static> SmartHttpTransport<C> {
947 pub fn new(client: C) -> Self {
949 Self { client }
950 }
951
952 pub fn client(&self) -> &C {
954 &self.client
955 }
956
957 pub fn push(
974 &self,
975 local_git_dir: &Path,
976 repo_url: &str,
977 refs: &[crate::transfer::PushRefSpec],
978 opts: &crate::transfer::PushOptions,
979 progress: &mut dyn Progress,
980 cancel: &dyn crate::progress::Cancel,
981 ) -> Result<crate::transfer::PushOutcome> {
982 use crate::push::push_remote;
983 use crate::transport::{ConnectOptions, Transport};
984 let connect = ConnectOptions {
985 protocol_version: 0,
986 diagnostics: opts.diagnostics.clone(),
987 network_trace: opts.network_trace,
988 ..Default::default()
989 };
990 let mut conn = self.connect(repo_url, Service::ReceivePack, &connect)?;
991 push_remote(local_git_dir, &mut *conn, refs, opts, progress, cancel)
992 }
993
994 fn discover_with_redirect(
1002 &self,
1003 repo_url: &str,
1004 service: Service,
1005 git_protocol: Option<&str>,
1006 ) -> Result<(String, Discovery)> {
1007 let info_url = info_refs_url_for_service(repo_url, service);
1008 let gp = git_protocol.or_else(|| self.client.git_protocol_header());
1009 let (body, final_url) = self.client.get_with_final_url(&info_url, gp)?;
1010 if let Some(new_base) = rebased_base_from_redirect(repo_url, final_url.as_deref()) {
1011 let _redirect = format!("http(s): redirected base {repo_url} -> {new_base}");
1012 self.client.reset_auth_after_redirect_rebase();
1013 let url = info_refs_url_for_service(&new_base, service);
1014 let body = self.client.get(&url, gp)?;
1015 let stripped = strip_service_advertisement_for_discovery(&body, None)?;
1016 let disc = parse_advertisement(stripped)?;
1017 return Ok((new_base, disc));
1018 }
1019 let stripped = strip_service_advertisement_for_discovery(&body, None)?;
1020 let disc = parse_advertisement(stripped)?;
1021 Ok((repo_url.to_owned(), disc))
1022 }
1023}
1024
1025fn info_refs_url_for_service(repo_url: &str, service: Service) -> String {
1026 let base = repo_url.trim_end_matches('/');
1027 let mut url = format!("{base}/info/refs");
1028 url.push_str(if url.contains('?') { "&" } else { "?" });
1029 url.push_str("service=");
1030 url.push_str(service.wire_name());
1031 url
1032}
1033
1034fn git_protocol_for_version(version: u8) -> Option<String> {
1037 if version >= 1 {
1038 Some(format!("version={version}"))
1039 } else {
1040 None
1041 }
1042}
1043
1044impl<C: HttpClient + Clone + 'static> Transport for SmartHttpTransport<C> {
1045 fn connect(
1046 &self,
1047 url: &str,
1048 service: Service,
1049 opts: &ConnectOptions,
1050 ) -> Result<Box<dyn Connection>> {
1051 crate::net_trace::net_trace!(
1052 opts.network_trace,
1053 opts.diagnostics.as_ref(),
1054 "http(s) discover {url} (service={}, request protocol v{})",
1055 service.wire_name(),
1056 opts.protocol_version
1057 );
1058 let gp = git_protocol_for_version(opts.protocol_version);
1059 let (repo_url, disc) = self.discover_with_redirect(url, service, gp.as_deref())?;
1060 let adv_refs: Vec<(String, ObjectId)> = disc
1061 .refs
1062 .iter()
1063 .filter(|r| r.name != "HEAD" && !r.name.ends_with("^{}"))
1064 .map(|r| (r.name.clone(), r.oid))
1065 .collect();
1066 let caps: Vec<String> = disc.caps.iter().cloned().collect();
1067 crate::net_trace::net_trace!(
1068 opts.network_trace,
1069 opts.diagnostics.as_ref(),
1070 "http(s) discovered: protocol v{}, {} ref(s) advertised",
1071 disc.protocol_version,
1072 adv_refs.len()
1073 );
1074 Ok(Box::new(StatelessHttpConnection::new(
1075 self.client.clone(),
1076 service,
1077 repo_url,
1078 disc.protocol_version,
1079 adv_refs,
1080 caps,
1081 disc.head_symref,
1082 disc.object_format,
1083 gp,
1084 )))
1085 }
1086}
1087
1088pub fn http_client_arc(client: impl HttpClient + 'static) -> std::sync::Arc<dyn HttpClient> {
1090 let boxed: Box<dyn HttpClient> = Box::new(client);
1091 std::sync::Arc::from(boxed)
1092}
1093
1094pub fn http_fetch(
1103 client: std::sync::Arc<dyn HttpClient>,
1104 local_git_dir: &Path,
1105 repo_url: &str,
1106 opts: &mut FetchOptions,
1107 progress: &mut dyn Progress,
1108) -> Result<FetchOutcome> {
1109 use crate::fetch::fetch_remote;
1110 use crate::transport::Transport;
1111 let protocol_version = client
1112 .git_protocol_header()
1113 .and_then(|h| h.strip_prefix("version="))
1114 .and_then(|v| v.parse::<u8>().ok())
1115 .unwrap_or(0);
1116 let connect = ConnectOptions {
1117 protocol_version,
1118 diagnostics: opts.diagnostics.clone(),
1119 network_trace: opts.network_trace,
1120 ..Default::default()
1121 };
1122 let transport = SmartHttpTransport::new(client);
1123 let mut conn = transport.connect(repo_url, Service::UploadPack, &connect)?;
1124 fetch_remote(
1125 local_git_dir,
1126 &mut *conn,
1127 opts,
1128 progress,
1129 &crate::progress::NeverCancel,
1130 )
1131}
1132
1133#[cfg(test)]
1134#[path = "http_discovery_tests.rs"]
1135mod discovery_tests;