Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 67 additions & 32 deletions crates/tiles/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2329,44 +2329,70 @@ mod tests {
serve_recorded(answers).0
}

/// Reads a whole request from `stream`, body too: answering and closing while a POST's body
/// is still arriving resets the connection, and the client sees that instead of the answer.
#[cfg(not(target_arch = "wasm32"))]
fn read_request(stream: &mut std::net::TcpStream) -> String {
use std::io::Read;
let _ = stream.set_read_timeout(Some(std::time::Duration::from_secs(5)));
let mut request = Vec::new();
let mut buf = [0u8; 2048];
while let Ok(n) = stream.read(&mut buf) {
if n == 0 {
break;
}
request.extend_from_slice(&buf[..n]);
let Some(head_end) = request.windows(4).position(|w| w == b"\r\n\r\n") else { continue };
let head = String::from_utf8_lossy(&request[..head_end]).to_ascii_lowercase();
let length = head
.lines()
.find_map(|line| line.strip_prefix("content-length:"))
.and_then(|v| v.trim().parse::<usize>().ok())
.unwrap_or(0);
if request.len() >= head_end + 4 + length {
break;
}
}
String::from_utf8_lossy(&request).into_owned()
}

#[cfg(not(target_arch = "wasm32"))]
fn serve_recorded(answers: Vec<Vec<u8>>) -> (String, std::sync::mpsc::Receiver<String>) {
use std::io::{Read, Write};
use std::io::Write;
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let (sent, received) = std::sync::mpsc::channel();
std::thread::spawn(move || {
for answer in answers {
let Ok((mut stream, _)) = listener.accept() else { return };
// The whole request, body too, before answering: answering and closing while a
// POST's body is still arriving resets the connection, and the client sees that
// instead of the answer.
let _ = stream.set_read_timeout(Some(std::time::Duration::from_secs(5)));
let mut request = Vec::new();
let mut buf = [0u8; 2048];
while let Ok(n) = stream.read(&mut buf) {
if n == 0 {
break;
}
request.extend_from_slice(&buf[..n]);
let Some(head_end) = request.windows(4).position(|w| w == b"\r\n\r\n") else { continue };
let head = String::from_utf8_lossy(&request[..head_end]).to_ascii_lowercase();
let length = head
.lines()
.find_map(|line| line.strip_prefix("content-length:"))
.and_then(|v| v.trim().parse::<usize>().ok())
.unwrap_or(0);
if request.len() >= head_end + 4 + length {
break;
}
}
let _ = sent.send(String::from_utf8_lossy(&request).into_owned());
let _ = sent.send(read_request(&mut stream));
let _ = stream.write_all(&answer);
}
});
(format!("http://{addr}"), received)
}

/// Serves every connection to the returned address with what `answer` gives for its request
/// path, sending each request to the receiver. It answers until the test binary exits, so its
/// thread and listener live that long.
#[cfg(not(target_arch = "wasm32"))]
fn serve_by_path(answer: impl Fn(&str) -> Vec<u8> + Send + 'static) -> (String, std::sync::mpsc::Receiver<String>) {
use std::io::Write;
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let (sent, received) = std::sync::mpsc::channel();
std::thread::spawn(move || {
for stream in listener.incoming() {
let Ok(mut stream) = stream else { return };
let request = read_request(&mut stream);
let path = request.split_whitespace().nth(1).unwrap_or("").to_string();
let _ = sent.send(request);
let _ = stream.write_all(&answer(&path));
}
});
(format!("http://{addr}"), received)
}

#[cfg(not(target_arch = "wasm32"))]
#[test]
fn logins_headers_follow_same_origin_redirects() {
Expand Down Expand Up @@ -2621,13 +2647,18 @@ mod tests {
let tilejson = serde_json::json!({"tiles": ["./tile/{z}/{x}/{y}.pbf"]});
let answer =
|body: String| format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).into_bytes();
let (base, asked) = serve_recorded(vec![
answer(style.to_string()),
answer(tilejson.to_string()),
answer("{}".into()),
answer("{}".into()),
answer("{}".into()),
]);
// Answered by path, for as many connections as come: a test running at once may clear the
// shared cache, and resolving then fetches the style again in the background, so the
// requests are neither five nor in a fixed order.
let (base, asked) = serve_by_path(move |path| {
Comment thread
giswqs marked this conversation as resolved.
Resolved
if path.starts_with("/style.json") {
answer(style.to_string())
} else if path.starts_with("/tiles.json") {
answer(tilejson.to_string())
} else {
answer("{}".into())
}
});
let binding =
logins::activate(&base, "derived-style", Method::Header("X-Service-Key".into()), "derived-style-secret", true).unwrap();
let url = logins::bind(&binding, &format!("{base}/style.json")).unwrap();
Expand Down Expand Up @@ -2678,7 +2709,11 @@ mod tests {
..Default::default()
};
native::get(&native::agent(), &info.request(0.0, 0.0, 10.0).unwrap(), &[]).unwrap();
for request in asked.iter().take(5) {
// Every request sent so far, background fetches included. Each of the test's own requests
// was sent on before its answer came back, so all of them are among these.
let requests: Vec<String> = asked.try_iter().collect();
assert!(requests.len() >= 5);
for request in &requests {
assert!(request.to_ascii_lowercase().contains("x-service-key: derived-style-secret\r\n"));
assert!(!request.contains("rustgis-login:"));
}
Expand Down
Loading