hive-agent: extra web proxies forward every repeated response header
Both proxy paths copied upstream response headers with HeaderMap::insert, which replaces earlier values of the same name, so an upstream sending two Set-Cookie (or Link, Vary, ...) headers reached the browser with only the last one. Use append in both loops. Tests cover the http and unix: paths.
This commit is contained in:
parent
f3860f3497
commit
a200935216
2 changed files with 71 additions and 3 deletions
|
|
@ -380,7 +380,8 @@ shaped).
|
||||||
`{"<name>": "<upstream_url>"}`). Each entry mounts a transparent
|
`{"<name>": "<upstream_url>"}`). Each entry mounts a transparent
|
||||||
reverse-proxy at `/extra/<name>/` that forwards every request (method,
|
reverse-proxy at `/extra/<name>/` that forwards every request (method,
|
||||||
headers, body) to the configured upstream, strips hop-by-hop headers
|
headers, body) to the configured upstream, strips hop-by-hop headers
|
||||||
on both sides, and buffers the full response body (MVP — SSE
|
on both sides (repeated response headers such as two `Set-Cookie` lines
|
||||||
|
are all forwarded), and buffers the full response body (MVP — SSE
|
||||||
connections will appear as one large response rather than streaming).
|
connections will appear as one large response rather than streaming).
|
||||||
The `/extra/` namespace ensures user-declared proxies can never
|
The `/extra/` namespace ensures user-declared proxies can never
|
||||||
conflict with native agent endpoints. Upstream values are either an
|
conflict with native agent endpoints. Upstream values are either an
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ async fn proxy_handler(
|
||||||
HeaderName::from_bytes(name.as_str().as_bytes()),
|
HeaderName::from_bytes(name.as_str().as_bytes()),
|
||||||
HeaderValue::from_bytes(value.as_bytes()),
|
HeaderValue::from_bytes(value.as_bytes()),
|
||||||
) {
|
) {
|
||||||
resp_headers.insert(n, v);
|
resp_headers.append(n, v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match upstream_resp.bytes().await {
|
match upstream_resp.bytes().await {
|
||||||
|
|
@ -269,8 +269,75 @@ async fn forward_over_unix_socket(
|
||||||
if HOP_BY_HOP.contains(&name.as_str()) {
|
if HOP_BY_HOP.contains(&name.as_str()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
resp_headers.insert(name.clone(), value.clone());
|
resp_headers.append(name.clone(), value.clone());
|
||||||
}
|
}
|
||||||
let body_bytes = upstream_resp.into_body().collect().await?.to_bytes();
|
let body_bytes = upstream_resp.into_body().collect().await?.to_bytes();
|
||||||
Ok((status, resp_headers, body_bytes).into_response())
|
Ok((status, resp_headers, body_bytes).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
|
/// Raw HTTP/1.1 answer with two `Set-Cookie` headers and a body.
|
||||||
|
const TWO_COOKIES: &[u8] = b"HTTP/1.1 200 OK\r\n\
|
||||||
|
Set-Cookie: session=abc\r\n\
|
||||||
|
Set-Cookie: csrf=def\r\n\
|
||||||
|
Content-Length: 2\r\n\
|
||||||
|
Connection: close\r\n\r\nok";
|
||||||
|
|
||||||
|
/// Read the request head, then answer with [`TWO_COOKIES`].
|
||||||
|
async fn answer_once<S: AsyncReadExt + AsyncWriteExt + Unpin>(mut stream: S) {
|
||||||
|
let mut buf = [0u8; 1024];
|
||||||
|
let _ = stream.read(&mut buf).await;
|
||||||
|
stream.write_all(TWO_COOKIES).await.unwrap();
|
||||||
|
stream.shutdown().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cookies(resp: &Response) -> Vec<&str> {
|
||||||
|
resp.headers()
|
||||||
|
.get_all("set-cookie")
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.to_str().unwrap())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unix_upstream_keeps_repeated_response_headers() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let sock = dir.path().join("up.sock");
|
||||||
|
let listener = tokio::net::UnixListener::bind(&sock).unwrap();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let (stream, _) = listener.accept().await.unwrap();
|
||||||
|
answer_once(stream).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
let resp =
|
||||||
|
forward_over_unix_socket(&sock, Method::GET, "/", HeaderMap::new(), Bytes::new())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cookies(&resp), ["session=abc", "csrf=def"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn http_upstream_keeps_repeated_response_headers() {
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let base = format!("http://{}", listener.local_addr().unwrap());
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let (stream, _) = listener.accept().await.unwrap();
|
||||||
|
answer_once(stream).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
let client = reqwest::Client::builder().no_proxy().build().unwrap();
|
||||||
|
let resp = proxy_handler(
|
||||||
|
State((Arc::new(client), Arc::new(base))),
|
||||||
|
Method::GET,
|
||||||
|
Uri::from_static("/"),
|
||||||
|
HeaderMap::new(),
|
||||||
|
Bytes::new(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(cookies(&resp), ["session=abc", "csrf=def"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue