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 async_trait::async_trait;
180
181 use super::*;
182 use crate::protocol::core::{AuthenticationService, RepositoryAccess};
183
184 #[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 fn make_handler() -> HttpGitHandler<MockRepo, MockAuth> {
245 HttpGitHandler::new(MockRepo, MockAuth)
246 }
247
248 #[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 #[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 #[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 #[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 #[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 #[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}