This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-pack / examples / knot_http_backend.rs
7.1 kB 205 lines
1use std::io::{Read, Write}; 2use std::path::{Path, PathBuf}; 3 4use knot_git::Repo; 5 6#[global_allocator] 7static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; 8 9const SUFFIXES: [&str; 3] = ["/info/refs", "/git-upload-pack", "/git-receive-pack"]; 10 11fn env(key: &str) -> Option<String> { 12 std::env::var(key).ok() 13} 14 15fn resolve() -> Option<(PathBuf, &'static str)> { 16 let path_info = env("PATH_INFO")?; 17 let (repo_sub, suffix) = SUFFIXES 18 .iter() 19 .find_map(|suffix| path_info.strip_suffix(suffix).map(|rest| (rest, *suffix)))?; 20 let root = env("GIT_PROJECT_ROOT") 21 .filter(|root| !root.is_empty()) 22 .or_else(|| { 23 let translated = env("PATH_TRANSLATED")?; 24 translated 25 .strip_suffix(path_info.as_str()) 26 .map(str::to_string) 27 })?; 28 let repo_sub = repo_sub.trim_start_matches('/'); 29 Some((Path::new(&root).join(repo_sub), suffix)) 30} 31 32fn wants_v2() -> bool { 33 env("HTTP_GIT_PROTOCOL") 34 .or_else(|| env("GIT_PROTOCOL")) 35 .is_some_and(|value| value.split(':').any(|token| token.trim() == "version=2")) 36} 37 38fn dev_push_allowed() -> bool { 39 env("KNOT_DEV_ALLOW_HTTP_PUSH").is_some_and(|value| { 40 let value = value.trim(); 41 value == "1" || value.eq_ignore_ascii_case("true") 42 }) 43} 44 45fn dev_pack_limits() -> knot_pack::PackLimits { 46 let mut limits = knot_pack::PackLimits::default(); 47 if let Some(value) = env("KNOT_DEV_MAX_OBJECTS").and_then(|value| value.trim().parse().ok()) { 48 limits.max_objects = knot_types::ObjectCount::new(value); 49 } 50 if let Some(value) = env("KNOT_DEV_MAX_TOTAL_BYTES").and_then(|value| value.trim().parse().ok()) 51 { 52 limits.max_total_bytes = knot_pack::MaxTotalBytes::new(value); 53 } 54 limits 55} 56 57fn apply_dev_selection_limits() { 58 let max_objects = env("KNOT_DEV_SELECTION_MAX_OBJECTS") 59 .and_then(|value| value.trim().parse().ok()) 60 .map(knot_types::ObjectCount::new); 61 let secs = 62 env("KNOT_DEV_SELECTION_TIME_BUDGET_SECS").and_then(|value| value.trim().parse().ok()); 63 if max_objects.is_none() && secs.is_none() { 64 return; 65 } 66 let base = knot_pack::SelectionLimits::default(); 67 knot_pack::init_selection_limits(knot_pack::SelectionLimits { 68 max_objects: max_objects.unwrap_or(base.max_objects), 69 time_budget: secs 70 .map(std::time::Duration::from_secs) 71 .unwrap_or(base.time_budget), 72 }); 73} 74 75fn apply_dev_resources() { 76 let max_threads = env("KNOT_DEV_MAX_THREADS") 77 .and_then(|value| value.trim().parse::<usize>().ok()) 78 .filter(|threads| *threads != 0) 79 .map(knot_resource::ThreadCount::new); 80 knot_resource::init(knot_resource::Ceilings { 81 max_threads, 82 ..knot_resource::Ceilings::default() 83 }); 84} 85 86fn read_body() -> Vec<u8> { 87 let mut raw = Vec::new(); 88 std::io::stdin().read_to_end(&mut raw).ok(); 89 let gzipped = env("HTTP_CONTENT_ENCODING").is_some_and(|value| { 90 value 91 .split(',') 92 .any(|token| token.trim().eq_ignore_ascii_case("gzip")) 93 }); 94 if !gzipped { 95 return raw; 96 } 97 let mut decoded = Vec::new(); 98 flate2::read::GzDecoder::new(raw.as_slice()) 99 .read_to_end(&mut decoded) 100 .ok(); 101 decoded 102} 103 104fn emit(out: &mut dyn Write, content_type: &str, body: &[u8]) { 105 let _ = write!( 106 out, 107 "Expires: Fri, 01 Jan 1980 00:00:00 GMT\r\nPragma: no-cache\r\nCache-Control: no-cache, max-age=0, must-revalidate\r\nContent-Type: {content_type}\r\n\r\n" 108 ); 109 let _ = out.write_all(body); 110} 111 112fn fail(out: &mut dyn Write, status: &str, message: &str) { 113 let _ = write!( 114 out, 115 "Status: {status}\r\nContent-Type: text/plain\r\n\r\n{message}\n" 116 ); 117} 118 119const PUSH_REFUSED: &str = "knot accepts pushes over SSH, not HTTP"; 120 121fn serve_receive_advert(out: &mut dyn Write, repo: &Repo) { 122 if !dev_push_allowed() { 123 return fail(out, "403 Forbidden", PUSH_REFUSED); 124 } 125 match knot_pack::advertise_receive(repo) { 126 Ok(body) => emit(out, "application/x-git-receive-pack-advertisement", &body), 127 Err(error) => fail(out, "500 Internal Server Error", &error.to_string()), 128 } 129} 130 131fn serve_receive_pack(out: &mut dyn Write, repo: &Repo) { 132 if !dev_push_allowed() { 133 return fail(out, "403 Forbidden", PUSH_REFUSED); 134 } 135 match knot_pack::receive_pack_with_limits(repo, &read_body(), &dev_pack_limits()) { 136 Ok(result) => emit(out, "application/x-git-receive-pack-result", &result), 137 Err(error) => fail(out, "500 Internal Server Error", &error.to_string()), 138 } 139} 140 141fn main() { 142 apply_dev_selection_limits(); 143 apply_dev_resources(); 144 let stdout = std::io::stdout(); 145 let mut out = stdout.lock(); 146 147 let (repo_dir, suffix) = match resolve() { 148 Some(parts) => parts, 149 None => { 150 return fail( 151 &mut out, 152 "400 Bad Request", 153 "unrecognized git smart-http path", 154 ); 155 } 156 }; 157 let repo = match Repo::open(&repo_dir) { 158 Ok(repo) => repo, 159 Err(_) => return fail(&mut out, "404 Not Found", "no such repository"), 160 }; 161 let method = env("REQUEST_METHOD").unwrap_or_default(); 162 163 match (method.as_str(), suffix) { 164 ("GET", "/info/refs") => { 165 let service = env("QUERY_STRING") 166 .and_then(|query| { 167 query 168 .split('&') 169 .find_map(|pair| pair.strip_prefix("service=").map(str::to_string)) 170 }) 171 .unwrap_or_default(); 172 match service.as_str() { 173 "git-upload-pack" => { 174 let body = if wants_v2() { 175 knot_pack::advertise_upload(&repo) 176 } else { 177 knot_pack::advertise_upload_v0(&repo) 178 }; 179 match body { 180 Ok(body) => emit( 181 &mut out, 182 "application/x-git-upload-pack-advertisement", 183 &body, 184 ), 185 Err(error) => { 186 fail(&mut out, "500 Internal Server Error", &error.to_string()) 187 } 188 } 189 } 190 "git-receive-pack" => serve_receive_advert(&mut out, &repo), 191 _ => fail(&mut out, "403 Forbidden", "unsupported service"), 192 } 193 } 194 ("POST", "/git-upload-pack") => match knot_pack::upload_pack(&repo, &read_body()) { 195 Ok(result) => emit(&mut out, "application/x-git-upload-pack-result", &result), 196 Err(error) => fail(&mut out, "500 Internal Server Error", &error.to_string()), 197 }, 198 ("POST", "/git-receive-pack") => serve_receive_pack(&mut out, &repo), 199 _ => fail( 200 &mut out, 201 "400 Bad Request", 202 "unsupported git smart-http request", 203 ), 204 } 205}