// Copyright (c) 2016-2018 The http-serve developers // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. use bytes::Bytes; use futures_core::Stream; use futures_util::{future, stream}; use http::header::HeaderValue; use http::{Request, Response}; use http_serve::Body; use hyper_util::rt::TokioIo; use once_cell::sync::Lazy; use std::ops::Range; use std::pin::Pin; use std::time::SystemTime; use tokio::net::TcpListener; type BoxError = Box; static BODY: &'static [u8] = b"01234567890123456789012345678901234567890123456789012345678901234567890123456789\ 01234567890123456789012345678901234567890123456789012345678901234567890123456789\ 01234567890123456789012345678901234567890123456789012345678901234567890123456789"; struct FakeEntity { etag: Option, last_modified: SystemTime, } impl http_serve::Entity for &'static FakeEntity { type Data = bytes::Bytes; type Error = Box; fn len(&self) -> u64 { BODY.len() as u64 } fn get_range( &self, range: Range, ) -> Pin> + Send + Sync>> { Box::pin(stream::once(future::ok( BODY[range.start as usize..range.end as usize].into(), ))) } fn add_headers(&self, headers: &mut http::header::HeaderMap) { headers.insert( http::header::CONTENT_TYPE, HeaderValue::from_static("application/octet-stream"), ); } fn etag(&self) -> Option { self.etag.clone() } fn last_modified(&self) -> Option { Some(self.last_modified) } } async fn serve( req: Request, ) -> Result>, BoxError> { let entity: &'static FakeEntity = match req.uri().path() { "/none" => &*ENTITY_NO_ETAG, "/strong" => &*ENTITY_STRONG_ETAG, "/weak" => &*ENTITY_WEAK_ETAG, p => panic!("unexpected path {}", p), }; Ok(http_serve::serve(entity, &req)) } fn new_server() -> String { let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); let _guard = rt.enter(); rt.block_on(async { let addr = std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, 0)); let listener = TcpListener::bind(addr).await.unwrap(); let addr = listener.local_addr().unwrap(); tx.send(addr).unwrap(); loop { let (tcp, _) = listener.accept().await.unwrap(); let io = TokioIo::new(tcp); tokio::task::spawn(async move { hyper::server::conn::http1::Builder::new() .serve_connection(io, hyper::service::service_fn(serve)) .await .unwrap(); }); } }); }); let addr = rx.recv().unwrap(); format!("http://{}:{}", addr.ip(), addr.port()) } const SOME_DATE_STR: &str = "Sun, 06 Nov 1994 08:49:37 GMT"; const LATER_DATE_STR: &str = "Sun, 06 Nov 1994 09:49:37 GMT"; const MIME: &str = "application/octet-stream"; static SOME_DATE: Lazy = Lazy::new(|| httpdate::parse_http_date(SOME_DATE_STR).unwrap()); static ENTITY_NO_ETAG: Lazy = Lazy::new(|| FakeEntity { etag: None, last_modified: *SOME_DATE, }); static ENTITY_STRONG_ETAG: Lazy = Lazy::new(|| FakeEntity { etag: Some(HeaderValue::from_static("\"foo\"")), last_modified: *SOME_DATE, }); static ENTITY_WEAK_ETAG: Lazy = Lazy::new(|| FakeEntity { etag: Some(HeaderValue::from_static("W/\"foo\"")), last_modified: *SOME_DATE, }); static SERVER: Lazy = Lazy::new(new_server); #[tokio::test] async fn serve_without_etag() { let client = reqwest::Client::new(); let url = format!("{}/none", *SERVER); // Full body. let resp = client.get(&url).send().await.unwrap(); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), MIME ); assert!(resp .headers() .get(reqwest::header::CONTENT_LENGTH) .is_some()); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); let buf = resp.bytes().await.unwrap(); assert_eq!(BODY, &buf[..]); // If-Match any should still send the full body. let resp = client .get(&url) .header("If-Match", "*") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), MIME ); assert!(resp .headers() .get(reqwest::header::CONTENT_LENGTH) .is_some()); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); let buf = resp.bytes().await.unwrap(); assert_eq!(BODY, &buf[..]); // If-Match by etag doesn't match (as this request has no etag). let resp = client .get(&url) .header("If-Match", "\"foo\"") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::PRECONDITION_FAILED, resp.status()); // If-None-Match any. let resp = client .get(&url) .header("If-None-Match", "*") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::NOT_MODIFIED, resp.status()); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); let buf = resp.bytes().await.unwrap(); assert_eq!(b"", &buf[..]); // If-None-Match by etag doesn't match (as this request has no etag). let resp = client .get(&url) .header("If-None-Match", "\"foo\"") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), MIME ); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); let buf = resp.bytes().await.unwrap(); assert_eq!(BODY, &buf[..]); // Unmodified since supplied date. let resp = client .get(&url) .header("If-Modified-Since", SOME_DATE_STR) .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::NOT_MODIFIED, resp.status()); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); let buf = resp.bytes().await.unwrap(); assert_eq!(b"", &buf[..]); // Range serving - basic case. let resp = client .get(&url) .header("Range", "bytes=1-3") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::PARTIAL_CONTENT, resp.status()); assert!(resp .headers() .get(reqwest::header::CONTENT_LENGTH) .is_some()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_RANGE).unwrap(), &format!("bytes 1-3/{}", BODY.len()) ); let buf = resp.bytes().await.unwrap(); assert_eq!(b"123", &buf[..]); // Range serving - multiple ranges. let resp = client .get(&url) .header("Range", "bytes=0-1, 3-4") .send() .await .unwrap(); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); assert_eq!(reqwest::StatusCode::PARTIAL_CONTENT, resp.status()); assert!(resp .headers() .get(reqwest::header::CONTENT_LENGTH) .is_some()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), &"multipart/byteranges; boundary=B" ); let buf = resp.bytes().await.unwrap(); assert_eq!( &b"\ \r\n--B\r\n\ Content-Range: bytes 0-1/240\r\n\ content-type: application/octet-stream\r\n\ \r\n\ 01\r\n\ --B\r\n\ Content-Range: bytes 3-4/240\r\n\ content-type: application/octet-stream\r\n\ \r\n\ 34\r\n\ --B--\r\n"[..], &buf[..] ); // Range serving - multiple ranges which are less efficient than sending the whole. let resp = client .get(&url) .header("Range", "bytes=0-100, 120-240") .send() .await .unwrap(); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); assert!(resp .headers() .get(reqwest::header::CONTENT_LENGTH) .is_some()); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), MIME ); let buf = resp.bytes().await.unwrap(); assert_eq!(BODY, &buf[..]); // Range serving - not satisfiable. let resp = client .get(&url) .header("Range", "bytes=500-") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::RANGE_NOT_SATISFIABLE, resp.status()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_RANGE).unwrap(), &format!("bytes */{}", BODY.len()) ); let buf = resp.bytes().await.unwrap(); assert_eq!(b"", &buf[..]); // Range serving - matching If-Range by date doesn't honor the range. let resp = client .get(&url) .header("Range", "bytes=1-3") .header("If-Range", SOME_DATE_STR) .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), MIME ); let buf = resp.bytes().await.unwrap(); assert_eq!(BODY, &buf[..]); // Range serving - non-matching If-Range by date ignores the range. let resp = client .get(&url) .header("Range", "bytes=1-3") .header("If-Range", LATER_DATE_STR) .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), MIME ); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); let buf = resp.bytes().await.unwrap(); assert_eq!(BODY, &buf[..]); // Range serving - this resource has no etag, so any If-Range by etag ignores the range. let resp = client .get(&url) .header("Range", "bytes=1-3") .header("If-Range", "\"foo\"") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), MIME ); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); let buf = resp.bytes().await.unwrap(); assert_eq!(BODY, &buf[..]); } #[tokio::test] async fn serve_with_strong_etag() { let client = reqwest::Client::new(); let url = format!("{}/strong", *SERVER); // If-Match any should still send the full body. let resp = client .get(&url) .header("If-Match", "*") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), MIME ); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); assert_eq!(BODY, &resp.bytes().await.unwrap()[..]); // If-Match by matching etag should send the full body. let resp = client .get(&url) .header("If-Match", "\"foo\"") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), MIME ); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); assert_eq!(BODY, &resp.bytes().await.unwrap()[..]); // If-Match by etag which doesn't match. let resp = client .get(&url) .header("If-Match", "\"bar\"") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::PRECONDITION_FAILED, resp.status()); // If-None-Match by etag which matches. let resp = client .get(&url) .header("If-None-Match", "\"foo\"") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::NOT_MODIFIED, resp.status()); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); assert_eq!(b"", &resp.bytes().await.unwrap()[..]); // If-None-Match by etag which doesn't match. let resp = client .get(&url) .header("If-None-Match", "\"bar\"") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); assert_eq!(BODY, &resp.bytes().await.unwrap()[..]); // If-None-Match by etag which doesn't match, If-Modified-Since which does. let resp = client .get(&url) .header("If-None-Match", "\"bar\"") .header("If-Modified-Since", SOME_DATE_STR) .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); assert_eq!(BODY, &resp.bytes().await.unwrap()[..]); // Range serving - If-Range matching by etag. let resp = client .get(&url) .header("Range", "bytes=1-3") .header("If-Range", "\"foo\"") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::PARTIAL_CONTENT, resp.status()); assert_eq!(None, resp.headers().get(reqwest::header::CONTENT_TYPE)); assert_eq!( resp.headers().get(reqwest::header::CONTENT_RANGE).unwrap(), &format!("bytes 1-3/{}", BODY.len()) ); assert_eq!(b"123", &resp.bytes().await.unwrap()[..]); // Range serving - If-Range not matching by etag. let resp = client .get(&url) .header("Range", "bytes=1-3") .header("If-Range", "\"bar\"") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), MIME ); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); assert_eq!(BODY, &resp.bytes().await.unwrap()[..]); } #[tokio::test] async fn serve_with_weak_etag() { let client = reqwest::Client::new(); let url = format!("{}/weak", *SERVER); // If-Match any should still send the full body. let resp = client .get(&url) .header("If-Match", "*") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), MIME ); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); assert_eq!(BODY, &resp.bytes().await.unwrap()[..]); // If-Match by etag doesn't match because matches use the strong comparison function. let resp = client .get(&url) .header("If-Match", "W/\"foo\"") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::PRECONDITION_FAILED, resp.status()); // If-None-Match by identical weak etag is sufficient. let resp = client .get(&url) .header("If-None-Match", "W/\"foo\"") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::NOT_MODIFIED, resp.status()); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); assert_eq!(b"", &resp.bytes().await.unwrap()[..]); // If-None-Match by etag which doesn't match. let resp = client .get(&url) .header("If-None-Match", "W/\"bar\"") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); assert_eq!(BODY, &resp.bytes().await.unwrap()[..]); // Range serving - If-Range matching by weak etag isn't sufficient. let resp = client .get(&url) .header("Range", "bytes=1-3") .header("If-Range", "\"foo\"") .send() .await .unwrap(); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), MIME ); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); assert_eq!(BODY, &resp.bytes().await.unwrap()[..]); } // TODO: stream that returns too much data. // TODO: stream that returns too little data.