Local workflow, smart-HTTP clone/fetch/push and public HTTPS clone/fetch match native git in stdout and exit codes. Only WASI imports (wasi:http, exit-with-code).
87 lines
2.8 KiB
Rust
87 lines
2.8 KiB
Rust
//! A local HTTP CGI server for integration tests against native git http-backend.
|
|
use std::{
|
|
io::{Read, Write},
|
|
net::TcpListener,
|
|
process::{Command, Stdio},
|
|
};
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let args: Vec<_> = std::env::args().collect();
|
|
let listener = TcpListener::bind("127.0.0.1:0")?;
|
|
std::fs::write(&args[2], listener.local_addr()?.port().to_string())?;
|
|
for stream in listener.incoming() {
|
|
let mut stream = stream?;
|
|
if let Err(error) = serve(&mut stream, &args[1]) {
|
|
eprintln!("request failed: {error}");
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
fn serve(io: &mut (impl Read + Write), root: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut header = Vec::new();
|
|
while !header.ends_with(b"\r\n\r\n") {
|
|
let mut byte = [0];
|
|
io.read_exact(&mut byte)?;
|
|
header.push(byte[0]);
|
|
}
|
|
let text = std::str::from_utf8(&header)?;
|
|
let mut lines = text.split("\r\n");
|
|
let request: Vec<_> = lines
|
|
.next()
|
|
.ok_or("no request")?
|
|
.split_whitespace()
|
|
.collect();
|
|
let mut length = 0;
|
|
let mut content_type = "";
|
|
for line in lines {
|
|
if let Some((key, value)) = line.split_once(':') {
|
|
if key.eq_ignore_ascii_case("content-length") {
|
|
length = value.trim().parse()?;
|
|
}
|
|
if key.eq_ignore_ascii_case("content-type") {
|
|
content_type = value.trim();
|
|
}
|
|
}
|
|
}
|
|
let mut body = vec![0; length];
|
|
io.read_exact(&mut body)?;
|
|
let (path, query) = request[1].split_once('?').unwrap_or((request[1], ""));
|
|
let mut child = Command::new("git")
|
|
.arg("http-backend")
|
|
.env("GIT_PROJECT_ROOT", root)
|
|
.env("GIT_HTTP_EXPORT_ALL", "1")
|
|
.env("PATH_INFO", path)
|
|
.env("QUERY_STRING", query)
|
|
.env("REQUEST_METHOD", request[0])
|
|
.env("CONTENT_TYPE", content_type)
|
|
.env("CONTENT_LENGTH", length.to_string())
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.spawn()?;
|
|
child.stdin.take().ok_or("no CGI stdin")?.write_all(&body)?;
|
|
let response = child.wait_with_output()?;
|
|
let split = response
|
|
.stdout
|
|
.windows(4)
|
|
.position(|w| w == b"\r\n\r\n")
|
|
.ok_or("invalid CGI response")?;
|
|
let headers = std::str::from_utf8(&response.stdout[..split])?;
|
|
let status = headers
|
|
.lines()
|
|
.find_map(|l| l.strip_prefix("Status: "))
|
|
.unwrap_or("200 OK");
|
|
write!(
|
|
io,
|
|
"HTTP/1.1 {status}\r\nConnection: close\r\nContent-Length: {}\r\n",
|
|
response.stdout.len() - split - 4
|
|
)?;
|
|
for line in headers.split("\r\n") {
|
|
if !line.starts_with("Status:") {
|
|
write!(io, "{line}\r\n")?;
|
|
}
|
|
}
|
|
io.write_all(b"\r\n")?;
|
|
io.write_all(&response.stdout[split + 4..])?;
|
|
io.flush()?;
|
|
Ok(())
|
|
}
|