Skip to content

Commit

Permalink
preserve h1 header order+casing & expose in ramaproxy.org FP/Echo (#375)
Browse files Browse the repository at this point in the history
a rama-driven http proxy should now preserve h1 and pseudo headers
from original request :) (order / casing)
  • Loading branch information
GlenDC authored Jan 1, 2025
1 parent 1e2acae commit 65e4884
Show file tree
Hide file tree
Showing 29 changed files with 1,310 additions and 727 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 13 additions & 16 deletions rama-cli/src/cmd/fp/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ use super::State;
use rama::{
error::{BoxError, ErrorContext},
http::{
core::h2::{PseudoHeader, PseudoHeaderOrder},
dep::http::request::Parts,
dep::http::{request::Parts, Extensions},
headers::Forwarded,
Request,
proto::{h1::Http1HeaderMap, h2::PseudoHeaderOrder},
HeaderMap,
},
net::{http::RequestContext, stream::SocketInfo},
tls::types::{
Expand Down Expand Up @@ -187,27 +187,24 @@ pub(super) async fn get_request_info(
#[derive(Debug, Clone, Serialize)]
pub(super) struct HttpInfo {
pub(super) headers: Vec<(String, String)>,
pub(super) pseudo_headers: Option<Vec<PseudoHeader>>,
pub(super) pseudo_headers: Option<Vec<String>>,
}

pub(super) fn get_http_info(req: &Request) -> HttpInfo {
// TODO: get in correct order
// TODO: get in correct case
let headers = req
.headers()
.iter()
pub(super) fn get_http_info(headers: HeaderMap, ext: &mut Extensions) -> HttpInfo {
let headers: Vec<_> = Http1HeaderMap::new(headers, Some(ext))
.into_iter()
.map(|(name, value)| {
(
name.as_str().to_owned(),
value.to_str().map(|v| v.to_owned()).unwrap_or_default(),
name.to_string(),
std::str::from_utf8(value.as_bytes())
.map(|s| s.to_owned())
.unwrap_or_else(|_| format!("0x{:x?}", value.as_bytes())),
)
})
.collect();

let pseudo_headers: Option<Vec<_>> = req
.extensions()
let pseudo_headers: Option<Vec<_>> = ext
.get::<PseudoHeaderOrder>()
.map(|o| o.iter().collect());
.map(|o| o.iter().map(|p| p.to_string()).collect());

HttpInfo {
headers,
Expand Down
53 changes: 26 additions & 27 deletions rama-cli/src/cmd/fp/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,7 @@ pub(super) async fn get_report(
mut ctx: Context<Arc<State>>,
req: Request,
) -> Result<Html, Response> {
let http_info = get_http_info(&req);

let (parts, _) = req.into_parts();
let (mut parts, _) = req.into_parts();

let user_agent_info = get_user_agent_info(&ctx).await;

Expand All @@ -95,6 +93,8 @@ pub(super) async fn get_report(
.await
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?;

let http_info = get_http_info(parts.headers, &mut parts.extensions);

let head = r#"<script src="/assets/script.js"></script>"#.to_owned();

let mut tables = vec![
Expand All @@ -110,14 +110,7 @@ pub(super) async fn get_report(
if let Some(pseudo) = http_info.pseudo_headers {
tables.push(Table {
title: "🚗 H2 Pseudo Headers".to_owned(),
rows: vec![(
"order".to_owned(),
pseudo
.into_iter()
.map(|h| h.as_str())
.collect::<Vec<_>>()
.join(", "),
)],
rows: vec![("order".to_owned(), pseudo.join(", "))],
});
}

Expand Down Expand Up @@ -174,9 +167,7 @@ pub(super) async fn get_api_fetch_number(
mut ctx: Context<Arc<State>>,
req: Request,
) -> Result<Json<serde_json::Value>, Response> {
let http_info = get_http_info(&req);

let (parts, _) = req.into_parts();
let (mut parts, _) = req.into_parts();

let user_agent_info = get_user_agent_info(&ctx).await;

Expand All @@ -190,6 +181,8 @@ pub(super) async fn get_api_fetch_number(
.await
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?;

let http_info = get_http_info(parts.headers, &mut parts.extensions);

let tls_info = get_tls_display_info(&ctx);

Ok(Json(json!({
Expand All @@ -208,9 +201,7 @@ pub(super) async fn post_api_fetch_number(
mut ctx: Context<Arc<State>>,
req: Request,
) -> Result<Json<serde_json::Value>, Response> {
let http_info = get_http_info(&req);

let (parts, _) = req.into_parts();
let (mut parts, _) = req.into_parts();

let user_agent_info = get_user_agent_info(&ctx).await;

Expand All @@ -224,6 +215,8 @@ pub(super) async fn post_api_fetch_number(
.await
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?;

let http_info = get_http_info(parts.headers, &mut parts.extensions);

let tls_info = get_tls_display_info(&ctx);

Ok(Json(json!({
Expand All @@ -241,9 +234,7 @@ pub(super) async fn get_api_xml_http_request_number(
mut ctx: Context<Arc<State>>,
req: Request,
) -> Result<Json<serde_json::Value>, Response> {
let http_info = get_http_info(&req);

let (parts, _) = req.into_parts();
let (mut parts, _) = req.into_parts();

let user_agent_info = get_user_agent_info(&ctx).await;

Expand All @@ -257,6 +248,8 @@ pub(super) async fn get_api_xml_http_request_number(
.await
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?;

let http_info = get_http_info(parts.headers, &mut parts.extensions);

Ok(Json(json!({
"number": ctx.state().counter.fetch_add(1, std::sync::atomic::Ordering::AcqRel),
"fp": {
Expand All @@ -272,9 +265,7 @@ pub(super) async fn post_api_xml_http_request_number(
mut ctx: Context<Arc<State>>,
req: Request,
) -> Result<Json<serde_json::Value>, Response> {
let http_info = get_http_info(&req);

let (parts, _) = req.into_parts();
let (mut parts, _) = req.into_parts();

let user_agent_info = get_user_agent_info(&ctx).await;

Expand All @@ -288,6 +279,8 @@ pub(super) async fn post_api_xml_http_request_number(
.await
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?;

let http_info = get_http_info(parts.headers, &mut parts.extensions);

let tls_info = get_tls_display_info(&ctx);

Ok(Json(json!({
Expand All @@ -308,10 +301,7 @@ pub(super) async fn post_api_xml_http_request_number(
pub(super) async fn form(mut ctx: Context<Arc<State>>, req: Request) -> Result<Html, Response> {
// TODO: get TLS Info (for https access only)
// TODO: support HTTP1, HTTP2 and AUTO (for now we are only doing auto)

let http_info = get_http_info(&req);

let (parts, _) = req.into_parts();
let (mut parts, _) = req.into_parts();

let user_agent_info = get_user_agent_info(&ctx).await;

Expand All @@ -325,6 +315,8 @@ pub(super) async fn form(mut ctx: Context<Arc<State>>, req: Request) -> Result<H
.await
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?;

let http_info = get_http_info(parts.headers, &mut parts.extensions);

let mut content = String::new();

content.push_str(r##"<a href="/report" title="Back to Home">🏠 Back to Home...</a>"##);
Expand Down Expand Up @@ -354,6 +346,13 @@ pub(super) async fn form(mut ctx: Context<Arc<State>>, req: Request) -> Result<H
},
];

if let Some(pseudo) = http_info.pseudo_headers {
tables.push(Table {
title: "🚗 H2 Pseudo Headers".to_owned(),
rows: vec![("order".to_owned(), pseudo.join(", "))],
});
}

let tls_info = get_tls_display_info(&ctx);
if let Some(tls_info) = tls_info {
let mut tls_tables = tls_info.into();
Expand Down
1 change: 0 additions & 1 deletion rama-http-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ pin-project-lite = { workspace = true }
rama-core = { version = "0.2.0-alpha.5", path = "../rama-core" }
rama-http-types = { version = "0.2.0-alpha.5", path = "../rama-http-types" }
rama-utils = { version = "0.2.0-alpha.5", path = "../rama-utils" }
serde = { workspace = true }
slab = { workspace = true }
smallvec = { workspace = true }
tokio = { workspace = true, features = ["io-util"] }
Expand Down
146 changes: 0 additions & 146 deletions rama-http-core/src/ext/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
//! HTTP extensions.
use bytes::Bytes;
use rama_http_types::header::HeaderName;
use rama_http_types::header::{HeaderMap, IntoHeaderName, ValueIter};
use std::collections::HashMap;
use std::fmt;

mod h1_reason_phrase;
Expand Down Expand Up @@ -59,145 +55,3 @@ impl fmt::Debug for Protocol {
self.inner.fmt(f)
}
}

/// A map from header names to their original casing as received in an HTTP message.
///
/// If an HTTP/1 response `res` is parsed on a connection whose option
/// [`preserve_header_case`] was set to true and the response included
/// the following headers:
///
/// ```text
/// x-Bread: Baguette
/// X-BREAD: Pain
/// x-bread: Ficelle
/// ```
///
/// Then `res.extensions().get::<HeaderCaseMap>()` will return a map with:
///
/// ```text
/// HeaderCaseMap({
/// "x-bread": ["x-Bread", "X-BREAD", "x-bread"],
/// })
/// ```
///
/// [`preserve_header_case`]: /client/struct.Client.html#method.preserve_header_case
#[derive(Clone, Debug)]
pub(crate) struct HeaderCaseMap(HeaderMap<Bytes>);

impl HeaderCaseMap {
/// Returns a view of all spellings associated with that header name,
/// in the order they were found.
pub(crate) fn get_all<'a>(
&'a self,
name: &HeaderName,
) -> impl Iterator<Item = impl AsRef<[u8]> + 'a> + 'a {
self.get_all_internal(name)
}

/// Returns a view of all spellings associated with that header name,
/// in the order they were found.
pub(crate) fn get_all_internal(&self, name: &HeaderName) -> ValueIter<'_, Bytes> {
self.0.get_all(name).into_iter()
}

pub(crate) fn default() -> Self {
Self(Default::default())
}

#[allow(dead_code)]
pub(crate) fn insert(&mut self, name: HeaderName, orig: Bytes) {
self.0.insert(name, orig);
}

pub(crate) fn append<N>(&mut self, name: N, orig: Bytes)
where
N: IntoHeaderName,
{
self.0.append(name, orig);
}
}

#[derive(Clone, Debug, Default)]
/// Hashmap<Headername, numheaders with that name>
pub struct OriginalHeaderOrder {
/// Stores how many entries a Headername maps to. This is used
/// for accounting.
num_entries: HashMap<HeaderName, usize>,
/// Stores the ordering of the headers. ex: `vec[i] = (headerName, idx)`,
/// The vector is ordered such that the ith element
/// represents the ith header that came in off the line.
/// The `HeaderName` and `idx` are then used elsewhere to index into
/// the multi map that stores the header values.
entry_order: Vec<(HeaderName, usize)>,
}

impl OriginalHeaderOrder {
pub fn insert(&mut self, name: HeaderName) {
if !self.num_entries.contains_key(&name) {
let idx = 0;
self.num_entries.insert(name.clone(), 1);
self.entry_order.push((name, idx));
}
// Replacing an already existing element does not
// change ordering, so we only care if its the first
// header name encountered
}

pub fn append<N>(&mut self, name: N)
where
N: IntoHeaderName + Into<HeaderName> + Clone,
{
let name: HeaderName = name.into();
let idx;
if self.num_entries.contains_key(&name) {
idx = self.num_entries[&name];
*self.num_entries.get_mut(&name).unwrap() += 1;
} else {
idx = 0;
self.num_entries.insert(name.clone(), 1);
}
self.entry_order.push((name, idx));
}

/// This returns an iterator that provides header names and indexes
/// in the original order received.
///
/// # Examples
///
/// ```
/// use rama_http_core::ext::OriginalHeaderOrder;
/// use rama_http_types::header::{HeaderName, HeaderValue, HeaderMap};
///
/// let mut h_order = OriginalHeaderOrder::default();
/// let mut h_map = HeaderMap::new();
///
/// let name1 = HeaderName::try_from("Set-CookiE").expect("valid Set-CookiE header name");
/// let value1 = HeaderValue::from_static("a=b");
/// h_map.append(name1.clone(), value1);
/// h_order.append(name1);
///
/// let name2 = HeaderName::try_from("Content-Encoding").expect("valid Content-Encoding header name");
/// let value2 = HeaderValue::from_static("gzip");
/// h_map.append(name2.clone(), value2);
/// h_order.append(name2);
///
/// let name3 = HeaderName::try_from("SET-COOKIE").expect("valid SET-COOKIE header name");
/// let value3 = HeaderValue::from_static("c=d");
/// h_map.append(name3.clone(), value3);
/// h_order.append(name3);
///
/// let mut iter = h_order.get_in_order();
///
/// let (name, idx) = iter.next().unwrap();
/// assert_eq!("a=b", h_map.get_all(name).iter().nth(*idx).expect("get set-cookie header value"));
///
/// let (name, idx) = iter.next().unwrap();
/// assert_eq!("gzip", h_map.get_all(name).iter().nth(*idx).expect("get content-encoding header value"));
///
/// let (name, idx) = iter.next().unwrap();
/// assert_eq!("c=d", h_map.get_all(name).iter().nth(*idx).expect("get SET-COOKIE header value"));
/// ```
pub fn get_in_order(&self) -> impl Iterator<Item = &(HeaderName, usize)> {
self.entry_order.iter()
}
}
3 changes: 2 additions & 1 deletion rama-http-core/src/h2/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,11 @@ use crate::h2::codec::{Codec, SendError, UserError};
use crate::h2::ext::Protocol;
use crate::h2::frame::{Headers, Pseudo, Reason, Settings, StreamId};
use crate::h2::proto::{self, Error};
use crate::h2::{FlowControl, PingPong, PseudoHeaderOrder, RecvStream, SendStream};
use crate::h2::{FlowControl, PingPong, RecvStream, SendStream};

use bytes::{Buf, Bytes};
use rama_http_types::dep::http::{request, uri};
use rama_http_types::proto::h2::PseudoHeaderOrder;
use rama_http_types::{HeaderMap, Method, Request, Response, Version};
use std::fmt;
use std::future::Future;
Expand Down
Loading

0 comments on commit 65e4884

Please sign in to comment.