Skip to main content

git_internal/protocol/
http.rs

1//! HTTP transport adapter that wires request parsing and content negotiation onto the generic
2//! `GitProtocol`, exposing helpers for info/refs, upload-pack, and receive-pack endpoints.
3
4use std::collections::HashMap;
5
6/// HTTP transport adapter for Git protocol
7///
8/// This module provides HTTP-specific handling for Git smart protocol operations.
9/// It's a thin wrapper around the core GitProtocol that handles HTTP-specific
10/// request/response formatting and uses the utility functions for proper HTTP handling.
11use serde::Deserialize;
12
13use super::{
14    core::{AuthenticationService, GitProtocol, RepositoryAccess},
15    types::{ProtocolError, ProtocolStream},
16};
17
18/// HTTP Git protocol handler
19pub struct HttpGitHandler<R: RepositoryAccess, A: AuthenticationService> {
20    protocol: GitProtocol<R, A>,
21}
22
23impl<R: RepositoryAccess, A: AuthenticationService> HttpGitHandler<R, A> {
24    /// Create a new HTTP Git handler
25    pub fn new(repo_access: R, auth_service: A) -> Self {
26        let mut protocol = GitProtocol::new(repo_access, auth_service);
27        protocol.set_transport(super::types::TransportProtocol::Http);
28        Self { protocol }
29    }
30
31    /// Authenticate the HTTP request using provided headers
32    /// Call this before invoking handle_* methods if your server requires auth
33    pub async fn authenticate_http(
34        &self,
35        headers: &HashMap<String, String>,
36    ) -> Result<(), ProtocolError> {
37        self.protocol.authenticate_http(headers).await
38    }
39
40    /// Handle HTTP info/refs request
41    ///
42    /// Processes GET requests to /{repo}/info/refs?service=git-{service}
43    /// Uses extract_repo_path and get_service_from_query for proper parsing
44    pub async fn handle_info_refs(
45        &mut self,
46        request_path: &str,
47        query: &str,
48    ) -> Result<(Vec<u8>, &'static str), ProtocolError> {
49        // Validate repository path exists in request
50        extract_repo_path(request_path)
51            .ok_or_else(|| ProtocolError::InvalidRequest("Invalid repository path".to_string()))?;
52
53        // Get service from query parameters
54        let service = get_service_from_query(query).ok_or_else(|| {
55            ProtocolError::InvalidRequest("Missing service parameter".to_string())
56        })?;
57
58        // Validate it's a Git request
59        if !is_git_request(request_path) {
60            return Err(ProtocolError::InvalidRequest(
61                "Not a Git request".to_string(),
62            ));
63        }
64
65        let response_data = self.protocol.info_refs(service).await?;
66        let content_type = get_advertisement_content_type(service);
67
68        Ok((response_data, content_type))
69    }
70
71    /// Handle HTTP upload-pack request
72    ///
73    /// Processes POST requests to /{repo}/git-upload-pack
74    pub async fn handle_upload_pack(
75        &mut self,
76        request_path: &str,
77        request_body: &[u8],
78    ) -> Result<(ProtocolStream, &'static str), ProtocolError> {
79        // Validate repository path exists in request
80        extract_repo_path(request_path)
81            .ok_or_else(|| ProtocolError::InvalidRequest("Invalid repository path".to_string()))?;
82
83        // Validate it's a Git request
84        if !is_git_request(request_path) {
85            return Err(ProtocolError::InvalidRequest(
86                "Not a Git request".to_string(),
87            ));
88        }
89
90        let response_stream = self.protocol.upload_pack(request_body).await?;
91        let content_type = get_content_type("git-upload-pack");
92
93        Ok((response_stream, content_type))
94    }
95
96    /// Handle HTTP receive-pack request
97    ///
98    /// Processes POST requests to /{repo}/git-receive-pack
99    pub async fn handle_receive_pack(
100        &mut self,
101        request_path: &str,
102        request_stream: ProtocolStream,
103    ) -> Result<(ProtocolStream, &'static str), ProtocolError> {
104        // Validate repository path exists in request
105        extract_repo_path(request_path)
106            .ok_or_else(|| ProtocolError::InvalidRequest("Invalid repository path".to_string()))?;
107
108        // Validate it's a Git request
109        if !is_git_request(request_path) {
110            return Err(ProtocolError::InvalidRequest(
111                "Not a Git request".to_string(),
112            ));
113        }
114
115        let response_stream = self.protocol.receive_pack(request_stream).await?;
116        let content_type = get_content_type("git-receive-pack");
117
118        Ok((response_stream, content_type))
119    }
120}
121
122/// HTTP-specific utility functions
123/// Get content type for Git HTTP responses
124pub fn get_content_type(service: &str) -> &'static str {
125    match service {
126        "git-upload-pack" => "application/x-git-upload-pack-result",
127        "git-receive-pack" => "application/x-git-receive-pack-result",
128        _ => "application/x-git-upload-pack-advertisement",
129    }
130}
131
132/// Get content type for Git HTTP info/refs advertisement
133pub fn get_advertisement_content_type(service: &str) -> &'static str {
134    match service {
135        "git-upload-pack" => "application/x-git-upload-pack-advertisement",
136        "git-receive-pack" => "application/x-git-receive-pack-advertisement",
137        _ => "application/x-git-upload-pack-advertisement",
138    }
139}
140
141/// Check if request is a Git smart protocol request
142pub fn is_git_request(path: &str) -> bool {
143    path.ends_with("/info/refs")
144        || path.ends_with("/git-upload-pack")
145        || path.ends_with("/git-receive-pack")
146}
147
148/// Extract repository path from HTTP request path
149pub fn extract_repo_path(path: &str) -> Option<&str> {
150    if let Some(pos) = path.rfind("/info/refs") {
151        Some(&path[..pos])
152    } else if let Some(pos) = path.rfind("/git-upload-pack") {
153        Some(&path[..pos])
154    } else if let Some(pos) = path.rfind("/git-receive-pack") {
155        Some(&path[..pos])
156    } else {
157        None
158    }
159}
160
161/// Get Git service from query parameters
162pub fn get_service_from_query(query: &str) -> Option<&str> {
163    for param in query.split('&') {
164        if let Some(("service", value)) = param.split_once('=') {
165            return Some(value);
166        }
167    }
168    None
169}
170
171/// Parameters for git info-refs request
172#[derive(Debug, Deserialize)]
173pub struct InfoRefsParams {
174    pub service: String,
175}
176
177#[cfg(test)]
178mod tests {
179    use async_trait::async_trait;
180
181    use super::*;
182    use crate::protocol::core::{AuthenticationService, RepositoryAccess};
183
184    /// Mock repository access for testing
185    #[derive(Clone)]
186    struct MockRepo;
187
188    #[async_trait]
189    impl RepositoryAccess for MockRepo {
190        async fn get_repository_refs(&self) -> Result<Vec<(String, String)>, ProtocolError> {
191            Ok(vec![("refs/heads/main".into(), "0".repeat(40))])
192        }
193        async fn has_object(&self, _object_hash: &str) -> Result<bool, ProtocolError> {
194            Ok(false)
195        }
196        async fn get_object(&self, _object_hash: &str) -> Result<Vec<u8>, ProtocolError> {
197            Ok(Vec::new())
198        }
199        async fn store_pack_data(&self, _pack_data: &[u8]) -> Result<(), ProtocolError> {
200            Ok(())
201        }
202        async fn update_reference(
203            &self,
204            _ref_name: &str,
205            _old_hash: Option<&str>,
206            _new_hash: &str,
207        ) -> Result<(), ProtocolError> {
208            Ok(())
209        }
210        async fn get_objects_for_pack(
211            &self,
212            _wants: &[String],
213            _haves: &[String],
214        ) -> Result<Vec<String>, ProtocolError> {
215            Ok(Vec::new())
216        }
217        async fn has_default_branch(&self) -> Result<bool, ProtocolError> {
218            Ok(false)
219        }
220        async fn post_receive_hook(&self) -> Result<(), ProtocolError> {
221            Ok(())
222        }
223    }
224
225    struct MockAuth;
226    #[async_trait]
227    impl AuthenticationService for MockAuth {
228        async fn authenticate_http(
229            &self,
230            _headers: &std::collections::HashMap<String, String>,
231        ) -> Result<(), ProtocolError> {
232            Ok(())
233        }
234        async fn authenticate_ssh(
235            &self,
236            _username: &str,
237            _public_key: &[u8],
238        ) -> Result<(), ProtocolError> {
239            Ok(())
240        }
241    }
242
243    /// Helper to create HttpGitHandler with mock repo and auth
244    fn make_handler() -> HttpGitHandler<MockRepo, MockAuth> {
245        HttpGitHandler::new(MockRepo, MockAuth)
246    }
247
248    /// extract_repo_path should strip known suffixes.
249    #[test]
250    fn extract_repo_path_variants() {
251        assert_eq!(extract_repo_path("/repo/info/refs"), Some("/repo"));
252        assert_eq!(extract_repo_path("/repo/git-upload-pack"), Some("/repo"));
253        assert_eq!(extract_repo_path("/repo/git-receive-pack"), Some("/repo"));
254        assert!(extract_repo_path("/repo/other").is_none());
255    }
256
257    /// get_service_from_query should return value when present.
258    #[test]
259    fn parse_service_from_query() {
260        assert_eq!(
261            get_service_from_query("service=git-upload-pack"),
262            Some("git-upload-pack")
263        );
264        assert_eq!(
265            get_service_from_query("foo=bar&service=git-receive-pack"),
266            Some("git-receive-pack")
267        );
268        assert!(get_service_from_query("foo=bar").is_none());
269    }
270
271    /// is_git_request should recognize Git smart protocol endpoints.
272    #[test]
273    fn detect_git_request() {
274        assert!(is_git_request("/repo/info/refs"));
275        assert!(is_git_request("/repo/git-upload-pack"));
276        assert!(is_git_request("/repo/git-receive-pack"));
277        assert!(!is_git_request("/repo/other"));
278    }
279
280    /// handle_info_refs should succeed on valid path/service and return content-type.
281    #[tokio::test]
282    async fn handle_info_refs_ok() {
283        let mut handler = make_handler();
284        let (data, content_type) = handler
285            .handle_info_refs("/repo/info/refs", "service=git-upload-pack")
286            .await
287            .expect("info_refs");
288        assert!(!data.is_empty());
289        assert_eq!(
290            content_type,
291            get_advertisement_content_type("git-upload-pack")
292        );
293    }
294
295    /// Missing service param should error.
296    #[tokio::test]
297    async fn handle_info_refs_missing_service_errors() {
298        let mut handler = make_handler();
299        let err = handler
300            .handle_info_refs("/repo/info/refs", "")
301            .await
302            .unwrap_err();
303        assert!(matches!(err, ProtocolError::InvalidRequest(_)));
304    }
305
306    /// Non-git paths should error.
307    #[tokio::test]
308    async fn handle_info_refs_non_git_path_errors() {
309        let mut handler = make_handler();
310        let err = handler
311            .handle_info_refs("/repo/other", "service=git-upload-pack")
312            .await
313            .unwrap_err();
314        assert!(matches!(err, ProtocolError::InvalidRequest(_)));
315    }
316}