forks/sorcery
git clone https://git.gae.moe/forks/sorcery
efaf04a
main
1use axum:: body::{ Body , to_bytes}; 2use axum:: extract::{ Path as AxumPath , RawQuery , State }; 3use axum:: http::{ HeaderMap , HeaderValue , Request , StatusCode , header}; 4use axum:: response::{ IntoResponse , Response }; 5 6use crate :: githttp; 7use crate :: render:: encode_path; 8 9use super ::{ AppState , WebResult , internal, x_accel}; 10 11/// Read-only static access to an allowlisted subset of the bare repository, 12/// for the in-browser git client (and dumb-protocol clones). Objects are 13/// content-addressed and immutable; refs are never cached. 14pub ( super ) async fn git_dir ( 15State ( state): State < AppState >, 16AxumPath (( user, repo, path)): AxumPath <( String , String , String )>, 17) ->WebResult < Response > { 18if !allowed_git_path ( & path) { 19return Err (( StatusCode :: NOT_FOUND , "not served" . into ())); 20} 21let entry = state. resolve ( & user, & repo) ?; 22let repo_dir = entry 23. repository 24. path 25. file_name () 26. ok_or_else ( ||internal ( "repository path has no file name" )) ? 27. to_string_lossy (); 28let mut response =x_accel ( 29& state. config . git_internal_prefix , 30& format! ( 31"{}/{}/{}" , 32encode_path ( & user), 33encode_path ( & repo_dir), 34encode_path ( & path) 35), 36) ?; 37let cache_control =if path. starts_with ( "objects/" ) && path !="objects/info/packs" { 38"public, max-age=31536000, immutable" 39} else { 40"no-cache" 41}; 42 response. headers_mut (). insert ( 43 header:: CACHE_CONTROL , 44HeaderValue :: from_static ( cache_control), 45); 46Ok ( response) 47} 48 49/// `HEAD`, refs, and the object store; never `config` (may hold credentials 50/// for mirrors) or `hooks/`. 51fn allowed_git_path ( path : & str ) ->bool { 52let is_hex = |s : & str | !s. is_empty () && s. bytes (). all ( |byte| byte. is_ascii_hexdigit ()); 53match path{ 54"HEAD" |"packed-refs" |"info/refs" |"objects/info/packs" =>true , 55 pathif path. starts_with ( "refs/" ) => path 56. split ( '/' ) 57. all ( |component| !component. is_empty () && component !="." && component !=".." ), 58 path =>match path. strip_prefix ( "objects/" ) { 59Some ( rest) =>match rest. strip_prefix ( "pack/" ) { 60Some ( pack) =>{ 61let stem = pack 62. strip_suffix ( ".pack" ) 63. or_else ( || pack. strip_suffix ( ".idx" )) 64. and_then ( |stem| stem. strip_prefix ( "pack-" )); 65 stem. is_some_and ( is_hex) 66} 67None =>matches! ( 68 rest. split_once ( '/' ), 69Some (( fan, name)) if fan. len () ==2 &&is_hex ( fan) &&is_hex ( name) 70), 71}, 72None =>false , 73}, 74} 75} 76 77pub ( super ) async fn info_refs ( 78State ( state): State < AppState >, 79AxumPath (( user, repo)): AxumPath <( String , String )>, 80RawQuery ( query): RawQuery , 81headers : HeaderMap , 82) ->WebResult < Response > { 83if query. as_deref () !=Some ( "service=git-upload-pack" ) { 84return Err (( 85StatusCode :: FORBIDDEN , 86"only git-upload-pack is supported" . into (), 87)); 88} 89let entry = state. resolve ( & user, & repo) ?; 90 githttp:: advertise ( & entry. repository . path , & headers, state. git_permit () ?) 91} 92 93pub ( super ) async fn upload_pack ( 94State ( state): State < AppState >, 95AxumPath (( user, repo)): AxumPath <( String , String )>, 96request : Request < Body >, 97) ->WebResult < Response > { 98let entry = state. resolve ( & user, & repo) ?; 99let permit = state. git_permit () ?; 100let ( parts, body) = request. into_parts (); 101let body =to_bytes ( body, 64 <<20 ). await . map_err ( |_|{ 102( 103StatusCode :: PAYLOAD_TOO_LARGE , 104"git request body is too large" . into (), 105) 106}) ?; 107 githttp:: upload ( & entry. repository . path , & parts. headers , body, permit) 108} 109 110pub ( super ) async fn raw_blob ( 111State ( state): State < AppState >, 112AxumPath (( user, repo, oid, path)): AxumPath <( String , String , String , String )>, 113) ->WebResult < Response > { 114let oid = gix:: ObjectId :: from_hex ( oid. as_bytes ()) 115. map_err ( |_|( StatusCode :: NOT_FOUND , "blob not found" . into ())) ?; 116let repo_path = state. resolve ( & user, & repo) ?. repository . path ; 117let permit = state. git_permit () ?; 118let data = tokio:: task:: spawn_blocking ( move || -> anyhow:: Result < Option < Vec < u8 >>> { 119let _permit = permit; 120let repo = gix:: open ( repo_path) ?; 121let Ok ( object) = repo. find_object ( oid) else { 122return Ok ( None ); 123}; 124Ok (( object. kind == gix:: object:: Kind :: Blob ). then ( || object. data . to_vec ())) 125}) 126. await 127. map_err ( internal) ? 128. map_err ( internal) ? 129. ok_or_else ( ||( StatusCode :: NOT_FOUND , "blob not found" . into ())) ?; 130 131let content_type =raw_content_type ( & path, & data); 132let mut response =Body :: from ( data). into_response (); 133 response. headers_mut (). insert ( 134 header:: CONTENT_TYPE , 135HeaderValue :: from_str ( & content_type). map_err ( internal) ?, 136); 137 response. headers_mut (). insert ( 138 header:: CONTENT_DISPOSITION , 139HeaderValue :: from_static ( "inline" ), 140); 141 response. headers_mut (). insert ( 142"content-security-policy" , 143HeaderValue :: from_static ( 144"sandbox; default-src 'none'; base-uri 'none'; form-action 'none'" , 145), 146); 147 response. headers_mut (). insert ( 148"x-content-type-options" , 149HeaderValue :: from_static ( "nosniff" ), 150); 151 response. headers_mut (). insert ( 152 header:: CACHE_CONTROL , 153HeaderValue :: from_static ( "public, max-age=31536000, immutable" ), 154); 155Ok ( response) 156} 157 158fn raw_content_type ( path : & str , data : & [ u8 ]) ->String { 159if let Some ( mime) =crate :: render:: image_mime ( path) { 160return mime. into (); 161} 162if std:: str:: from_utf8 ( data). is_ok () { 163return "text/plain; charset=utf-8" . into (); 164} 165 mime_guess:: from_path ( path) 166. first () 167. filter ( |mime|{ 168let essence = mime. essence_str (); 169 essence. starts_with ( "audio/" ) 170 || essence. starts_with ( "video/" ) 171 || essence. starts_with ( "image/" ) && essence !="image/svg+xml" 172}) 173. map ( |mime| mime. to_string ()) 174. unwrap_or_else ( ||"application/octet-stream" . into ()) 175} 176 177# [ cfg ( test )] 178mod tests{ 179use super :: raw_content_type; 180 181# [ test ] 182fn raw_blobs_never_render_active_content () { 183assert_eq! ( 184raw_content_type ( "page.html" , b"<script>alert(1)</script>" ), 185"text/plain; charset=utf-8" 186); 187assert_eq! ( 188raw_content_type ( "image.svg" , b"<svg><script/></svg>" ), 189"text/plain; charset=utf-8" 190); 191assert_eq! ( 192raw_content_type ( "page.html" , b"\xff\xfe<html>" ), 193"application/octet-stream" 194); 195assert_eq! ( 196raw_content_type ( "image.png" , b"\x89PNG\r\n\x1a\n" ), 197"image/png" 198); 199assert_eq! ( raw_content_type ( "tiny.GIF" , b"GIF89a\x01\0\x01\0" ), "image/gif" ); 200} 201}