use super::discovery::is_valid_name;
use crate::server::DynamicHandler;
use std::{
io::Read,
str::FromStr,
sync::{
atomic::{AtomicUsize, Ordering},
Arc, RwLock,
},
thread,
time::Duration,
};
use tiny_http::{Header, Request, Response, StatusCode};
use tracing::warn;
const MAX_BODY: usize = 100 * 1024 * 1024;
const MAX_CONCURRENT: usize = 32;
const HOP_BY_HOP: &[&str] = &[
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade",
];
#[derive(Debug, Clone)]
pub(crate) enum ProxyState {
Starting,
Ready { port: u16 },
Failed(String),
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Route {
Static,
Carrier {
name: String,
upstream: String,
},
Reject,
}
pub(crate) fn route(raw_url: &str) -> Route {
let (path, query) = match raw_url.split_once('?') {
Some((path, query)) => (path, Some(query)),
None => (raw_url, None),
};
let rest = match path.strip_prefix("/carriers/") {
Some(rest) => rest,
None => return Route::Static,
};
let (name, tail) = match rest.split_once('/') {
Some((name, tail)) => (name, Some(tail)),
None => (rest, None),
};
if !is_valid_name(name) {
return Route::Reject;
}
let mut upstream = format!("/carrier/{}", name);
if let Some(tail) = tail {
upstream.push('/');
upstream.push_str(tail);
}
if let Some(query) = query {
upstream.push('?');
upstream.push_str(query);
}
Route::Carrier {
name: name.to_string(),
upstream,
}
}
pub(crate) struct CarrierProxy {
state: Arc<RwLock<ProxyState>>,
client: reqwest::blocking::Client,
in_flight: Arc<AtomicUsize>,
}
impl CarrierProxy {
pub fn new(state: Arc<RwLock<ProxyState>>) -> Self {
Self {
state,
client: reqwest::blocking::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.no_proxy()
.timeout(Duration::from_secs(120))
.build()
.expect("a blocking client with no TLS config always builds"),
in_flight: Arc::new(AtomicUsize::new(0)),
}
}
fn forward(&self, mut request: Request, upstream: String) {
let port = match &*self.state.read().unwrap() {
ProxyState::Ready { port } => *port,
ProxyState::Starting => {
return respond(
request,
Response::from_string("Carriers are still starting")
.with_status_code(503)
.with_header(header("Retry-After: 1")),
);
}
ProxyState::Failed(message) => {
return respond(
request,
Response::from_string(message.to_owned()).with_status_code(502),
);
}
};
let method = match reqwest::Method::from_str(request.method().as_str()) {
Ok(method) => method,
Err(_) => {
return respond(
request,
Response::from_string("Unsupported method").with_status_code(405),
)
}
};
let mut headers = reqwest::header::HeaderMap::new();
for header in request.headers() {
let name = header.field.as_str().as_str().to_lowercase();
if HOP_BY_HOP.contains(&name.as_str())
|| name == "host"
|| name == "content-length"
|| name == "x-archival-token"
{
continue;
}
if let (Ok(name), Ok(value)) = (
reqwest::header::HeaderName::from_bytes(name.as_bytes()),
reqwest::header::HeaderValue::from_str(header.value.as_str()),
) {
headers.insert(name, value);
}
}
let mut body = Vec::new();
let limit = request.body_length().unwrap_or(0);
if limit > MAX_BODY {
return respond(
request,
Response::from_string("Request body is too large").with_status_code(413),
);
}
if let Err(e) = request
.as_reader()
.take(MAX_BODY as u64 + 1)
.read_to_end(&mut body)
{
return respond(
request,
Response::from_string(format!("Couldn't read the request body: {}", e))
.with_status_code(400),
);
}
if body.len() > MAX_BODY {
return respond(
request,
Response::from_string("Request body is too large").with_status_code(413),
);
}
let url = format!("http://127.0.0.1:{}{}", port, upstream);
let response = match self
.client
.request(method, &url)
.headers(headers)
.body(body)
.send()
{
Ok(response) => response,
Err(e) => {
return respond(
request,
Response::from_string(format!("The carrier sidecar didn't answer: {}", e))
.with_status_code(502),
)
}
};
let status = response.status().as_u16();
let mut out_headers = vec![];
for (name, value) in response.headers() {
let name = name.as_str().to_lowercase();
if HOP_BY_HOP.contains(&name.as_str())
|| name == "content-length"
|| name == "content-encoding"
{
continue;
}
if let (Ok(field), Ok(value)) = (
tiny_http::HeaderField::from_str(&name),
value.to_str().map(|v| v.to_string()),
) {
if let Ok(value) = value.parse() {
out_headers.push(Header { field, value });
}
}
}
let bytes = match response.bytes() {
Ok(bytes) => bytes.to_vec(),
Err(e) => {
return respond(
request,
Response::from_string(format!("The carrier's response failed: {}", e))
.with_status_code(502),
)
}
};
respond(
request,
Response::from_data(bytes)
.with_status_code(StatusCode(status))
.with_header_list(out_headers),
);
}
}
impl DynamicHandler for CarrierProxy {
fn handle(&self, request: Request) -> Option<Request> {
let upstream = match route(request.url()) {
Route::Static => return Some(request),
Route::Reject => {
respond(
request,
Response::from_string("Not Found").with_status_code(404),
);
return None;
}
Route::Carrier { upstream, .. } => upstream,
};
if self.in_flight.load(Ordering::SeqCst) >= MAX_CONCURRENT {
respond(
request,
Response::from_string("Too many carrier requests in flight")
.with_status_code(503)
.with_header(header("Retry-After: 1")),
);
return None;
}
let state = self.state.clone();
let client = self.client.clone();
let in_flight = self.in_flight.clone();
in_flight.fetch_add(1, Ordering::SeqCst);
thread::spawn(move || {
let proxy = CarrierProxy {
state,
client,
in_flight: in_flight.clone(),
};
proxy.forward(request, upstream);
in_flight.fetch_sub(1, Ordering::SeqCst);
});
None
}
}
trait WithHeaderList {
fn with_header_list(self, headers: Vec<Header>) -> Self;
}
impl<R: Read> WithHeaderList for Response<R> {
fn with_header_list(mut self, headers: Vec<Header>) -> Self {
for header in headers {
self.add_header(header);
}
self
}
}
fn header(raw: &str) -> Header {
Header::from_str(raw).expect("formatted correctly")
}
fn respond<R: Read>(request: Request, response: Response<R>) {
if let Err(e) = request.respond(response) {
warn!("failed answering a carrier request: {}", e);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn carrier(url: &str) -> (String, String) {
match route(url) {
Route::Carrier { name, upstream } => (name, upstream),
other => panic!("expected a carrier route for {}, got {:?}", url, other),
}
}
#[test]
fn anything_outside_carriers_is_static() {
assert_eq!(route("/"), Route::Static);
assert_eq!(route("/index.html"), Route::Static);
assert_eq!(route("/carriersfoo"), Route::Static);
assert_eq!(route("/carriers"), Route::Static);
}
#[test]
fn the_query_string_is_forwarded() {
assert_eq!(
carrier("/carriers/echo?name=x&b=1"),
("echo".to_string(), "/carrier/echo?name=x&b=1".to_string())
);
}
#[test]
fn a_trailing_path_is_forwarded() {
assert_eq!(
carrier("/carriers/echo/a/b?q=1"),
("echo".to_string(), "/carrier/echo/a/b?q=1".to_string())
);
}
#[test]
fn a_bare_carrier_name_routes() {
assert_eq!(
carrier("/carriers/echo"),
("echo".to_string(), "/carrier/echo".to_string())
);
}
#[test]
fn nothing_can_address_the_control_plane() {
assert_eq!(route("/carriers/../__control/state"), Route::Reject);
assert_eq!(route("/carriers/..%2f__control/state"), Route::Reject);
assert_eq!(route("/carriers/."), Route::Reject);
assert_eq!(route("/carriers/"), Route::Reject);
let (_, upstream) = carrier("/carriers/echo/__control/state");
assert!(upstream.starts_with("/carrier/echo/"));
}
}