use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use http::header::HeaderValue;
use http::{HeaderMap, Response};
use tower_layer::Layer;
use tower_service::Service;
use crate::error::Error;
use crate::link::{self, ManifestLink};
pub const LINK: http::header::HeaderName = http::header::LINK;
pub fn append_to(headers: &mut HeaderMap, uri: &str) -> Result<(), Error> {
let value = link::format(uri)?;
let header = HeaderValue::from_str(&value)
.map_err(|_| Error::Malformed("target URI is not a valid header value"))?;
headers.append(LINK, header);
Ok(())
}
pub fn extract_from(headers: &HeaderMap) -> Result<ManifestLink, Error> {
link::extract(headers.get_all(LINK).iter().filter_map(|v| v.to_str().ok()))
}
#[derive(Debug, Clone)]
pub struct ManifestLinkLayer {
header: HeaderValue,
}
impl ManifestLinkLayer {
pub fn new(uri: &str) -> Result<Self, Error> {
Self::from_value(link::format(uri)?)
}
pub fn new_strict(uri: &str) -> Result<Self, Error> {
Self::from_value(link::format_strict(uri)?)
}
fn from_value(value: String) -> Result<Self, Error> {
let header = HeaderValue::from_str(&value)
.map_err(|_| Error::Malformed("target URI is not a valid header value"))?;
Ok(Self { header })
}
}
impl<S> Layer<S> for ManifestLinkLayer {
type Service = ManifestLinkService<S>;
fn layer(&self, inner: S) -> Self::Service {
ManifestLinkService {
inner,
header: self.header.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct ManifestLinkService<S> {
inner: S,
header: HeaderValue,
}
impl<S, Request, B> Service<Request> for ManifestLinkService<S>
where
S: Service<Request, Response = Response<B>>,
{
type Response = S::Response;
type Error = S::Error;
type Future = ResponseFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, request: Request) -> Self::Future {
ResponseFuture {
inner: self.inner.call(request),
header: self.header.clone(),
}
}
}
pin_project_lite::pin_project! {
#[derive(Debug)]
pub struct ResponseFuture<F> {
#[pin]
inner: F,
header: HeaderValue,
}
}
impl<F, B, E> Future for ResponseFuture<F>
where
F: Future<Output = Result<Response<B>, E>>,
{
type Output = Result<Response<B>, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let mut response = match this.inner.poll(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Ready(Ok(r)) => r,
};
response.headers_mut().append(LINK, this.header.clone());
Poll::Ready(Ok(response))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tower::{ServiceBuilder, ServiceExt};
const URI: &str = "https://a.example/m.c2pa";
async fn ok(_: http::Request<()>) -> Result<Response<()>, std::convert::Infallible> {
Ok(Response::new(()))
}
async fn with_existing_link(
_: http::Request<()>,
) -> Result<Response<()>, std::convert::Infallible> {
let mut r = Response::new(());
r.headers_mut()
.append(LINK, HeaderValue::from_static("</s.css>; rel=preload"));
Ok(r)
}
#[tokio::test]
async fn the_layer_advertises_the_manifest() {
let svc = ServiceBuilder::new()
.layer(ManifestLinkLayer::new(URI).unwrap())
.service_fn(ok);
let response = svc.oneshot(http::Request::new(())).await.unwrap();
assert_eq!(extract_from(response.headers()).unwrap().uri, URI);
}
#[tokio::test]
async fn an_existing_link_header_survives() {
let svc = ServiceBuilder::new()
.layer(ManifestLinkLayer::new(URI).unwrap())
.service_fn(with_existing_link);
let response = svc.oneshot(http::Request::new(())).await.unwrap();
assert_eq!(response.headers().get_all(LINK).iter().count(), 2);
assert_eq!(extract_from(response.headers()).unwrap().uri, URI);
}
#[test]
fn append_and_extract_round_trip() {
let mut headers = HeaderMap::new();
assert_eq!(extract_from(&headers), Err(Error::NotFound));
append_to(&mut headers, URI).unwrap();
assert_eq!(extract_from(&headers).unwrap().uri, URI);
}
#[test]
fn append_preserves_unrelated_links() {
let mut headers = HeaderMap::new();
headers.append(LINK, HeaderValue::from_static("</a>; rel=next"));
append_to(&mut headers, URI).unwrap();
assert_eq!(headers.get_all(LINK).iter().count(), 2);
assert_eq!(extract_from(&headers).unwrap().uri, URI);
}
#[test]
fn a_hostile_target_cannot_inject_a_header() {
let mut headers = HeaderMap::new();
append_to(&mut headers, "https://a.example/\r\nX-Evil: 1").unwrap();
assert_eq!(headers.len(), 1, "a second header was injected");
assert!(headers.get("x-evil").is_none());
assert!(extract_from(&headers).unwrap().uri.contains("%0D%0A"));
}
#[tokio::test]
async fn a_hostile_target_stays_inert_through_the_layer() {
let svc = ServiceBuilder::new()
.layer(ManifestLinkLayer::new("https://a.example/\r\nX-Evil: 1").unwrap())
.service_fn(ok);
let response = svc.oneshot(http::Request::new(())).await.unwrap();
assert_eq!(response.headers().len(), 1);
assert!(response.headers().get("x-evil").is_none());
}
#[test]
fn strict_construction_rejects_what_lenient_repairs() {
assert!(ManifestLinkLayer::new_strict("https://a.example/a b").is_err());
assert!(ManifestLinkLayer::new("https://a.example/a b").is_ok());
assert!(ManifestLinkLayer::new_strict("https://a.example/m.c2pa").is_ok());
}
#[test]
fn a_jumbf_target_survives_the_header_round_trip() {
let mut headers = HeaderMap::new();
append_to(&mut headers, "https://a.example/i.jpg#jumbf=c2pa").unwrap();
let found = extract_from(&headers).unwrap();
assert!(found.is_embedded());
assert_eq!(found.jumbf.as_deref(), Some("c2pa"));
}
#[test]
fn competing_targets_across_two_fields_are_rejected() {
let mut headers = HeaderMap::new();
append_to(&mut headers, "https://a.example/a.c2pa").unwrap();
append_to(&mut headers, "https://a.example/b.c2pa").unwrap();
assert_eq!(extract_from(&headers), Err(Error::MultipleLinks));
}
#[test]
fn a_non_utf8_header_value_does_not_hide_a_good_one() {
let mut headers = HeaderMap::new();
headers.append(
LINK,
HeaderValue::from_bytes(b"</\xFF\xFE>; rel=next").unwrap(),
);
append_to(&mut headers, URI).unwrap();
assert_eq!(extract_from(&headers).unwrap().uri, URI);
}
}