diff --git a/knot2/worker/src/proxy.rs b/knot2/worker/src/proxy.rs index cad5ef795..f35f780fc 100644 --- a/knot2/worker/src/proxy.rs +++ b/knot2/worker/src/proxy.rs @@ -1,7 +1,8 @@ -// knot2's git smart-http routes, forwarded to the repo's artifacts remote. -// fetches use the knot's read token; pushes carry the pusher's own artifacts -// token. a push's ref commands and report-status are read on the way through -// so the knot can announce what moved. +// knot2's git smart-http routes. fetches redirect to the repo's artifacts +// remote with the knot's read token in the url, so git talks to artifacts +// directly. pushes carry the pusher's own artifacts token and are forwarded, +// their ref commands and report-status read on the way through so the knot +// can announce what moved. use worker::{Fetch, Headers, Method, Request, RequestInit, Response}; @@ -82,6 +83,39 @@ async fn resolve(knot: &Knot, path: &GitPath) -> Result, KnotErro } } +// userinfo may not hold the token's '?', '=' and friends as-is +fn userinfo_escape(value: &str) -> String { + value + .bytes() + .map(|byte| match byte { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => (byte as char).to_string(), + _ => format!("%{byte:02X}"), + }) + .collect() +} + +// 302 for info/refs, which git follows and rebases the fetch onto; 307 keeps a +// bare upload-pack post a post +async fn redirect(knot: &Knot, hosted: &Hosted, route: &GitRoute, service: Option<&str>) -> worker::Result { + let token = match knot.read_token(hosted).await { + Ok(token) => token, + Err(error) => return plain(503, &error.to_string()), + }; + let base = hosted.remote.trim_end_matches('/'); + let base = match (token, base.split_once("://")) { + (Some(token), Some((scheme, rest))) => format!("{scheme}://x:{}@{rest}", userinfo_escape(&token)), + _ => base.to_string(), + }; + let (location, status) = match route { + GitRoute::InfoRefs => (format!("{base}/info/refs?service={}", service.unwrap_or_default()), 302), + _ => (format!("{base}/git-upload-pack"), 307), + }; + let headers = Headers::new(); + headers.set("Location", &location)?; + headers.set("Cache-Control", "no-store")?; + Ok(Response::empty()?.with_status(status).with_headers(headers)) +} + fn copy_header(from: &Headers, to: &Headers, name: &str) -> worker::Result<()> { if let Some(value) = from.get(name)? { to.set(name, &value)?; @@ -191,29 +225,25 @@ pub async fn serve( { return plain(400, "unsupported service"); } + if !receive { + return redirect(knot, &hosted, &path.route, service.as_deref()).await; + } let incoming = req.headers().clone(); let outgoing = Headers::new(); for name in ["Content-Type", "Content-Encoding", "Accept", "Git-Protocol", "User-Agent"] { copy_header(&incoming, &outgoing, name)?; } - match receive { - true => match incoming.get("Authorization")? { - Some(value) => outgoing.set("Authorization", &value)?, - None => { - let headers = Headers::new(); - headers.set("WWW-Authenticate", "Basic realm=\"knot\"")?; - headers.set("Content-Type", "text/plain; charset=utf-8")?; - return Ok(Response::from_bytes(b"push needs an artifacts token".to_vec())? - .with_status(401) - .with_headers(headers)); - } - }, - false => match knot.read_token(&hosted).await { - Ok(Some(token)) => outgoing.set("Authorization", &format!("Bearer {token}"))?, - Ok(None) => {} - Err(error) => return plain(503, &error.to_string()), - }, + match incoming.get("Authorization")? { + Some(value) => outgoing.set("Authorization", &value)?, + None => { + let headers = Headers::new(); + headers.set("WWW-Authenticate", "Basic realm=\"knot\"")?; + headers.set("Content-Type", "text/plain; charset=utf-8")?; + return Ok(Response::from_bytes(b"push needs an artifacts token".to_vec())? + .with_status(401) + .with_headers(headers)); + } } let base = hosted.remote.trim_end_matches('/'); @@ -223,7 +253,7 @@ pub async fn serve( Method::Get, None, ), - GitRoute::UploadPack => (format!("{base}/git-upload-pack"), Method::Post, Some(req.bytes().await?)), + GitRoute::UploadPack => return plain(400, "unsupported service"), GitRoute::ReceivePack => (format!("{base}/git-receive-pack"), Method::Post, Some(req.bytes().await?)), }; let commands = match (&path.route, &body) {