Enforce that requests are mapped to connections for each Host: header values (#492)
This PR ensures that the mapping of requests to outbound connections is segregated by `Host:` header values. In most cases, the desired behavior is provided by Hyper's connection pooling. However, Hyper does not handle the case where a request had no `Host:` header and the request URI had no authority part, and the request was routed based on the SO_ORIGINAL_DST in the desired manner. We would like these requests to each have their own outbound connection, but Hyper will reuse the same connection for such requests. Therefore, I have modified `conduit_proxy_router::Recognize` to allow implementations of `Recognize` to indicate whether the service for a given key can be cached, and to only cache the service when it is marked as cachable. I've also changed the `reconstruct_uri` function, which rewrites HTTP/1 requests, to mark when a request had no authority and no `Host:` header, and the authority was rewritten to be the request's ORIGINAL_DST. When this is the case, the `Recognize` implementations for `Inbound` and `Outbound` will mark these requests as non-cachable. I've also added unit tests ensuring that A, connections are created per `Host:` header, and B, that requests with no `Host:` header each create a new connection. The first test passes without any additional changes, but the second only passes on this branch. The tests were added in PR #489, but this branch supersedes that branch. Fixes #415. Closes #489.
This commit is contained in:
parent
6d3e0a2850
commit
d2c8d588e6
|
@ -111,7 +111,7 @@ dependencies = [
|
|||
"h2 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"http 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"httparse 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper 0.11.20 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper 0.11.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"indexmap 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ipnet 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -298,7 +298,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.11.20"
|
||||
version = "0.11.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"base64 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -1010,7 +1010,7 @@ dependencies = [
|
|||
"checksum heck 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ea04fa3ead4e05e51a7c806fc07271fdbde4e246a6c6d1efd52e72230b771b82"
|
||||
"checksum http 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "75df369fd52c60635208a4d3e694777c099569b3dcf4844df8f652dc004644ab"
|
||||
"checksum httparse 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c2f407128745b78abc95c0ffbe4e5d37427fdc0d45470710cfef8c44522a2e37"
|
||||
"checksum hyper 0.11.20 (registry+https://github.com/rust-lang/crates.io-index)" = "1545100ac38f42f338c63bff290d7285b7117021a564b3c0f34e8d57cf81451f"
|
||||
"checksum hyper 0.11.21 (registry+https://github.com/rust-lang/crates.io-index)" = "a3a77dea5dccbf32ba4e9ddd7d80a5a3bb3b9f1f3835e18daf5dbea6bee0efbf"
|
||||
"checksum indexmap 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7164c96d6e18ccc3ce43f3dedac996c21a220670a106c275b96ad92110401362"
|
||||
"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08"
|
||||
"checksum ipnet 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "51268c3a27ad46afd1cca0bbf423a5be2e9fd3e6a7534736c195f0f834b763ef"
|
||||
|
|
|
@ -22,7 +22,7 @@ futures = "0.1"
|
|||
h2 = "0.1"
|
||||
http = "0.1"
|
||||
httparse = "1.2"
|
||||
hyper = { version = "0.11.20", default-features = false, features = ["compat"] }
|
||||
hyper = { version = "0.11.21", default-features = false, features = ["compat"] }
|
||||
ipnet = "1.0"
|
||||
log = "0.4.1"
|
||||
indexmap = "0.4.1"
|
||||
|
|
|
@ -7,6 +7,7 @@ use indexmap::IndexMap;
|
|||
use tower::Service;
|
||||
|
||||
use std::{error, fmt, mem};
|
||||
use std::convert::AsRef;
|
||||
use std::hash::Hash;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
|
@ -40,7 +41,7 @@ pub trait Recognize {
|
|||
Error = Self::Error>;
|
||||
|
||||
/// Obtains a Key for a request.
|
||||
fn recognize(&self, req: &Self::Request) -> Option<Self::Key>;
|
||||
fn recognize(&self, req: &Self::Request) -> Option<Reuse<Self::Key>>;
|
||||
|
||||
/// Return a `Service` to handle requests from the provided authority.
|
||||
///
|
||||
|
@ -51,6 +52,18 @@ pub trait Recognize {
|
|||
|
||||
pub struct Single<S>(Option<S>);
|
||||
|
||||
/// Whether or not the service to a given key may be cached.
|
||||
///
|
||||
/// Some services may, for various reasons, may not be able to
|
||||
/// be used to serve multiple requests. When this is the case,
|
||||
/// implementors of `recognize` may use `Reuse::SingleUse` to
|
||||
/// indicate that the service should not be cached.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Reuse<T> {
|
||||
Reusable(T),
|
||||
SingleUse(T),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error<T, U> {
|
||||
Inner(T),
|
||||
|
@ -123,28 +136,37 @@ where T: Recognize,
|
|||
let service;
|
||||
|
||||
if let Some(key) = inner.recognize.recognize(&request) {
|
||||
if let Some(s) = inner.routes.get_mut(&key) {
|
||||
// The service for the authority is already cached
|
||||
// Is the bound service for that key reusable? If `recognize`
|
||||
// returned `SingleUse`, that indicates that the service may
|
||||
// not be used to serve multiple requests.
|
||||
let cached = if let Reuse::Reusable(ref key) = key {
|
||||
// The key is reusable --- look in the cache.
|
||||
inner.routes.get_mut(key)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(s) = cached {
|
||||
// The service for the authority is already cached.
|
||||
service = s;
|
||||
} else {
|
||||
// The authority does not match an existing route, try to
|
||||
// recognize it.
|
||||
match inner.recognize.bind_service(&key) {
|
||||
match inner.recognize.bind_service(key.as_ref()) {
|
||||
Ok(s) => {
|
||||
// A new service has been matched. Set the outer
|
||||
// variables and jump out o the loop
|
||||
// variables and jump out o the loop.
|
||||
new_key = key.clone();
|
||||
new_service = s;
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
// Route recognition failed
|
||||
// Route recognition failed.
|
||||
return ResponseFuture { state: State::RouteError(e) };
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The request has no authority
|
||||
// The request has no authority.
|
||||
return ResponseFuture { state: State::NotRecognized };
|
||||
}
|
||||
|
||||
|
@ -153,13 +175,15 @@ where T: Recognize,
|
|||
return ResponseFuture { state: State::Inner(response) };
|
||||
}
|
||||
|
||||
// First, route the request to the new service
|
||||
// First, route the request to the new service.
|
||||
let response = new_service.call(request);
|
||||
|
||||
// Now, cache the new service
|
||||
inner.routes.insert(new_key, new_service);
|
||||
// Now, cache the new service.
|
||||
if let Reuse::Reusable(new_key) = new_key {
|
||||
inner.routes.insert(new_key, new_service);
|
||||
}
|
||||
|
||||
// And finally, return the response
|
||||
// And finally, return the response.
|
||||
ResponseFuture { state: State::Inner(response) }
|
||||
}
|
||||
}
|
||||
|
@ -190,8 +214,8 @@ impl<S: Service> Recognize for Single<S> {
|
|||
type RouteError = ();
|
||||
type Service = S;
|
||||
|
||||
fn recognize(&self, _: &Self::Request) -> Option<Self::Key> {
|
||||
Some(())
|
||||
fn recognize(&self, _: &Self::Request) -> Option<Reuse<Self::Key>> {
|
||||
Some(Reuse::Reusable(()))
|
||||
}
|
||||
|
||||
fn bind_service(&mut self, _: &Self::Key) -> Result<S, Self::RouteError> {
|
||||
|
@ -262,3 +286,14 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== impl Reuse =====
|
||||
|
||||
impl<T> AsRef<T> for Reuse<T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
match *self {
|
||||
Reuse::Reusable(ref key) => key,
|
||||
Reuse::SingleUse(ref key) => key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,25 @@
|
|||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::default::Default;
|
||||
use std::marker::PhantomData;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
|
||||
use http;
|
||||
use futures::{future, Future, Poll};
|
||||
use futures::future::{Either, Map};
|
||||
use http::{self, uri};
|
||||
use tokio_core::reactor::Handle;
|
||||
use tower;
|
||||
use tower_h2;
|
||||
use tower_reconnect::Reconnect;
|
||||
|
||||
use conduit_proxy_controller_grpc;
|
||||
use conduit_proxy_router::Reuse;
|
||||
use control;
|
||||
use ctx;
|
||||
use telemetry::{self, sensor};
|
||||
use transparency::{self, HttpBody};
|
||||
use transparency::{self, HttpBody, h1};
|
||||
use transport;
|
||||
|
||||
/// Binds a `Service` from a `SocketAddr`.
|
||||
|
@ -39,14 +43,37 @@ pub struct BindProtocol<C, B> {
|
|||
protocol: Protocol,
|
||||
}
|
||||
|
||||
/// Mark whether to use HTTP/1 or HTTP/2
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
/// Protocol portion of the `Recognize` key for a request.
|
||||
///
|
||||
/// This marks whether to use HTTP/2 or HTTP/1.x for a request. In
|
||||
/// the case of HTTP/1.x requests, it also stores a "host" key to ensure
|
||||
/// that each host receives its own connection.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Protocol {
|
||||
Http1,
|
||||
Http1(Host),
|
||||
Http2
|
||||
}
|
||||
|
||||
pub type Service<B> = Reconnect<NewHttp<B>>;
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Host {
|
||||
Authority(uri::Authority),
|
||||
NoAuthority,
|
||||
}
|
||||
|
||||
/// Rewrites HTTP/1.x requests so that their URIs are in a canonical form.
|
||||
///
|
||||
/// The following transformations are applied:
|
||||
/// - If an absolute-form URI is received, it must replace
|
||||
/// the host header (in accordance with RFC7230#section-5.4)
|
||||
/// - If the request URI is not in absolute form, it is rewritten to contain
|
||||
/// the authority given in the `Host:` header, or, failing that, from the
|
||||
/// request's original destination according to `SO_ORIGINAL_DST`.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct ReconstructUri<S> {
|
||||
inner: S
|
||||
}
|
||||
|
||||
pub type Service<B> = Reconnect<ReconstructUri<NewHttp<B>>>;
|
||||
|
||||
pub type NewHttp<B> = sensor::NewHttp<Client<B>, B, HttpBody>;
|
||||
|
||||
|
@ -83,7 +110,6 @@ impl Error for BufferSpawnError {
|
|||
fn cause(&self) -> Option<&Error> { None }
|
||||
}
|
||||
|
||||
|
||||
impl<B> Bind<(), B> {
|
||||
pub fn new(executor: Handle) -> Self {
|
||||
Self {
|
||||
|
@ -150,7 +176,7 @@ impl<B> Bind<Arc<ctx::Proxy>, B>
|
|||
where
|
||||
B: tower_h2::Body + 'static,
|
||||
{
|
||||
pub fn bind_service(&self, addr: &SocketAddr, protocol: Protocol) -> Service<B> {
|
||||
pub fn bind_service(&self, addr: &SocketAddr, protocol: &Protocol) -> Service<B> {
|
||||
trace!("bind_service addr={}, protocol={:?}", addr, protocol);
|
||||
let client_ctx = ctx::transport::Client::new(
|
||||
&self.ctx,
|
||||
|
@ -170,7 +196,15 @@ where
|
|||
self.executor.clone(),
|
||||
);
|
||||
|
||||
let proxy = self.sensors.http(self.req_ids.clone(), client, &client_ctx);
|
||||
let sensors = self.sensors.http(
|
||||
self.req_ids.clone(),
|
||||
client,
|
||||
&client_ctx
|
||||
);
|
||||
|
||||
// Rewrite the HTTP/1 URI, if the authorities in the Host header
|
||||
// and request URI are not in agreement, or are not present.
|
||||
let proxy = ReconstructUri::new(sensors);
|
||||
|
||||
// Automatically perform reconnects if the connection fails.
|
||||
//
|
||||
|
@ -202,7 +236,117 @@ where
|
|||
type BindError = ();
|
||||
|
||||
fn bind(&self, addr: &SocketAddr) -> Result<Self::Service, Self::BindError> {
|
||||
Ok(self.bind.bind_service(addr, self.protocol))
|
||||
Ok(self.bind.bind_service(addr, &self.protocol))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===== impl ReconstructUri =====
|
||||
|
||||
|
||||
impl<S> ReconstructUri<S> {
|
||||
fn new (inner: S) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B> tower::NewService for ReconstructUri<S>
|
||||
where
|
||||
S: tower::NewService<
|
||||
Request=http::Request<B>,
|
||||
Response=HttpResponse,
|
||||
>,
|
||||
S::Service: tower::Service<
|
||||
Request=http::Request<B>,
|
||||
Response=HttpResponse,
|
||||
>,
|
||||
ReconstructUri<S::Service>: tower::Service,
|
||||
B: tower_h2::Body,
|
||||
{
|
||||
type Request = <Self::Service as tower::Service>::Request;
|
||||
type Response = <Self::Service as tower::Service>::Response;
|
||||
type Error = <Self::Service as tower::Service>::Error;
|
||||
type Service = ReconstructUri<S::Service>;
|
||||
type InitError = S::InitError;
|
||||
type Future = Map<
|
||||
S::Future,
|
||||
fn(S::Service) -> ReconstructUri<S::Service>
|
||||
>;
|
||||
fn new_service(&self) -> Self::Future {
|
||||
self.inner.new_service().map(ReconstructUri::new)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B> tower::Service for ReconstructUri<S>
|
||||
where
|
||||
S: tower::Service<
|
||||
Request=http::Request<B>,
|
||||
Response=HttpResponse,
|
||||
>,
|
||||
B: tower_h2::Body,
|
||||
{
|
||||
type Request = S::Request;
|
||||
type Response = HttpResponse;
|
||||
type Error = S::Error;
|
||||
type Future = Either<
|
||||
S::Future,
|
||||
future::FutureResult<Self::Response, Self::Error>,
|
||||
>;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), S::Error> {
|
||||
self.inner.poll_ready()
|
||||
}
|
||||
|
||||
fn call(&mut self, mut request: S::Request) -> Self::Future {
|
||||
if request.version() == http::Version::HTTP_2 {
|
||||
// skip `reconstruct_uri` entirely if the request is HTTP/2.
|
||||
return Either::A(self.inner.call(request));
|
||||
}
|
||||
|
||||
if let Err(_) = h1::reconstruct_uri(&mut request) {
|
||||
let res = http::Response::builder()
|
||||
.status(http::StatusCode::BAD_REQUEST)
|
||||
.body(Default::default())
|
||||
.unwrap();
|
||||
return Either::B(future::ok(res));
|
||||
}
|
||||
Either::A(self.inner.call(request))
|
||||
}
|
||||
}
|
||||
|
||||
// ===== impl Protocol =====
|
||||
|
||||
|
||||
impl Protocol {
|
||||
|
||||
pub fn detect<B>(req: &http::Request<B>) -> Self {
|
||||
if req.version() == http::Version::HTTP_2 {
|
||||
return Protocol::Http2
|
||||
}
|
||||
|
||||
// If the request has an authority part, use that as the host part of
|
||||
// the key for an HTTP/1.x request.
|
||||
let host = req.uri().authority_part()
|
||||
.cloned()
|
||||
.or_else(|| h1::authority_from_host(req))
|
||||
.map(Host::Authority)
|
||||
.unwrap_or_else(|| Host::NoAuthority);
|
||||
|
||||
Protocol::Http1(host)
|
||||
}
|
||||
|
||||
pub fn is_cachable(&self) -> bool {
|
||||
match *self {
|
||||
Protocol::Http2 | Protocol::Http1(Host::Authority(_)) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_key<T>(self, key: T) -> Reuse<(T, Protocol)> {
|
||||
if self.is_cachable() {
|
||||
Reuse::Reusable((key, self))
|
||||
} else {
|
||||
Reuse::SingleUse((key, self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use tower;
|
|||
use tower_buffer::{self, Buffer};
|
||||
use tower_in_flight_limit::{self, InFlightLimit};
|
||||
use tower_h2;
|
||||
use conduit_proxy_router::Recognize;
|
||||
use conduit_proxy_router::{Reuse, Recognize};
|
||||
|
||||
use bind;
|
||||
use ctx;
|
||||
|
@ -46,7 +46,7 @@ where
|
|||
type RouteError = bind::BufferSpawnError;
|
||||
type Service = InFlightLimit<Buffer<bind::Service<B>>>;
|
||||
|
||||
fn recognize(&self, req: &Self::Request) -> Option<Self::Key> {
|
||||
fn recognize(&self, req: &Self::Request) -> Option<Reuse<Self::Key>> {
|
||||
let key = req.extensions()
|
||||
.get::<Arc<ctx::transport::Server>>()
|
||||
.and_then(|ctx| {
|
||||
|
@ -55,15 +55,12 @@ where
|
|||
})
|
||||
.or_else(|| self.default_addr);
|
||||
|
||||
let proto = match req.version() {
|
||||
http::Version::HTTP_2 => bind::Protocol::Http2,
|
||||
_ => bind::Protocol::Http1,
|
||||
};
|
||||
|
||||
let key = key.map(|addr| (addr, proto));
|
||||
let proto = bind::Protocol::detect(req);
|
||||
|
||||
let key = key.map(move|addr| proto.into_key(addr));
|
||||
trace!("recognize key={:?}", key);
|
||||
|
||||
|
||||
key
|
||||
}
|
||||
|
||||
|
@ -74,7 +71,7 @@ where
|
|||
/// Buffering is currently unbounded and does not apply timeouts. This must be
|
||||
/// changed.
|
||||
fn bind_service(&mut self, key: &Self::Key) -> Result<Self::Service, Self::RouteError> {
|
||||
let &(ref addr, proto) = key;
|
||||
let &(ref addr, ref proto) = key;
|
||||
debug!("building inbound {:?} client to {}", proto, addr);
|
||||
|
||||
Buffer::new(self.bind.bind_service(addr, proto), self.bind.executor())
|
||||
|
@ -96,7 +93,7 @@ mod tests {
|
|||
|
||||
use super::Inbound;
|
||||
use conduit_proxy_controller_grpc::common::Protocol;
|
||||
use bind::{self, Bind};
|
||||
use bind::{self, Bind, Host};
|
||||
use ctx;
|
||||
|
||||
fn new_inbound(default: Option<net::SocketAddr>, ctx: &Arc<ctx::Proxy>) -> Inbound<()> {
|
||||
|
@ -117,7 +114,9 @@ mod tests {
|
|||
|
||||
let srv_ctx = ctx::transport::Server::new(&ctx, &local, &remote, &Some(orig_dst), Protocol::Http);
|
||||
|
||||
let rec = srv_ctx.orig_dst_if_not_local().map(|addr| (addr, bind::Protocol::Http1));
|
||||
let rec = srv_ctx.orig_dst_if_not_local().map(|addr|
|
||||
bind::Protocol::Http1(Host::NoAuthority).into_key(addr)
|
||||
);
|
||||
|
||||
let mut req = http::Request::new(());
|
||||
req.extensions_mut()
|
||||
|
@ -145,7 +144,9 @@ mod tests {
|
|||
Protocol::Http,
|
||||
));
|
||||
|
||||
inbound.recognize(&req) == default.map(|addr| (addr, bind::Protocol::Http1))
|
||||
inbound.recognize(&req) == default.map(|addr|
|
||||
bind::Protocol::Http1(Host::NoAuthority).into_key(addr)
|
||||
)
|
||||
}
|
||||
|
||||
fn recognize_default_no_ctx(default: Option<net::SocketAddr>) -> bool {
|
||||
|
@ -155,7 +156,9 @@ mod tests {
|
|||
|
||||
let req = http::Request::new(());
|
||||
|
||||
inbound.recognize(&req) == default.map(|addr| (addr, bind::Protocol::Http1))
|
||||
inbound.recognize(&req) == default.map(|addr|
|
||||
bind::Protocol::Http1(Host::NoAuthority).into_key(addr)
|
||||
)
|
||||
}
|
||||
|
||||
fn recognize_default_no_loop(
|
||||
|
@ -177,7 +180,9 @@ mod tests {
|
|||
Protocol::Http,
|
||||
));
|
||||
|
||||
inbound.recognize(&req) == default.map(|addr| (addr, bind::Protocol::Http1))
|
||||
inbound.recognize(&req) == default.map(|addr|
|
||||
bind::Protocol::Http1(Host::NoAuthority).into_key(addr)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ use tower_buffer::Buffer;
|
|||
use tower_discover::{Change, Discover};
|
||||
use tower_in_flight_limit::InFlightLimit;
|
||||
use tower_h2;
|
||||
use conduit_proxy_router::Recognize;
|
||||
use conduit_proxy_router::{Reuse, Recognize};
|
||||
|
||||
use bind::{self, Bind, Protocol};
|
||||
use control::{self, discovery};
|
||||
|
@ -58,6 +58,21 @@ pub enum Destination {
|
|||
External(SocketAddr),
|
||||
}
|
||||
|
||||
impl From<FullyQualifiedAuthority> for Destination {
|
||||
#[inline]
|
||||
fn from(authority: FullyQualifiedAuthority) -> Self {
|
||||
Destination::LocalSvc(authority)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SocketAddr> for Destination {
|
||||
#[inline]
|
||||
fn from(addr: SocketAddr) -> Self {
|
||||
Destination::External(addr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<B> Recognize for Outbound<B>
|
||||
where
|
||||
B: tower_h2::Body + 'static,
|
||||
|
@ -72,7 +87,9 @@ where
|
|||
choose::PowerOfTwoChoices<rand::ThreadRng>
|
||||
>>>>;
|
||||
|
||||
fn recognize(&self, req: &Self::Request) -> Option<Self::Key> {
|
||||
fn recognize(&self, req: &Self::Request) -> Option<Reuse<Self::Key>> {
|
||||
let proto = bind::Protocol::detect(req);
|
||||
|
||||
let local = req.uri().authority_part().map(|authority| {
|
||||
FullyQualifiedAuthority::normalize(
|
||||
authority,
|
||||
|
@ -102,12 +119,8 @@ where
|
|||
Destination::External(orig_dst?)
|
||||
};
|
||||
|
||||
let proto = match req.version() {
|
||||
http::Version::HTTP_2 => Protocol::Http2,
|
||||
_ => Protocol::Http1,
|
||||
};
|
||||
|
||||
Some((dest, proto))
|
||||
Some(proto.into_key(dest))
|
||||
}
|
||||
|
||||
/// Builds a dynamic, load balancing service.
|
||||
|
@ -123,18 +136,19 @@ where
|
|||
&mut self,
|
||||
key: &Self::Key,
|
||||
) -> Result<Self::Service, Self::RouteError> {
|
||||
let &(ref dest, protocol) = key;
|
||||
let &(ref dest, ref protocol) = key;
|
||||
debug!("building outbound {:?} client to {:?}", protocol, dest);
|
||||
|
||||
let resolve = match *dest {
|
||||
Destination::LocalSvc(ref authority) => {
|
||||
Discovery::LocalSvc(self.discovery.resolve(
|
||||
authority,
|
||||
self.bind.clone().with_protocol(protocol),
|
||||
self.bind.clone().with_protocol(protocol.clone()),
|
||||
))
|
||||
},
|
||||
Destination::External(addr) => {
|
||||
Discovery::External(Some((addr, self.bind.clone().with_protocol(protocol))))
|
||||
Discovery::External(Some((addr, self.bind.clone()
|
||||
.with_protocol(protocol.clone()))))
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -73,9 +73,13 @@ where
|
|||
B: tower_h2::Body + 'static,
|
||||
{
|
||||
/// Create a new `Client`, bound to a specific protocol (HTTP/1 or HTTP/2).
|
||||
pub fn new(protocol: bind::Protocol, connect: C, executor: Handle) -> Self {
|
||||
match protocol {
|
||||
bind::Protocol::Http1 => {
|
||||
pub fn new(protocol: &bind::Protocol,
|
||||
connect: C,
|
||||
executor: Handle)
|
||||
-> Self
|
||||
{
|
||||
match *protocol {
|
||||
bind::Protocol::Http1(_) => {
|
||||
let h1 = hyper::Client::configure()
|
||||
.connector(HyperConnect::new(connect))
|
||||
.body()
|
||||
|
|
|
@ -217,11 +217,6 @@ where
|
|||
let mut req: http::Request<hyper::Body> = req.into();
|
||||
req.extensions_mut().insert(self.srv_ctx.clone());
|
||||
|
||||
if let Err(()) = h1::reconstruct_uri(&mut req) {
|
||||
let res = hyper::Response::new()
|
||||
.with_status(hyper::BadRequest);
|
||||
return Either::B(future::ok(res));
|
||||
}
|
||||
h1::strip_connection_headers(req.headers_mut());
|
||||
|
||||
let req = req.map(|b| HttpBody::Http1(b));
|
||||
|
|
|
@ -6,7 +6,6 @@ use bytes::BytesMut;
|
|||
use http;
|
||||
use http::header::{HeaderValue, HOST};
|
||||
use http::uri::{Authority, Parts, Scheme, Uri};
|
||||
|
||||
use ctx::transport::{Server as ServerCtx};
|
||||
|
||||
pub fn reconstruct_uri<B>(req: &mut http::Request<B>) -> Result<(), ()> {
|
||||
|
@ -27,20 +26,9 @@ pub fn reconstruct_uri<B>(req: &mut http::Request<B>) -> Result<(), ()> {
|
|||
}
|
||||
|
||||
// try to parse the Host header
|
||||
if let Some(host) = req.headers().get(HOST).cloned() {
|
||||
let auth = host.to_str()
|
||||
.ok()
|
||||
.and_then(|s| {
|
||||
if s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
s.parse::<Authority>().ok()
|
||||
}
|
||||
});
|
||||
if let Some(auth) = auth {
|
||||
set_authority(req.uri_mut(), auth);
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(auth) = authority_from_host(&req) {
|
||||
set_authority(req.uri_mut(), auth);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// last resort is to use the so_original_dst
|
||||
|
@ -62,6 +50,21 @@ pub fn reconstruct_uri<B>(req: &mut http::Request<B>) -> Result<(), ()> {
|
|||
Err(())
|
||||
}
|
||||
|
||||
/// Returns an Authority from a request's Host header.
|
||||
pub fn authority_from_host<B>(req: &http::Request<B>) -> Option<Authority> {
|
||||
req.headers().get(HOST).cloned()
|
||||
.and_then(|host| {
|
||||
host.to_str().ok()
|
||||
.and_then(|s| {
|
||||
if s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
s.parse::<Authority>().ok()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn set_authority(uri: &mut http::Uri, auth: Authority) {
|
||||
let mut parts = Parts::from(mem::replace(uri, Uri::default()));
|
||||
parts.scheme = Some(Scheme::HTTP);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
mod client;
|
||||
mod glue;
|
||||
mod h1;
|
||||
pub mod h1;
|
||||
mod protocol;
|
||||
mod server;
|
||||
mod tcp;
|
||||
|
|
|
@ -23,6 +23,9 @@ pub struct Listening {
|
|||
pub inbound: SocketAddr,
|
||||
pub outbound: SocketAddr,
|
||||
|
||||
pub outbound_server: Option<server::Listening>,
|
||||
pub inbound_server: Option<server::Listening>,
|
||||
|
||||
shutdown: Shutdown,
|
||||
}
|
||||
|
||||
|
@ -150,8 +153,6 @@ fn run(proxy: Proxy, mut env: config::TestEnv) -> Listening {
|
|||
.name("support proxy".into())
|
||||
.spawn(move || {
|
||||
let _c = controller;
|
||||
let _i = inbound;
|
||||
let _o = outbound;
|
||||
|
||||
let _ = running_tx.send(());
|
||||
main.run_until(rx);
|
||||
|
@ -165,6 +166,10 @@ fn run(proxy: Proxy, mut env: config::TestEnv) -> Listening {
|
|||
control: control_addr,
|
||||
inbound: inbound_addr,
|
||||
outbound: outbound_addr,
|
||||
|
||||
outbound_server: outbound,
|
||||
inbound_server: inbound,
|
||||
|
||||
shutdown: tx,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::thread;
|
||||
|
||||
use support::*;
|
||||
|
@ -30,6 +31,13 @@ pub struct Server {
|
|||
pub struct Listening {
|
||||
pub addr: SocketAddr,
|
||||
pub(super) shutdown: Shutdown,
|
||||
pub(super) conn_count: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl Listening {
|
||||
pub fn connections(&self) -> usize {
|
||||
self.conn_count.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
impl Server {
|
||||
|
@ -81,6 +89,8 @@ impl Server {
|
|||
pub fn run(self) -> Listening {
|
||||
let (tx, rx) = shutdown_signal();
|
||||
let (addr_tx, addr_rx) = oneshot::channel();
|
||||
let conn_count = Arc::new(AtomicUsize::from(0));
|
||||
let srv_conn_count = Arc::clone(&conn_count);
|
||||
::std::thread::Builder::new().name("support server".into()).spawn(move || {
|
||||
let mut core = Core::new().unwrap();
|
||||
let reactor = core.handle();
|
||||
|
@ -93,7 +103,11 @@ impl Server {
|
|||
|
||||
Box::new(move |sock| {
|
||||
let h1_clone = h1.clone();
|
||||
let srv_conn_count = Arc::clone(&srv_conn_count);
|
||||
let conn = new_svc.new_service()
|
||||
.inspect(move |_| {
|
||||
srv_conn_count.fetch_add(1, Ordering::Release);
|
||||
})
|
||||
.from_err()
|
||||
.and_then(move |svc| h1_clone.serve_connection(sock, svc))
|
||||
.map(|_| ())
|
||||
|
@ -108,8 +122,12 @@ impl Server {
|
|||
reactor.clone(),
|
||||
);
|
||||
Box::new(move |sock| {
|
||||
let srv_conn_count = Arc::clone(&srv_conn_count);
|
||||
let conn = h2.serve(sock)
|
||||
.map_err(|e| println!("server h2 error: {:?}", e));
|
||||
.map_err(|e| println!("server h2 error: {:?}", e))
|
||||
.inspect(move |_| {
|
||||
srv_conn_count.fetch_add(1, Ordering::Release);
|
||||
});
|
||||
Box::new(conn)
|
||||
})
|
||||
},
|
||||
|
@ -122,7 +140,7 @@ impl Server {
|
|||
let _ = addr_tx.send(local_addr);
|
||||
|
||||
let serve = bind.incoming()
|
||||
.fold((srv, reactor), |(srv, reactor), (sock, _)| {
|
||||
.fold((srv, reactor), move |(srv, reactor), (sock, _)| {
|
||||
if let Err(e) = sock.set_nodelay(true) {
|
||||
return Err(e);
|
||||
}
|
||||
|
@ -145,6 +163,7 @@ impl Server {
|
|||
Listening {
|
||||
addr,
|
||||
shutdown: tx,
|
||||
conn_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ use support::*;
|
|||
|
||||
use std::collections::VecDeque;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use self::futures::sync::{mpsc, oneshot};
|
||||
use self::tokio_core::net::TcpStream;
|
||||
|
@ -182,6 +184,8 @@ fn run_client(addr: SocketAddr) -> TcpSender {
|
|||
fn run_server(tcp: TcpServer) -> server::Listening {
|
||||
let (tx, rx) = shutdown_signal();
|
||||
let (addr_tx, addr_rx) = oneshot::channel();
|
||||
let conn_count = Arc::new(AtomicUsize::from(0));
|
||||
let srv_conn_count = Arc::clone(&conn_count);
|
||||
::std::thread::Builder::new().name("support server".into()).spawn(move || {
|
||||
let mut core = Core::new().unwrap();
|
||||
let reactor = core.handle();
|
||||
|
@ -196,8 +200,10 @@ fn run_server(tcp: TcpServer) -> server::Listening {
|
|||
|
||||
let work = bind.incoming().for_each(move |(sock, _)| {
|
||||
let cb = accepts.pop_front().expect("no more accepts");
|
||||
srv_conn_count.fetch_add(1, Ordering::Release);
|
||||
|
||||
let fut = cb.call_box(sock);
|
||||
|
||||
reactor.spawn(fut);
|
||||
Ok(())
|
||||
});
|
||||
|
@ -209,5 +215,6 @@ fn run_server(tcp: TcpServer) -> server::Listening {
|
|||
server::Listening {
|
||||
addr,
|
||||
shutdown: tx,
|
||||
conn_count,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -357,7 +357,7 @@ fn http1_requests_without_body_doesnt_add_transfer_encoding() {
|
|||
} else {
|
||||
StatusCode::OK
|
||||
};
|
||||
let mut res = Response::new(Default::default());
|
||||
let mut res = Response::new("".into());
|
||||
*res.status_mut() = status;
|
||||
res
|
||||
})
|
||||
|
@ -605,3 +605,116 @@ fn http1_response_end_of_file() {
|
|||
assert_eq!(body, "body till eof", "HTTP/{} body", v);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http1_one_connection_per_host() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let srv = server::http1().route("/", "hello").run();
|
||||
let ctrl = controller::new()
|
||||
.run();
|
||||
let proxy = proxy::new().controller(ctrl).inbound(srv).run();
|
||||
|
||||
let client = client::http1(proxy.inbound, "foo.bar");
|
||||
|
||||
let inbound = &proxy.inbound_server.as_ref()
|
||||
.expect("no inbound server!");
|
||||
|
||||
// Make a request with the header "Host: foo.bar". After the request, the
|
||||
// server should have seen one connection.
|
||||
let res1 = client.request(client.request_builder("/")
|
||||
.version(http::Version::HTTP_11)
|
||||
.header("host", "foo.bar")
|
||||
);
|
||||
assert_eq!(res1.status(), http::StatusCode::OK);
|
||||
assert_eq!(res1.version(), http::Version::HTTP_11);
|
||||
assert_eq!(inbound.connections(), 1);
|
||||
|
||||
// Another request with the same host. The proxy may reuse the connection.
|
||||
let res1 = client.request(client.request_builder("/")
|
||||
.version(http::Version::HTTP_11)
|
||||
.header("host", "foo.bar")
|
||||
);
|
||||
assert_eq!(res1.status(), http::StatusCode::OK);
|
||||
assert_eq!(res1.version(), http::Version::HTTP_11);
|
||||
assert_eq!(inbound.connections(), 1);
|
||||
|
||||
// Make a request with a different Host header. This request must use a new
|
||||
// connection.
|
||||
let res2 = client.request(client.request_builder("/")
|
||||
.version(http::Version::HTTP_11)
|
||||
.header("host", "bar.baz"));
|
||||
assert_eq!(res2.status(), http::StatusCode::OK);
|
||||
assert_eq!(res2.version(), http::Version::HTTP_11);
|
||||
assert_eq!(inbound.connections(), 2);
|
||||
|
||||
let res2 = client.request(client.request_builder("/")
|
||||
.version(http::Version::HTTP_11)
|
||||
.header("host", "bar.baz"));
|
||||
assert_eq!(res2.status(), http::StatusCode::OK);
|
||||
assert_eq!(res2.version(), http::Version::HTTP_11);
|
||||
assert_eq!(inbound.connections(), 2);
|
||||
|
||||
// Make a request with a different Host header. This request must use a new
|
||||
// connection.
|
||||
let res3 = client.request(client.request_builder("/")
|
||||
.version(http::Version::HTTP_11)
|
||||
.header("host", "quuuux.com"));
|
||||
assert_eq!(res3.status(), http::StatusCode::OK);
|
||||
assert_eq!(res3.version(), http::Version::HTTP_11);
|
||||
assert_eq!(inbound.connections(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http1_requests_without_host_have_unique_connections() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let srv = server::http1().route("/", "hello").run();
|
||||
let ctrl = controller::new()
|
||||
.run();
|
||||
let proxy = proxy::new().controller(ctrl).inbound(srv).run();
|
||||
|
||||
let client = client::http1(proxy.inbound, "foo.bar");
|
||||
|
||||
let inbound = &proxy.inbound_server.as_ref()
|
||||
.expect("no inbound server!");
|
||||
|
||||
// Make a request with no Host header and no authority in the request path.
|
||||
let res = client.request(client.request_builder("/")
|
||||
.version(http::Version::HTTP_11)
|
||||
.header("host", "")
|
||||
);
|
||||
assert_eq!(res.status(), http::StatusCode::OK);
|
||||
assert_eq!(res.version(), http::Version::HTTP_11);
|
||||
assert_eq!(inbound.connections(), 1);
|
||||
|
||||
// Another request with no Host. The proxy must open a new connection
|
||||
// for that request.
|
||||
let res = client.request(client.request_builder("/")
|
||||
.version(http::Version::HTTP_11)
|
||||
.header("host", "")
|
||||
);
|
||||
assert_eq!(res.status(), http::StatusCode::OK);
|
||||
assert_eq!(res.version(), http::Version::HTTP_11);
|
||||
assert_eq!(inbound.connections(), 2);
|
||||
|
||||
// Make a request with a host header. It must also receive its
|
||||
// own connection.
|
||||
let res = client.request(client.request_builder("/")
|
||||
.version(http::Version::HTTP_11)
|
||||
.header("host", "foo.bar")
|
||||
);
|
||||
assert_eq!(res.status(), http::StatusCode::OK);
|
||||
assert_eq!(res.version(), http::Version::HTTP_11);
|
||||
assert_eq!(inbound.connections(), 3);
|
||||
|
||||
// Another request with no Host. The proxy must open a new connection
|
||||
// for that request.
|
||||
let res = client.request(client.request_builder("/")
|
||||
.version(http::Version::HTTP_11)
|
||||
.header("host", "")
|
||||
);
|
||||
assert_eq!(res.status(), http::StatusCode::OK);
|
||||
assert_eq!(res.version(), http::Version::HTTP_11);
|
||||
assert_eq!(inbound.connections(), 4);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue