1use std::collections::HashMap;
5
6use serde::Deserialize;
12
13use super::{
14 core::{AuthenticationService, GitProtocol, RepositoryAccess},
15 types::{ProtocolError, ProtocolStream},
16};
17
18pub struct HttpGitHandler<R: RepositoryAccess, A: AuthenticationService> {
20 protocol: GitProtocol<R, A>,
21}
22
23impl<R: RepositoryAccess, A: AuthenticationService> HttpGitHandler<R, A> {
24 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 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 pub async fn handle_info_refs(
45 &mut self,
46 request_path: &str,
47 query: &str,
48 ) -> Result<(Vec<u8>, &'static str), ProtocolError> {
49 extract_repo_path(request_path)
51 .ok_or_else(|| ProtocolError::InvalidRequest("Invalid repository path".to_string()))?;
52
53 let service = get_service_from_query(query).ok_or_else(|| {
55 ProtocolError::InvalidRequest("Missing service parameter".to_string())
56 })?;
57
58 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 pub async fn handle_upload_pack(
75 &mut self,
76 request_path: &str,
77 request_body: &[u8],
78 ) -> Result<(ProtocolStream, &'static str), ProtocolError> {
79 extract_repo_path(request_path)
81 .ok_or_else(|| ProtocolError::InvalidRequest("Invalid repository path".to_string()))?;
82
83 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 pub async fn handle_receive_pack(
100 &mut self,
101 request_path: &str,
102 request_stream: ProtocolStream,
103 ) -> Result<(ProtocolStream, &'static str), ProtocolError> {
104 extract_repo_path(request_path)
106 .ok_or_else(|| ProtocolError::InvalidRequest("Invalid repository path".to_string()))?;
107
108 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
122pub 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
132pub 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
141pub 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
148pub 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
161pub 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#[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 #[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 fn make_handler() -> HttpGitHandler<MockRepo, MockAuth> {
244 HttpGitHandler::new(MockRepo, MockAuth)
245 }
246
247 #[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 #[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 #[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 #[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 #[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 #[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}