clawhdf5-remote: redirects are followed safely
ureq's defaults followed up to 10 redirects, including from https to plain http, and forwarded the custom HttpOptions::headers (X-Api-Key, Cookie, ...) to whatever host a redirect named — only Authorization was stripped. HttpStorage now follows redirects itself (ureq's max_redirects is 0): - at most HttpOptions::max_redirects per request (default 5; 0 refuses any redirect), then RemoteError::Redirect; - never from https to another scheme, nor to a non-http(s) URL; - once a redirect leaves the URL's origin (scheme, host, port), none of the custom headers is sent any more (Authorization included); - each hop counts as a request; errors show the target redacted. Tests: a redirect to another local port reads the right data and the target never sees X-Api-Key or Authorization (it did before); a same-origin redirect keeps them; a loop stops after 6 requests; 0 refuses; unit tests for target resolution, the https downgrade and origins. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -57,6 +57,10 @@ pub struct Shared {
|
||||
pub force_status: AtomicU32,
|
||||
/// Answer ranges with a `Content-Range` one byte off.
|
||||
pub wrong_range: AtomicBool,
|
||||
/// Path → `Location`: answered `302 Found` (counted as a request).
|
||||
pub redirects: RwLock<HashMap<String, String>>,
|
||||
/// (path, headers) of every counted request, header names lowercase.
|
||||
pub seen: Mutex<Vec<(String, HashMap<String, String>)>>,
|
||||
/// Requests for a served path (every status); requests for other
|
||||
/// paths are not counted.
|
||||
pub requests: AtomicU64,
|
||||
@@ -119,11 +123,31 @@ impl Server {
|
||||
self.shared.bytes.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Zero the counters and the log.
|
||||
/// Zero the counters and the logs.
|
||||
pub fn reset(&self) {
|
||||
self.shared.requests.store(0, Ordering::SeqCst);
|
||||
self.shared.bytes.store(0, Ordering::SeqCst);
|
||||
self.shared.log.lock().unwrap().clear();
|
||||
self.shared.seen.lock().unwrap().clear();
|
||||
}
|
||||
|
||||
/// Answer `path` with a redirect to `location`.
|
||||
pub fn redirect(&self, path: &str, location: &str) {
|
||||
self.shared
|
||||
.redirects
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(path.to_string(), location.to_string());
|
||||
}
|
||||
|
||||
/// Whether any request the server counted carried header `name`.
|
||||
pub fn saw_header(&self, name: &str) -> bool {
|
||||
self.shared
|
||||
.seen
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|(_, h)| h.contains_key(name))
|
||||
}
|
||||
|
||||
/// The ranges asked for so far.
|
||||
@@ -242,6 +266,16 @@ fn serve(conn: TcpStream, s: &Shared) -> std::io::Result<()> {
|
||||
let close = headers
|
||||
.get("connection")
|
||||
.is_some_and(|v| v.eq_ignore_ascii_case("close"));
|
||||
let redirect = s.redirects.read().unwrap().get(&path).cloned();
|
||||
if let Some(location) = redirect {
|
||||
s.requests.fetch_add(1, Ordering::SeqCst);
|
||||
s.seen.lock().unwrap().push((path.clone(), headers.clone()));
|
||||
write!(
|
||||
out,
|
||||
"HTTP/1.1 302 Found\r\nLocation: {location}\r\nContent-Length: 0\r\n\r\n"
|
||||
)?;
|
||||
continue;
|
||||
}
|
||||
let res = {
|
||||
let files = s.files.read().unwrap();
|
||||
files
|
||||
@@ -257,6 +291,7 @@ fn serve(conn: TcpStream, s: &Shared) -> std::io::Result<()> {
|
||||
continue;
|
||||
};
|
||||
s.requests.fetch_add(1, Ordering::SeqCst);
|
||||
s.seen.lock().unwrap().push((path.clone(), headers.clone()));
|
||||
let delay = s.delay_ms.load(Ordering::SeqCst);
|
||||
if delay > 0 {
|
||||
std::thread::sleep(Duration::from_millis(delay));
|
||||
|
||||
Reference in New Issue
Block a user