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 super::*;
180    use crate::protocol::core::{AuthenticationService, RepositoryAccess};
181    use async_trait::async_trait;
182
183    /// Mock repository access for testing
184    #[derive(Clone)]
185    struct MockRepo;
186
187    #[async_trait]
188    impl RepositoryAccess for MockRepo {
189        async fn get_repository_refs(&self) -> Result<Vec<(String, String)>, ProtocolError> {
190            Ok(vec![("refs/heads/main".into(), "0".repeat(40))])
191        }
192        async fn has_object(&self, _object_hash: &str) -> Result<bool, ProtocolError> {
193            Ok(false)
194        }
195        async fn get_object(&self, _object_hash: &str) -> Result<Vec<u8>, ProtocolError> {
196            Ok(Vec::new())
197        }
198        async fn store_pack_data(&self, _pack_data: &[u8]) -> Result<(), ProtocolError> {
199            Ok(())
200        }
201        async fn update_reference(
202            &self,
203            _ref_name: &str,
204            _old_hash: Option<&str>,
205            _new_hash: &str,
206        ) -> Result<(), ProtocolError> {
207            Ok(())
208        }
209        async fn get_objects_for_pack(
210            &self,
211            _wants: &[String],
212            _haves: &[String],
213        ) -> Result<Vec<String>, ProtocolError> {
214            Ok(Vec::new())
215        }
216        async fn has_default_branch(&self) -> Result<bool, ProtocolError> {
217            Ok(false)
218        }
219        async fn post_receive_hook(&self) -> Result<(), ProtocolError> {
220            Ok(())
221        }
222    }
223
224    struct MockAuth;
225    #[async_trait]
226    impl AuthenticationService for MockAuth {
227        async fn authenticate_http(
228            &self,
229            _headers: &std::collections::HashMap<String, String>,
230        ) -> Result<(), ProtocolError> {
231            Ok(())
232        }
233        async fn authenticate_ssh(
234            &self,
235            _username: &str,
236            _public_key: &[u8],
237        ) -> Result<(), ProtocolError> {
238            Ok(())
239        }
240    }
241
242    /// Helper to create HttpGitHandler with mock repo and auth
243    fn make_handler() -> HttpGitHandler<MockRepo, MockAuth> {
244        HttpGitHandler::new(MockRepo, MockAuth)
245    }
246
247    /// extract_repo_path should strip known suffixes.
248    #[test]
249    fn extract_repo_path_variants() {
250        assert_eq!(extract_repo_path("/repo/info/refs"), Some("/repo"));
251        assert_eq!(extract_repo_path("/repo/git-upload-pack"), Some("/repo"));
252        assert_eq!(extract_repo_path("/repo/git-receive-pack"), Some("/repo"));
253        assert!(extract_repo_path("/repo/other").is_none());
254    }
255
256    /// get_service_from_query should return value when present.
257    #[test]
258    fn parse_service_from_query() {
259        assert_eq!(
260            get_service_from_query("service=git-upload-pack"),
261            Some("git-upload-pack")
262        );
263        assert_eq!(
264            get_service_from_query("foo=bar&service=git-receive-pack"),
265            Some("git-receive-pack")
266        );
267        assert!(get_service_from_query("foo=bar").is_none());
268    }
269
270    /// is_git_request should recognize Git smart protocol endpoints.
271    #[test]
272    fn detect_git_request() {
273        assert!(is_git_request("/repo/info/refs"));
274        assert!(is_git_request("/repo/git-upload-pack"));
275        assert!(is_git_request("/repo/git-receive-pack"));
276        assert!(!is_git_request("/repo/other"));
277    }
278
279    /// handle_info_refs should succeed on valid path/service and return content-type.
280    #[tokio::test]
281    async fn handle_info_refs_ok() {
282        let mut handler = make_handler();
283        let (data, content_type) = handler
284            .handle_info_refs("/repo/info/refs", "service=git-upload-pack")
285            .await
286            .expect("info_refs");
287        assert!(!data.is_empty());
288        assert_eq!(
289            content_type,
290            get_advertisement_content_type("git-upload-pack")
291        );
292    }
293
294    /// Missing service param should error.
295    #[tokio::test]
296    async fn handle_info_refs_missing_service_errors() {
297        let mut handler = make_handler();
298        let err = handler
299            .handle_info_refs("/repo/info/refs", "")
300            .await
301            .unwrap_err();
302        assert!(matches!(err, ProtocolError::InvalidRequest(_)));
303    }
304
305    /// Non-git paths should error.
306    #[tokio::test]
307    async fn handle_info_refs_non_git_path_errors() {
308        let mut handler = make_handler();
309        let err = handler
310            .handle_info_refs("/repo/other", "service=git-upload-pack")
311            .await
312            .unwrap_err();
313        assert!(matches!(err, ProtocolError::InvalidRequest(_)));
314    }
315}