@@ -5,18 +5,18 @@ use std::sync::Arc;
55
66use anyhow:: Result ;
77use axum:: body:: Body ;
8- use axum:: extract:: { Query , State } ;
8+ use axum:: extract:: FromRef ;
99use axum:: http:: { self , Request , Uri } ;
1010use axum:: response:: Response ;
11- use axum:: routing:: get ;
12- use axum:: { Router , middleware} ;
11+ use axum:: routing:: any ;
12+ use axum:: { Router , ServiceExt , middleware} ;
1313use ceres:: api_service:: cache:: GitObjectCache ;
1414use ceres:: api_service:: state:: ProtocolApiState ;
1515use http:: { HeaderValue , Method } ;
16- use lazy_static:: lazy_static;
17- use regex:: Regex ;
16+
1817use saturn:: entitystore:: EntityStore ;
1918use time:: Duration ;
19+ use tower:: Layer ;
2020use tower:: ServiceBuilder ;
2121use tower_http:: cors:: CorsLayer ;
2222use tower_http:: decompression:: RequestDecompressionLayer ;
@@ -39,22 +39,24 @@ use crate::api::oauth::oauth_client;
3939use crate :: api:: router:: lfs_router;
4040use context:: AppContext ;
4141
42- pub fn remove_git_suffix ( uri : Uri , git_suffix : & str ) -> PathBuf {
43- PathBuf :: from ( uri . path ( ) . replace ( ".git" , "" ) . replace ( git_suffix, "" ) )
42+ pub fn remove_git_suffix ( full_path : & str , git_suffix : & str ) -> PathBuf {
43+ PathBuf :: from ( full_path . replace ( ".git" , "" ) . replace ( git_suffix, "" ) )
4444}
4545
4646pub async fn start_http ( ctx : AppContext , options : CommonHttpOptions ) {
4747 let CommonHttpOptions { host, port } = options. clone ( ) ;
4848
49+ let middleware = tower:: util:: MapRequestLayer :: new ( rewrite_lfs_request_uri :: < Body > ) ;
4950 let app = app ( ctx, host. clone ( ) , port) . await ;
51+ let app_with_middleware = middleware. layer ( app) ;
5052
5153 let server_url = format ! ( "{host}:{port}" ) ;
5254
5355 let addr = SocketAddr :: from_str ( & server_url) . unwrap ( ) ;
5456 tracing:: info!( "HTTP server started up!" ) ;
5557
5658 let listener = tokio:: net:: TcpListener :: bind ( addr) . await . unwrap ( ) ;
57- axum:: serve ( listener, app . into_make_service ( ) )
59+ axum:: serve ( listener, app_with_middleware . into_make_service ( ) )
5860 . await
5961 . unwrap ( ) ;
6062}
@@ -97,10 +99,6 @@ pub async fn app(ctx: AppContext, host: String, port: u16) -> Router {
9799 connection : ctx. connection . clone ( ) ,
98100 prefix : "git-object-bincode" . to_string ( ) ,
99101 } ) ;
100- let state = ProtocolApiState {
101- storage : storage. clone ( ) ,
102- git_object_cache : git_object_cache. clone ( ) ,
103- } ;
104102
105103 let api_state = MonoApiServiceState {
106104 storage : storage. clone ( ) ,
@@ -135,11 +133,22 @@ pub async fn app(ctx: AppContext, host: String, port: u16) -> Router {
135133 "/api/v1" ,
136134 api_router:: routers ( )
137135 . with_state ( api_state. clone ( ) )
138- . route_layer ( middleware:: from_fn_with_state ( api_state, cedar_guard) ) ,
136+ . route_layer ( middleware:: from_fn_with_state (
137+ api_state. clone ( ) ,
138+ cedar_guard,
139+ ) ) ,
139140 )
140141 // .nest("/auth", oauth::routers().with_state(api_state.clone()))
141142 // Using Regular Expressions for Path Matching in Protocol
142- . route ( "/{*path}" , get ( get_method_router) . post ( post_method_router) )
143+ . route (
144+ "/{*path}" ,
145+ any ( {
146+ let api_state = api_state. clone ( ) ;
147+ move |req : Request < Body > | {
148+ handle_smart_protocol ( req, Arc :: new ( ProtocolApiState :: from_ref ( & api_state) ) )
149+ }
150+ } ) ,
151+ )
143152 . layer (
144153 ServiceBuilder :: new ( ) . layer ( session_layer) . layer (
145154 CorsLayer :: new ( )
@@ -160,59 +169,76 @@ pub async fn app(ctx: AppContext, host: String, port: u16) -> Router {
160169 )
161170 . layer ( TraceLayer :: new_for_http ( ) )
162171 . layer ( RequestDecompressionLayer :: new ( ) )
163- . with_state ( state )
172+ . with_state ( api_state )
164173 . split_for_parts ( ) ;
165174 router. merge ( SwaggerUi :: new ( "/swagger-ui" ) . url ( "/api/openapi.json" , api) )
166175}
167176
168- lazy_static ! {
169- /// The following regular expressions are used to match the Git server protocol.
170- static ref INFO_REFS_REGEX : Regex = Regex :: new( r"/info/refs$" ) . unwrap( ) ;
171- static ref REGEX_GIT_UPLOAD_PACK : Regex = Regex :: new( r"/git-upload-pack$" ) . unwrap( ) ;
172- static ref REGEX_GIT_RECEIVE_PACK : Regex = Regex :: new( r"/git-receive-pack$" ) . unwrap( ) ;
173- }
177+ fn rewrite_lfs_request_uri < B > ( mut req : Request < B > ) -> Request < B > {
178+ let full_path = req. uri ( ) . path ( ) ;
174179
175- pub async fn get_method_router (
176- state : State < ProtocolApiState > ,
177- Query ( params) : Query < InfoRefsParams > ,
178- uri : Uri ,
179- ) -> Result < Response < Body > , ProtocolError > {
180- if INFO_REFS_REGEX . is_match ( uri. path ( ) ) {
181- let pack_protocol = SmartProtocol :: new (
182- remove_git_suffix ( uri, "/info/refs" ) ,
183- TransportProtocol :: Http ,
184- ) ;
185- crate :: git_protocol:: http:: git_info_refs ( & state, params, pack_protocol) . await
186- } else {
187- Err ( ProtocolError :: NotFound (
188- "Operation not supported" . to_owned ( ) ,
189- ) )
180+ if let Some ( pos) = full_path. rfind ( "/info/lfs/" ) {
181+ let lfs_subpath = & full_path[ pos..] ;
182+
183+ let new_path_and_query = if let Some ( query) = req. uri ( ) . query ( ) {
184+ format ! ( "{}?{}" , lfs_subpath, query)
185+ } else {
186+ lfs_subpath. to_owned ( )
187+ } ;
188+
189+ let new_uri = match Uri :: builder ( ) . path_and_query ( & new_path_and_query) . build ( ) {
190+ Ok ( uri) => uri,
191+ Err ( e) => {
192+ tracing:: warn!(
193+ "Failed to rewrite LFS URI: {}, error: {}" ,
194+ new_path_and_query,
195+ e
196+ ) ;
197+ // Return the request unchanged, let downstream handlers deal with it
198+ return req;
199+ }
200+ } ;
201+
202+ tracing:: debug!( "rewrite: old uri {:?}" , req. uri( ) ) ;
203+ * req. uri_mut ( ) = new_uri;
204+ tracing:: debug!( "rewrite: new uri {:?}" , req. uri( ) ) ;
190205 }
206+ req
191207}
192208
193- pub async fn post_method_router (
194- state : State < ProtocolApiState > ,
195- uri : Uri ,
209+ async fn handle_smart_protocol (
196210 req : Request < Body > ,
211+ state : Arc < ProtocolApiState > ,
197212) -> Result < Response , ProtocolError > {
198- if REGEX_GIT_UPLOAD_PACK . is_match ( uri. path ( ) ) {
213+ let full_path = req. uri ( ) . path ( ) ;
214+ if full_path. ends_with ( "/info/refs" ) && req. method ( ) . eq ( & Method :: GET ) {
215+ let pack_protocol = SmartProtocol :: new (
216+ remove_git_suffix ( full_path, "/info/refs" ) ,
217+ TransportProtocol :: Http ,
218+ ) ;
219+ let uri = req. uri ( ) ;
220+ let query_str = uri. query ( ) . unwrap_or ( "" ) ;
221+ let params: InfoRefsParams = serde_urlencoded:: from_str ( query_str) . unwrap ( ) ;
222+ crate :: git_protocol:: http:: git_info_refs ( & state, params, pack_protocol) . await
223+ } else if full_path. ends_with ( "/git-upload-pack" ) && req. method ( ) . eq ( & Method :: POST ) {
199224 let mut pack_protocol = SmartProtocol :: new (
200- remove_git_suffix ( uri . clone ( ) , "/git-upload-pack" ) ,
225+ remove_git_suffix ( full_path , "/git-upload-pack" ) ,
201226 TransportProtocol :: Http ,
202227 ) ;
203228 pack_protocol. service_type = Some ( ServiceType :: UploadPack ) ;
204229 crate :: git_protocol:: http:: git_upload_pack ( & state, req, pack_protocol) . await
205- } else if REGEX_GIT_RECEIVE_PACK . is_match ( uri . path ( ) ) {
230+ } else if full_path . ends_with ( "/git-receive-pack" ) && req . method ( ) . eq ( & Method :: POST ) {
206231 let mut pack_protocol = SmartProtocol :: new (
207- remove_git_suffix ( uri . clone ( ) , "/git-receive-pack" ) ,
232+ remove_git_suffix ( full_path , "/git-receive-pack" ) ,
208233 TransportProtocol :: Http ,
209234 ) ;
210235 pack_protocol. service_type = Some ( ServiceType :: ReceivePack ) ;
211236 crate :: git_protocol:: http:: git_receive_pack ( & state, req, pack_protocol) . await
212237 } else {
213- Err ( ProtocolError :: NotFound (
214- "Operation not supported" . to_owned ( ) ,
215- ) )
238+ Ok ( Response :: builder ( )
239+ . status ( 404 )
240+ . body ( Body :: from ( "Operation not supported" ) )
241+ . unwrap ( ) )
216242 }
217243}
218244
@@ -247,4 +273,79 @@ pub const MERGE_QUEUE_TAG: &str = "Merge Queue Management";
247273struct ApiDoc ;
248274
249275#[ cfg( test) ]
250- mod test { }
276+ mod tests {
277+ use super :: * ;
278+ use http:: Request ;
279+
280+ #[ test]
281+ fn test_rewrite_lfs_uri_basic ( ) {
282+ let req = Request :: builder ( )
283+ . uri ( "/repo/a/b/info/lfs/objects/123" )
284+ . body ( ( ) )
285+ . unwrap ( ) ;
286+
287+ let new_req = rewrite_lfs_request_uri ( req) ;
288+
289+ assert_eq ! ( new_req. uri( ) . path( ) , "/info/lfs/objects/123" ) ;
290+ }
291+
292+ #[ test]
293+ fn test_rewrite_keeps_query_string ( ) {
294+ let req = Request :: builder ( )
295+ . uri ( "/repo/a/info/lfs/locks?token=abc123" )
296+ . body ( ( ) )
297+ . unwrap ( ) ;
298+
299+ let new_req = rewrite_lfs_request_uri ( req) ;
300+
301+ assert_eq ! (
302+ new_req. uri( ) . path_and_query( ) . unwrap( ) . to_string( ) ,
303+ "/info/lfs/locks?token=abc123"
304+ ) ;
305+ }
306+
307+ #[ test]
308+ fn test_no_rewrite_when_no_lfs_prefix ( ) {
309+ let req = Request :: builder ( ) . uri ( "/not-lfs-path" ) . body ( ( ) ) . unwrap ( ) ;
310+
311+ let new_req = rewrite_lfs_request_uri ( req) ;
312+
313+ assert_eq ! ( new_req. uri( ) . path( ) , "/not-lfs-path" ) ;
314+ }
315+
316+ #[ test]
317+ fn test_rewrite_with_trailing_slash ( ) {
318+ let req = Request :: builder ( )
319+ . uri ( "/repo/info/lfs/locks/" )
320+ . body ( ( ) )
321+ . unwrap ( ) ;
322+
323+ let new_req = rewrite_lfs_request_uri ( req) ;
324+
325+ assert_eq ! ( new_req. uri( ) . path( ) , "/info/lfs/locks/" ) ;
326+ }
327+
328+ #[ test]
329+ fn test_rewrite_complex_path ( ) {
330+ let req = Request :: builder ( )
331+ . uri ( "/a/b/c/info/lfs/objects/abc/def/ghi" )
332+ . body ( ( ) )
333+ . unwrap ( ) ;
334+
335+ let new_req = rewrite_lfs_request_uri ( req) ;
336+
337+ assert_eq ! ( new_req. uri( ) . path( ) , "/info/lfs/objects/abc/def/ghi" ) ;
338+ }
339+
340+ #[ test]
341+ fn test_rewrite_when_repo_path_contains_info_lfs ( ) {
342+ let req = Request :: builder ( )
343+ . uri ( "/repos/info/lfs/info/lfs/objects/123" )
344+ . body ( ( ) )
345+ . unwrap ( ) ;
346+
347+ let new_req = rewrite_lfs_request_uri ( req) ;
348+
349+ assert_eq ! ( new_req. uri( ) . path( ) , "/info/lfs/objects/123" ) ;
350+ }
351+ }
0 commit comments