1515use std:: sync:: Arc ;
1616
1717use axum:: {
18- body:: Body ,
18+ body:: { self , Body } ,
1919 extract:: { Path , State } ,
2020 http:: { Request , StatusCode , Uri , header} ,
21- response:: IntoResponse ,
21+ response:: { IntoResponse , Response } ,
2222} ;
23- use base64:: { Engine , engine:: general_purpose:: STANDARD as Base64 } ;
24- use tracing:: { debug, error, warn} ;
23+ use tracing:: { error, info, trace, warn} ;
2524use uuid:: Uuid ;
2625
2726use crate :: {
@@ -36,11 +35,9 @@ use crate::{
3635/// This function handles authentication and routing for Git operations.
3736pub async fn proxy (
3837 State ( ctx) : State < Arc < Context > > ,
39- Path ( ( uuid, path ) ) : Path < ( Uuid , String ) > ,
40- mut req : Request < Body > ,
38+ Path ( ( uuid, _ ) ) : Path < ( Uuid , String ) > ,
39+ req : Request < Body > ,
4140) -> impl IntoResponse {
42- debug ! ( %uuid, %path, "Proxying Git request" ) ;
43-
4441 // Look up repository information by UUID (user course ID)
4542 // Return NOT_FOUND if repository is invalid or doesn't exist
4643 let ( owner, repo, email) = lookup ( & ctx. database , & uuid) . await . ok_or_else ( || {
@@ -51,24 +48,62 @@ pub async fn proxy(
5148 let Config { git_server_endpoint, auth_secret, .. } = & ctx. config ;
5249
5350 // Construct the URI for the Git server request to Gitea backend.
54- let sanitized_path = path. trim_start_matches ( '/' ) ;
55- let uri_str = format ! ( "{git_server_endpoint}/{owner}/{repo}.git/{}" , sanitized_path) ;
56- * req. uri_mut ( ) = Uri :: try_from ( uri_str) . map_err ( |e| {
57- error ! ( error = %e, "Failed to construct URI for proxy destination" ) ;
51+ let trimmed = strip_uuid_prefix ( req. uri ( ) , & uuid) ;
52+ let url = format ! ( "{git_server_endpoint}/{owner}/{repo}.git{trimmed}" ) ;
53+ let url = reqwest:: Url :: parse ( & url) . map_err ( |e| {
54+ error ! ( error = %e, "Failed to parse URI for proxy destination" ) ;
55+ StatusCode :: INTERNAL_SERVER_ERROR
56+ } ) ?;
57+ info ! ( url = %url, "Forwarding to Git server" ) ;
58+
59+ // Convert axum Request to reqwest Request
60+ let ( mut parts, body) = req. into_parts ( ) ;
61+ let body_bytes = body:: to_bytes ( body, usize:: MAX ) . await . map_err ( |e| {
62+ error ! ( error = %e, "Failed to read request body" ) ;
5863 StatusCode :: INTERNAL_SERVER_ERROR
5964 } ) ?;
6065
61- // Generate a password for the user's email using the auth secret and then
62- // construct the Basic Auth header value for the Git server request.
66+ // Remove the original host header
67+ parts . headers . remove ( header :: HOST ) ;
6368 let password = crypto:: password ( & email, auth_secret) ;
64- let credentials = Base64 . encode ( format ! ( "{owner}:{password}" ) ) ;
65- let auth_header_value = format ! ( "Basic {credentials}" ) . parse ( ) . unwrap ( ) ;
66- req. headers_mut ( ) . insert ( header:: AUTHORIZATION , auth_header_value) ;
6769
68- debug ! ( uri = %req. uri( ) , "Forwarding to Git server" ) ;
69- ctx. http . request ( req) . await . map_err ( |e| {
70+ let request = ctx
71+ . http
72+ . request ( parts. method , url)
73+ . headers ( parts. headers )
74+ . basic_auth ( owner, Some ( password) )
75+ . body ( body_bytes. to_vec ( ) )
76+ . build ( )
77+ . map_err ( |e| {
78+ error ! ( error = %e, "Failed to build reqwest request" ) ;
79+ StatusCode :: INTERNAL_SERVER_ERROR
80+ } ) ?;
81+ trace ! ( ?request, "Built client request for Git server" ) ;
82+
83+ // Execute the request
84+ let response = ctx. http . execute ( request) . await . map_err ( |e| {
7085 error ! ( error = %e, "Git server request failed" ) ;
7186 StatusCode :: BAD_GATEWAY
87+ } ) ?;
88+ trace ! ( ?response, "Received response from Git server" ) ;
89+
90+ // Convert reqwest Response to axum Response
91+ let status = response. status ( ) ;
92+ let headers = response. headers ( ) . clone ( ) ;
93+ let body = response. bytes ( ) . await . map_err ( |e| {
94+ error ! ( error = %e, "Failed to read response body" ) ;
95+ StatusCode :: INTERNAL_SERVER_ERROR
96+ } ) ?;
97+ trace ! ( body = ?String :: from_utf8_lossy( & body) , "Git server response" ) ;
98+
99+ let mut response_builder = Response :: builder ( ) . status ( status) ;
100+ for ( key, value) in headers. iter ( ) {
101+ response_builder = response_builder. header ( key, value) ;
102+ }
103+
104+ response_builder. body ( Body :: from ( body) ) . map_err ( |e| {
105+ error ! ( error = %e, "Failed to build response" ) ;
106+ StatusCode :: INTERNAL_SERVER_ERROR
72107 } )
73108}
74109
@@ -79,3 +114,13 @@ async fn lookup(db: &Database, uuid: &Uuid) -> Option<(String, String, String)>
79114
80115 Some ( ( user. username ( ) , course. course_slug , user. email ) )
81116}
117+
118+ /// Strip the leading "/{uuid}" from a request URI and return the remaining path+query.
119+ /// For example:
120+ /// input: "/5a0e.../info/refs?service=git-receive-pack"
121+ /// output: "/info/refs?service=git-receive-pack"
122+ fn strip_uuid_prefix ( uri : & Uri , uuid : & Uuid ) -> String {
123+ let path_and_query = uri. path_and_query ( ) . map ( |pq| pq. as_str ( ) ) . unwrap_or ( "" ) ;
124+ let prefix = format ! ( "/{}" , uuid) ;
125+ path_and_query. strip_prefix ( & prefix) . unwrap_or ( path_and_query) . to_string ( )
126+ }
0 commit comments