This repository has no description
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}