refactor: keep http-related code together
This commit is contained in:
committed by
Martin Algesten
parent
7dcd60cbaf
commit
06db500ec4
190
src/http_interop.rs
Normal file
190
src/http_interop.rs
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
use std::{
|
||||||
|
io::{Cursor, Read},
|
||||||
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{header::HeaderLine, response::ResponseStatusIndex, Request, Response};
|
||||||
|
|
||||||
|
impl<T: AsRef<[u8]> + Send + Sync + 'static> From<http::Response<T>> for Response {
|
||||||
|
fn from(value: http::Response<T>) -> Self {
|
||||||
|
let version_str = format!("{:?}", value.version());
|
||||||
|
let status_line = format!("{} {}", version_str, value.status());
|
||||||
|
let status_num = u16::from(value.status());
|
||||||
|
Response {
|
||||||
|
url: "https://example.com/".parse().unwrap(),
|
||||||
|
status_line,
|
||||||
|
index: ResponseStatusIndex {
|
||||||
|
http_version: version_str.len(),
|
||||||
|
response_code: version_str.len() + status_num.to_string().len(),
|
||||||
|
},
|
||||||
|
status: status_num,
|
||||||
|
headers: value
|
||||||
|
.headers()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(name, value)| {
|
||||||
|
let mut raw_header: Vec<u8> = name.to_string().into_bytes();
|
||||||
|
raw_header.extend([0x3a, 0x20]); // ": "
|
||||||
|
raw_header.extend(value.as_bytes());
|
||||||
|
|
||||||
|
HeaderLine::from(raw_header).into_header().ok()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
reader: Box::new(Cursor::new(value.into_body())),
|
||||||
|
remote_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 80),
|
||||||
|
history: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_builder(response: &Response) -> http::response::Builder {
|
||||||
|
let http_version = match response.http_version() {
|
||||||
|
"HTTP/0.9" => http::Version::HTTP_09,
|
||||||
|
"HTTP/1.0" => http::Version::HTTP_10,
|
||||||
|
"HTTP/1.1" => http::Version::HTTP_11,
|
||||||
|
"HTTP/2.0" => http::Version::HTTP_2,
|
||||||
|
"HTTP/3.0" => http::Version::HTTP_3,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response_builder = response
|
||||||
|
.headers
|
||||||
|
.iter()
|
||||||
|
.filter_map(|header| {
|
||||||
|
header
|
||||||
|
.value()
|
||||||
|
.map(|safe_value| (header.name().to_owned(), safe_value.to_owned()))
|
||||||
|
})
|
||||||
|
.fold(http::Response::builder(), |builder, header| {
|
||||||
|
builder.header(header.0, header.1)
|
||||||
|
})
|
||||||
|
.status(response.status())
|
||||||
|
.version(http_version);
|
||||||
|
|
||||||
|
response_builder
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Response> for http::Response<Box<dyn Read + Send + Sync + 'static>> {
|
||||||
|
fn from(value: Response) -> Self {
|
||||||
|
create_builder(&value).body(value.into_reader()).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Response> for http::Response<String> {
|
||||||
|
fn from(value: Response) -> Self {
|
||||||
|
create_builder(&value)
|
||||||
|
.body(value.into_string().unwrap())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<http::request::Builder> for Request {
|
||||||
|
fn from(value: http::request::Builder) -> Self {
|
||||||
|
let mut new_request = crate::agent().request(
|
||||||
|
value.method_ref().map_or("GET", |m| m.as_str()),
|
||||||
|
&value
|
||||||
|
.uri_ref()
|
||||||
|
.map_or("https://example.com".to_string(), |u| u.to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(headers) = value.headers_ref() {
|
||||||
|
new_request = headers
|
||||||
|
.iter()
|
||||||
|
.filter_map(|header| {
|
||||||
|
header
|
||||||
|
.1
|
||||||
|
.to_str()
|
||||||
|
.ok()
|
||||||
|
.map(|str_value| (header.0.as_str(), str_value))
|
||||||
|
})
|
||||||
|
.fold(new_request, |request, header| {
|
||||||
|
request.set(header.0, header.1)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
new_request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Request> for http::request::Builder {
|
||||||
|
fn from(value: Request) -> Self {
|
||||||
|
value
|
||||||
|
.headers
|
||||||
|
.iter()
|
||||||
|
.filter_map(|header| {
|
||||||
|
header
|
||||||
|
.value()
|
||||||
|
.map(|safe_value| (header.name().to_owned(), safe_value.to_owned()))
|
||||||
|
})
|
||||||
|
.fold(http::Request::builder(), |builder, header| {
|
||||||
|
builder.header(header.0, header.1)
|
||||||
|
})
|
||||||
|
.method(value.method())
|
||||||
|
.version(http::Version::HTTP_11)
|
||||||
|
.uri(value.url())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_http_response() {
|
||||||
|
use http::{Response, StatusCode, Version};
|
||||||
|
|
||||||
|
let http_response_body = (0..10240).into_iter().map(|_| 0xaa).collect::<Vec<u8>>();
|
||||||
|
let http_response = Response::builder()
|
||||||
|
.version(Version::HTTP_2)
|
||||||
|
.header("Custom-Header", "custom value")
|
||||||
|
.header("Content-Type", "application/octet-stream")
|
||||||
|
.status(StatusCode::IM_A_TEAPOT)
|
||||||
|
.body(http_response_body.clone())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response: super::Response = http_response.into();
|
||||||
|
assert_eq!(response.get_url(), "https://example.com/");
|
||||||
|
assert_eq!(response.http_version(), "HTTP/2.0");
|
||||||
|
assert_eq!(response.status(), u16::from(StatusCode::IM_A_TEAPOT));
|
||||||
|
assert_eq!(response.status_text(), "I'm a teapot");
|
||||||
|
assert_eq!(response.remote_addr().to_string().as_str(), "127.0.0.1:80");
|
||||||
|
assert_eq!(response.header("Custom-Header"), Some("custom value"));
|
||||||
|
assert_eq!(response.content_type(), "application/octet-stream");
|
||||||
|
|
||||||
|
let mut body_buf: Vec<u8> = vec![];
|
||||||
|
response.into_reader().read_to_end(&mut body_buf).unwrap();
|
||||||
|
assert_eq!(body_buf, http_response_body);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_http_response_string() {
|
||||||
|
use http::{Response, StatusCode, Version};
|
||||||
|
|
||||||
|
let http_response_body = "Some body string".to_string();
|
||||||
|
let http_response = Response::builder()
|
||||||
|
.version(Version::HTTP_11)
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(http_response_body.clone())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response: super::Response = http_response.into();
|
||||||
|
assert_eq!(response.get_url(), "https://example.com/");
|
||||||
|
assert_eq!(response.content_type(), "text/plain");
|
||||||
|
assert_eq!(response.into_string().unwrap(), http_response_body);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_http_response_bad_header() {
|
||||||
|
use http::{Response, StatusCode, Version};
|
||||||
|
|
||||||
|
let http_response = Response::builder()
|
||||||
|
.version(Version::HTTP_11)
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header("Some-Invalid-Header", vec![0xde, 0xad, 0xbe, 0xef])
|
||||||
|
.header("Some-Valid-Header", vec![0x48, 0x45, 0x4c, 0x4c, 0x4f])
|
||||||
|
.body(vec![])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response: super::Response = http_response.into();
|
||||||
|
assert_eq!(response.header("Some-Invalid-Header"), None);
|
||||||
|
assert_eq!(response.header("Some-Valid-Header"), Some("HELLO"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -412,6 +412,9 @@ mod test;
|
|||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
mod testserver;
|
mod testserver;
|
||||||
|
|
||||||
|
#[cfg(feature = "http-interop")]
|
||||||
|
mod http_interop;
|
||||||
|
|
||||||
pub use crate::agent::Agent;
|
pub use crate::agent::Agent;
|
||||||
pub use crate::agent::AgentBuilder;
|
pub use crate::agent::AgentBuilder;
|
||||||
pub use crate::agent::RedirectAuthHeaders;
|
pub use crate::agent::RedirectAuthHeaders;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ pub struct Request {
|
|||||||
agent: Agent,
|
agent: Agent,
|
||||||
method: String,
|
method: String,
|
||||||
url: String,
|
url: String,
|
||||||
headers: Vec<Header>,
|
pub(crate) headers: Vec<Header>,
|
||||||
timeout: Option<time::Duration>,
|
timeout: Option<time::Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,55 +500,6 @@ impl Request {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "http-interop")]
|
|
||||||
impl From<http::request::Builder> for Request {
|
|
||||||
fn from(value: http::request::Builder) -> Self {
|
|
||||||
let mut new_request = crate::agent().request(
|
|
||||||
value.method_ref().map_or("GET", |m| m.as_str()),
|
|
||||||
&value
|
|
||||||
.uri_ref()
|
|
||||||
.map_or("https://example.com".to_string(), |u| u.to_string()),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(headers) = value.headers_ref() {
|
|
||||||
new_request = headers
|
|
||||||
.iter()
|
|
||||||
.filter_map(|header| {
|
|
||||||
header
|
|
||||||
.1
|
|
||||||
.to_str()
|
|
||||||
.ok()
|
|
||||||
.map(|str_value| (header.0.as_str(), str_value))
|
|
||||||
})
|
|
||||||
.fold(new_request, |request, header| {
|
|
||||||
request.set(header.0, header.1)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
new_request
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "http-interop")]
|
|
||||||
impl From<Request> for http::request::Builder {
|
|
||||||
fn from(value: Request) -> Self {
|
|
||||||
value
|
|
||||||
.headers
|
|
||||||
.iter()
|
|
||||||
.filter_map(|header| {
|
|
||||||
header
|
|
||||||
.value()
|
|
||||||
.map(|safe_value| (header.name().to_owned(), safe_value.to_owned()))
|
|
||||||
})
|
|
||||||
.fold(http::Request::builder(), |builder, header| {
|
|
||||||
builder.header(header.0, header.1)
|
|
||||||
})
|
|
||||||
.method(value.method())
|
|
||||||
.version(http::Version::HTTP_11)
|
|
||||||
.uri(value.url())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parsed result of a request url with handy inspection methods.
|
/// Parsed result of a request url with handy inspection methods.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RequestUrl {
|
pub struct RequestUrl {
|
||||||
|
|||||||
161
src/response.rs
161
src/response.rs
@@ -28,9 +28,6 @@ use flate2::read::MultiGzDecoder;
|
|||||||
#[cfg(feature = "brotli")]
|
#[cfg(feature = "brotli")]
|
||||||
use brotli_decompressor::Decompressor as BrotliDecoder;
|
use brotli_decompressor::Decompressor as BrotliDecoder;
|
||||||
|
|
||||||
#[cfg(feature = "http-interop")]
|
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
|
||||||
|
|
||||||
pub const DEFAULT_CONTENT_TYPE: &str = "text/plain";
|
pub const DEFAULT_CONTENT_TYPE: &str = "text/plain";
|
||||||
pub const DEFAULT_CHARACTER_SET: &str = "utf-8";
|
pub const DEFAULT_CHARACTER_SET: &str = "utf-8";
|
||||||
const INTO_STRING_LIMIT: usize = 10 * 1_024 * 1_024;
|
const INTO_STRING_LIMIT: usize = 10 * 1_024 * 1_024;
|
||||||
@@ -74,13 +71,13 @@ enum BodyType {
|
|||||||
/// ```
|
/// ```
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
pub(crate) url: Url,
|
pub(crate) url: Url,
|
||||||
status_line: String,
|
pub(crate) status_line: String,
|
||||||
index: ResponseStatusIndex,
|
pub(crate) index: ResponseStatusIndex,
|
||||||
status: u16,
|
pub(crate) status: u16,
|
||||||
headers: Vec<Header>,
|
pub(crate) headers: Vec<Header>,
|
||||||
reader: Box<dyn Read + Send + Sync + 'static>,
|
pub(crate) reader: Box<dyn Read + Send + Sync + 'static>,
|
||||||
/// The socket address of the server that sent the response.
|
/// The socket address of the server that sent the response.
|
||||||
remote_addr: SocketAddr,
|
pub(crate) remote_addr: SocketAddr,
|
||||||
/// The redirect history of this response, if any. The history starts with
|
/// The redirect history of this response, if any. The history starts with
|
||||||
/// the first response received and ends with the response immediately
|
/// the first response received and ends with the response immediately
|
||||||
/// previous to this one.
|
/// previous to this one.
|
||||||
@@ -91,9 +88,9 @@ pub struct Response {
|
|||||||
|
|
||||||
/// index into status_line where we split: HTTP/1.1 200 OK
|
/// index into status_line where we split: HTTP/1.1 200 OK
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
struct ResponseStatusIndex {
|
pub(crate) struct ResponseStatusIndex {
|
||||||
http_version: usize,
|
pub(crate) http_version: usize,
|
||||||
response_code: usize,
|
pub(crate) response_code: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Response {
|
impl fmt::Debug for Response {
|
||||||
@@ -875,82 +872,6 @@ impl Read for ErrorReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "http-interop")]
|
|
||||||
impl<T: AsRef<[u8]> + Send + Sync + 'static> From<http::Response<T>> for Response {
|
|
||||||
fn from(value: http::Response<T>) -> Self {
|
|
||||||
let version_str = format!("{:?}", value.version());
|
|
||||||
let status_line = format!("{} {}", version_str, value.status());
|
|
||||||
let status_num = u16::from(value.status());
|
|
||||||
Response {
|
|
||||||
url: "https://example.com/".parse().unwrap(),
|
|
||||||
status_line,
|
|
||||||
index: ResponseStatusIndex {
|
|
||||||
http_version: version_str.len(),
|
|
||||||
response_code: version_str.len() + status_num.to_string().len(),
|
|
||||||
},
|
|
||||||
status: status_num,
|
|
||||||
headers: value
|
|
||||||
.headers()
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(name, value)| {
|
|
||||||
let mut raw_header: Vec<u8> = name.to_string().into_bytes();
|
|
||||||
raw_header.extend([0x3a, 0x20]); // ": "
|
|
||||||
raw_header.extend(value.as_bytes());
|
|
||||||
|
|
||||||
HeaderLine::from(raw_header).into_header().ok()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
reader: Box::new(std::io::Cursor::new(value.into_body())),
|
|
||||||
remote_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 80),
|
|
||||||
history: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "http-interop")]
|
|
||||||
fn create_builder(response: &Response) -> http::response::Builder {
|
|
||||||
let http_version = match response.http_version() {
|
|
||||||
"HTTP/0.9" => http::Version::HTTP_09,
|
|
||||||
"HTTP/1.0" => http::Version::HTTP_10,
|
|
||||||
"HTTP/1.1" => http::Version::HTTP_11,
|
|
||||||
"HTTP/2.0" => http::Version::HTTP_2,
|
|
||||||
"HTTP/3.0" => http::Version::HTTP_3,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let response_builder = response
|
|
||||||
.headers
|
|
||||||
.iter()
|
|
||||||
.filter_map(|header| {
|
|
||||||
header
|
|
||||||
.value()
|
|
||||||
.map(|safe_value| (header.name().to_owned(), safe_value.to_owned()))
|
|
||||||
})
|
|
||||||
.fold(http::Response::builder(), |builder, header| {
|
|
||||||
builder.header(header.0, header.1)
|
|
||||||
})
|
|
||||||
.status(response.status())
|
|
||||||
.version(http_version);
|
|
||||||
|
|
||||||
response_builder
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "http-interop")]
|
|
||||||
impl From<Response> for http::Response<Box<dyn Read + Send + Sync + 'static>> {
|
|
||||||
fn from(value: Response) -> Self {
|
|
||||||
create_builder(&value).body(value.into_reader()).unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "http-interop")]
|
|
||||||
impl From<Response> for http::Response<String> {
|
|
||||||
fn from(value: Response) -> Self {
|
|
||||||
create_builder(&value)
|
|
||||||
.body(value.into_string().unwrap())
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
@@ -1289,68 +1210,4 @@ mod tests {
|
|||||||
let body = resp.into_string().unwrap();
|
let body = resp.into_string().unwrap();
|
||||||
assert_eq!(body, "hi\n");
|
assert_eq!(body, "hi\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(feature = "http-interop")]
|
|
||||||
fn convert_http_response() {
|
|
||||||
use http::{Response, StatusCode, Version};
|
|
||||||
|
|
||||||
let http_response_body = (0..10240).into_iter().map(|_| 0xaa).collect::<Vec<u8>>();
|
|
||||||
let http_response = Response::builder()
|
|
||||||
.version(Version::HTTP_2)
|
|
||||||
.header("Custom-Header", "custom value")
|
|
||||||
.header("Content-Type", "application/octet-stream")
|
|
||||||
.status(StatusCode::IM_A_TEAPOT)
|
|
||||||
.body(http_response_body.clone())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let response: super::Response = http_response.into();
|
|
||||||
assert_eq!(response.get_url(), "https://example.com/");
|
|
||||||
assert_eq!(response.http_version(), "HTTP/2.0");
|
|
||||||
assert_eq!(response.status(), u16::from(StatusCode::IM_A_TEAPOT));
|
|
||||||
assert_eq!(response.status_text(), "I'm a teapot");
|
|
||||||
assert_eq!(response.remote_addr().to_string().as_str(), "127.0.0.1:80");
|
|
||||||
assert_eq!(response.header("Custom-Header"), Some("custom value"));
|
|
||||||
assert_eq!(response.content_type(), "application/octet-stream");
|
|
||||||
|
|
||||||
let mut body_buf: Vec<u8> = vec![];
|
|
||||||
response.into_reader().read_to_end(&mut body_buf).unwrap();
|
|
||||||
assert_eq!(body_buf, http_response_body);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(feature = "http-interop")]
|
|
||||||
fn convert_http_response_string() {
|
|
||||||
use http::{Response, StatusCode, Version};
|
|
||||||
|
|
||||||
let http_response_body = "Some body string".to_string();
|
|
||||||
let http_response = Response::builder()
|
|
||||||
.version(Version::HTTP_11)
|
|
||||||
.status(StatusCode::OK)
|
|
||||||
.body(http_response_body.clone())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let response: super::Response = http_response.into();
|
|
||||||
assert_eq!(response.get_url(), "https://example.com/");
|
|
||||||
assert_eq!(response.content_type(), "text/plain");
|
|
||||||
assert_eq!(response.into_string().unwrap(), http_response_body);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(feature = "http-interop")]
|
|
||||||
fn convert_http_response_bad_header() {
|
|
||||||
use http::{Response, StatusCode, Version};
|
|
||||||
|
|
||||||
let http_response = Response::builder()
|
|
||||||
.version(Version::HTTP_11)
|
|
||||||
.status(StatusCode::OK)
|
|
||||||
.header("Some-Invalid-Header", vec![0xde, 0xad, 0xbe, 0xef])
|
|
||||||
.header("Some-Valid-Header", vec![0x48, 0x45, 0x4c, 0x4c, 0x4f])
|
|
||||||
.body(vec![])
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let response: super::Response = http_response.into();
|
|
||||||
assert_eq!(response.header("Some-Invalid-Header"), None);
|
|
||||||
assert_eq!(response.header("Some-Valid-Header"), Some("HELLO"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user