use std::net::Ipv4Addr;
use cookie::{time::Duration, Cookie};
use fastly::{
http::{
header::{COOKIE, SET_COOKIE},
HeaderValue,
},
Error as FastlyError, Request, Response,
};
use uuid::Uuid;
use crate::{Error, SpecConfiguration, SpecProxyMode};
const SPEC_HEADER_FORWARD_ORIGIN: &str = "x-spec-forward-origin";
const SPEC_COOKIE_ID: &str = "x-spec-id";
const HEADER_X_FORWARDED_FOR: &str = "x-forwarded-for";
#[derive(Debug, Default)]
struct RequestMetadata<'a> {
has_spec_cookie: bool,
should_handle_request: bool,
fastly_backend: &'a str,
}
impl<'a> RequestMetadata<'a> {
pub fn fastly_backend(&self) -> &'a str {
self.fastly_backend
}
}
pub fn spec_proxy_process(
request: Request,
config: &SpecConfiguration,
) -> Result<Response, FastlyError> {
let (request, metadata) = spec_proxy_process_request(request, config)?;
request
.send(metadata.fastly_backend())
.map(|response| spec_proxy_process_response(response, metadata))
.map_err(Into::into)
}
fn spec_proxy_process_request<'a>(
mut request: Request,
config: &'a SpecConfiguration,
) -> Result<(Request, RequestMetadata<'a>), Error> {
let mut metadata = RequestMetadata {
should_handle_request: false,
has_spec_cookie: has_spec_cookie(request.get_header_all(COOKIE)),
fastly_backend: request
.get_url()
.host_str()
.ok_or(Error::MissingHost)
.and_then(|host| config.backend_for_host(host))?,
};
if config.disable_spec_proxy() {
return Ok((request, metadata));
}
if !should_handle_request(
ip_from_x_forwarded_for(request.get_header(HEADER_X_FORWARDED_FOR)).ok(),
config,
) {
return Ok((request, metadata));
}
metadata.should_handle_request = true;
let host = request
.get_url()
.host_str()
.map(|h| h.to_string())
.ok_or(Error::MissingHost)?;
match config.operating_mode() {
SpecProxyMode::Inline => {
let url = request.get_url_mut();
url.set_host(Some(format!("{}.specprotected.com", host).as_str()))
.ok();
request.set_header(SPEC_HEADER_FORWARD_ORIGIN, format!("https://{}", host));
}
SpecProxyMode::Listening => {
let mut proxy_request = request.clone_with_body();
let url = proxy_request.get_url_mut();
let newhost = format!("{}.specprotected.com", host);
url.set_host(Some(newhost.as_str())).ok();
proxy_request.set_pass(true);
match config.backend_for_host(&newhost) {
Ok(backend) => {
proxy_request.send_async(backend).ok();
}
_ => log::error!(
"Spec Proxy: could not find Fastly Backend for host: {}",
&newhost
),
};
}
};
metadata.fastly_backend = request
.get_url()
.host_str()
.ok_or(Error::MissingHost)
.and_then(|host| config.backend_for_host(host))?;
Ok((request, metadata))
}
fn spec_proxy_process_response(mut response: Response, metadata: RequestMetadata) -> Response {
if metadata.should_handle_request && !metadata.has_spec_cookie {
response.append_header(
SET_COOKIE,
Cookie::build(SPEC_COOKIE_ID, Uuid::new_v4().to_string())
.path("/")
.max_age(Duration::days(365 * 10)) .finish()
.to_string(),
);
}
response
}
fn should_handle_request(ip: Option<Ipv4Addr>, config: &SpecConfiguration) -> bool {
let percentage_of_ips = config.percentage_of_ips();
if percentage_of_ips >= 100 {
return true;
}
else if percentage_of_ips == 0 {
return false;
}
match ip {
None => false,
Some(ip) => {
let ip_octet_sum: u16 = ip.octets().iter().map(|&o| o as u16).sum();
let ip_octet_sum: u8 = (ip_octet_sum % 100) as u8;
ip_octet_sum < percentage_of_ips
}
}
}
fn ip_from_x_forwarded_for(
x_forwarded_for_header: Option<&HeaderValue>,
) -> Result<Ipv4Addr, Error> {
x_forwarded_for_header
.and_then(|h| h.to_str().ok())
.and_then(|h| h.split(',').next())
.unwrap_or("")
.parse()
.map_err(Into::into)
}
fn has_spec_cookie<'a>(cookie_headers: impl Iterator<Item = &'a HeaderValue>) -> bool {
cookie_headers
.filter_map(|value| value.to_str().ok())
.flat_map(|value| value.split(';'))
.filter_map(|cookies_str| Cookie::parse(cookies_str).ok())
.any(|cookie| cookie.name() == SPEC_COOKIE_ID)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn should_handle_request_ips() {
[
("0.0.0.0", 100, true),
("0.0.0.40", 100, true),
("0.0.0.99", 100, true),
("0.0.0.0", 100, true),
("0.0.0.40", 50, true),
("0.0.0.49", 50, true),
("0.0.0.50", 50, false),
("0.0.0.60", 50, false),
("0.0.0.0", 0, false),
("0.0.0.40", 0, false),
("0.0.0.99", 0, false),
("0.0.0.100", 0, false),
("24.68.195.11", 50, false),
("74.232.255.255", 50, true),
("89.2.79.2", 50, false),
("67.67.67.67", 50, false),
("10.0.0.8", 50, true),
]
.into_iter()
.for_each(|(ip, percentage_of_ips, expected)| {
let ip = ip.parse().ok();
let config = SpecConfiguration::builder(Default::default())
.with_percentage_of_ips(percentage_of_ips)
.build();
assert_eq!(should_handle_request(ip, &config), expected);
});
}
#[test]
fn extract_ip_from_x_forwarded_for() {
[
("24.68.195.11", Ipv4Addr::new(24, 68, 195, 11)),
("74.232.255.255", Ipv4Addr::new(74, 232, 255, 255)),
("89.2.79.2", Ipv4Addr::new(89, 2, 79, 2)),
("67.67.67.67", Ipv4Addr::new(67, 67, 67, 67)),
(
"24.68.195.11, 127.0.0.1, 10.9.23.11",
Ipv4Addr::new(24, 68, 195, 11),
),
(
"74.232.255.255, 127.0.0.1, 10.9.23.11",
Ipv4Addr::new(74, 232, 255, 255),
),
]
.into_iter()
.for_each(|(value, expected)| {
dbg!(&value, &expected);
let value: Option<HeaderValue> = value.try_into().ok();
assert_eq!(ip_from_x_forwarded_for(value.as_ref()).unwrap(), expected);
});
}
#[test]
fn examine_has_spec_cookie() {
[
(vec!["a=bcd; two=lsdkfjsldkfj; another_one=testing"], false),
(vec![""], false),
(vec![], false),
(
vec!["a=bcd; two=lsdkfjsldkfj; x-spec-id=something; another_one=testing"],
true,
),
(vec!["x-spec-id=something"], true),
(
vec![
"a=bcd; two=lsdkfjsldkfj; another_one=testing",
"more=cookies; for=you",
],
false,
),
(
vec![
"a=bcd; two=lsdkfjsldkfj; x-spec-id=something; another_one=testing",
"more=cookies; for=you",
],
true,
),
(
vec![
"a=bcd; two=lsdkfjsldkfj; another_one=testing",
"more=cookies; x-spec-id=something; for=you",
],
true,
),
]
.into_iter()
.for_each(|(cookie, expected)| {
dbg!(&cookie, &expected);
let headers = cookie
.into_iter()
.map(|v| v.try_into().unwrap())
.collect::<Vec<_>>();
assert_eq!(has_spec_cookie(headers.iter()), expected);
});
}
}