mirror of
https://github.com/actix/actix-web.git
synced 2024-12-17 13:46:36 +00:00
Add support for Proxy Protocol v1 & v2 for HTTP/1.x
This commit is contained in:
parent
e518170a30
commit
f4254c5a10
14 changed files with 424 additions and 28 deletions
2
.github/workflows/ci-post-merge.yml
vendored
2
.github/workflows/ci-post-merge.yml
vendored
|
@ -62,7 +62,7 @@ jobs:
|
||||||
set -e
|
set -e
|
||||||
cargo test --lib --tests -p=actix-router --all-features
|
cargo test --lib --tests -p=actix-router --all-features
|
||||||
cargo test --lib --tests -p=actix-http --all-features
|
cargo test --lib --tests -p=actix-http --all-features
|
||||||
cargo test --lib --tests -p=actix-web --features=rustls-0_20,rustls-0_21,rustls-0_22,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
|
cargo test --lib --tests -p=actix-web --features=rustls-0_20,rustls-0_21,rustls-0_22,openssl,proxy-protocol-- --skip=test_reading_deflate_encoding_large_random_rustls
|
||||||
cargo test --lib --tests -p=actix-web-codegen --all-features
|
cargo test --lib --tests -p=actix-web-codegen --all-features
|
||||||
cargo test --lib --tests -p=awc --all-features
|
cargo test --lib --tests -p=awc --all-features
|
||||||
cargo test --lib --tests -p=actix-http-test --all-features
|
cargo test --lib --tests -p=actix-http-test --all-features
|
||||||
|
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -76,7 +76,7 @@ jobs:
|
||||||
set -e
|
set -e
|
||||||
cargo test --lib --tests -p=actix-router --all-features
|
cargo test --lib --tests -p=actix-router --all-features
|
||||||
cargo test --lib --tests -p=actix-http --all-features
|
cargo test --lib --tests -p=actix-http --all-features
|
||||||
cargo test --lib --tests -p=actix-web --features=rustls-0_20,rustls-0_21,rustls-0_22,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
|
cargo test --lib --tests -p=actix-web --features=rustls-0_20,rustls-0_21,rustls-0_22,openssl,proxy-protocol -- --skip=test_reading_deflate_encoding_large_random_rustls
|
||||||
cargo test --lib --tests -p=actix-web-codegen --all-features
|
cargo test --lib --tests -p=actix-web-codegen --all-features
|
||||||
cargo test --lib --tests -p=awc --all-features
|
cargo test --lib --tests -p=awc --all-features
|
||||||
cargo test --lib --tests -p=actix-http-test --all-features
|
cargo test --lib --tests -p=actix-http-test --all-features
|
||||||
|
|
|
@ -66,6 +66,9 @@ rustls-0_21 = ["actix-tls/accept", "actix-tls/rustls-0_21"]
|
||||||
# TLS via Rustls v0.22
|
# TLS via Rustls v0.22
|
||||||
rustls-0_22 = ["actix-tls/accept", "actix-tls/rustls-0_22"]
|
rustls-0_22 = ["actix-tls/accept", "actix-tls/rustls-0_22"]
|
||||||
|
|
||||||
|
# Proxy protocol support
|
||||||
|
proxy-protocol = ["ppp"]
|
||||||
|
|
||||||
# Compression codecs
|
# Compression codecs
|
||||||
compress-brotli = ["__compress", "brotli"]
|
compress-brotli = ["__compress", "brotli"]
|
||||||
compress-gzip = ["__compress", "flate2"]
|
compress-gzip = ["__compress", "flate2"]
|
||||||
|
@ -111,13 +114,16 @@ rand = { version = "0.8", optional = true }
|
||||||
sha1 = { version = "0.10", optional = true }
|
sha1 = { version = "0.10", optional = true }
|
||||||
|
|
||||||
# openssl/rustls
|
# openssl/rustls
|
||||||
actix-tls = { version = "3.3", default-features = false, optional = true }
|
actix-tls = { version = "3.1", default-features = false, optional = true }
|
||||||
|
|
||||||
# compress-*
|
# compress-*
|
||||||
brotli = { version = "3.3.3", optional = true }
|
brotli = { version = "3.3.3", optional = true }
|
||||||
flate2 = { version = "1.0.13", optional = true }
|
flate2 = { version = "1.0.13", optional = true }
|
||||||
zstd = { version = "0.13", optional = true }
|
zstd = { version = "0.13", optional = true }
|
||||||
|
|
||||||
|
# Proxy protocol support
|
||||||
|
ppp = { version = "0.2", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-http-test = { version = "3", features = ["openssl"] }
|
actix-http-test = { version = "3", features = ["openssl"] }
|
||||||
actix-server = "2"
|
actix-server = "2"
|
||||||
|
|
|
@ -23,6 +23,7 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
|
||||||
upgrade: Option<U>,
|
upgrade: Option<U>,
|
||||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||||
_phantom: PhantomData<S>,
|
_phantom: PhantomData<S>,
|
||||||
|
proxy_protocol: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, S> Default for HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
|
impl<T, S> Default for HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
|
||||||
|
@ -46,6 +47,8 @@ where
|
||||||
upgrade: None,
|
upgrade: None,
|
||||||
on_connect_ext: None,
|
on_connect_ext: None,
|
||||||
_phantom: PhantomData,
|
_phantom: PhantomData,
|
||||||
|
|
||||||
|
proxy_protocol: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,6 +127,12 @@ where
|
||||||
self.client_disconnect_timeout(dur)
|
self.client_disconnect_timeout(dur)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enable `PROXY` protocol support.
|
||||||
|
pub fn proxy_protocol(mut self, enabled: bool) -> Self {
|
||||||
|
self.proxy_protocol = enabled;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Provide service for `EXPECT: 100-Continue` support.
|
/// Provide service for `EXPECT: 100-Continue` support.
|
||||||
///
|
///
|
||||||
/// Service get called with request that contains `EXPECT` header.
|
/// Service get called with request that contains `EXPECT` header.
|
||||||
|
@ -146,6 +155,7 @@ where
|
||||||
upgrade: self.upgrade,
|
upgrade: self.upgrade,
|
||||||
on_connect_ext: self.on_connect_ext,
|
on_connect_ext: self.on_connect_ext,
|
||||||
_phantom: PhantomData,
|
_phantom: PhantomData,
|
||||||
|
proxy_protocol: self.proxy_protocol,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,6 +180,7 @@ where
|
||||||
upgrade: Some(upgrade.into_factory()),
|
upgrade: Some(upgrade.into_factory()),
|
||||||
on_connect_ext: self.on_connect_ext,
|
on_connect_ext: self.on_connect_ext,
|
||||||
_phantom: PhantomData,
|
_phantom: PhantomData,
|
||||||
|
proxy_protocol: self.proxy_protocol,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,6 +212,7 @@ where
|
||||||
self.client_disconnect_timeout,
|
self.client_disconnect_timeout,
|
||||||
self.secure,
|
self.secure,
|
||||||
self.local_addr,
|
self.local_addr,
|
||||||
|
self.proxy_protocol,
|
||||||
);
|
);
|
||||||
|
|
||||||
H1Service::with_config(cfg, service.into_factory())
|
H1Service::with_config(cfg, service.into_factory())
|
||||||
|
@ -226,6 +238,7 @@ where
|
||||||
self.client_disconnect_timeout,
|
self.client_disconnect_timeout,
|
||||||
self.secure,
|
self.secure,
|
||||||
self.local_addr,
|
self.local_addr,
|
||||||
|
self.proxy_protocol,
|
||||||
);
|
);
|
||||||
|
|
||||||
crate::h2::H2Service::with_config(cfg, service.into_factory())
|
crate::h2::H2Service::with_config(cfg, service.into_factory())
|
||||||
|
@ -248,6 +261,7 @@ where
|
||||||
self.client_disconnect_timeout,
|
self.client_disconnect_timeout,
|
||||||
self.secure,
|
self.secure,
|
||||||
self.local_addr,
|
self.local_addr,
|
||||||
|
self.proxy_protocol,
|
||||||
);
|
);
|
||||||
|
|
||||||
HttpService::with_config(cfg, service.into_factory())
|
HttpService::with_config(cfg, service.into_factory())
|
||||||
|
|
|
@ -20,6 +20,7 @@ struct Inner {
|
||||||
secure: bool,
|
secure: bool,
|
||||||
local_addr: Option<std::net::SocketAddr>,
|
local_addr: Option<std::net::SocketAddr>,
|
||||||
date_service: DateService,
|
date_service: DateService,
|
||||||
|
proxy_protocol: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ServiceConfig {
|
impl Default for ServiceConfig {
|
||||||
|
@ -30,6 +31,7 @@ impl Default for ServiceConfig {
|
||||||
Duration::ZERO,
|
Duration::ZERO,
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,6 +44,7 @@ impl ServiceConfig {
|
||||||
client_disconnect_timeout: Duration,
|
client_disconnect_timeout: Duration,
|
||||||
secure: bool,
|
secure: bool,
|
||||||
local_addr: Option<net::SocketAddr>,
|
local_addr: Option<net::SocketAddr>,
|
||||||
|
proxy_protocol: bool,
|
||||||
) -> ServiceConfig {
|
) -> ServiceConfig {
|
||||||
ServiceConfig(Rc::new(Inner {
|
ServiceConfig(Rc::new(Inner {
|
||||||
keep_alive: keep_alive.normalize(),
|
keep_alive: keep_alive.normalize(),
|
||||||
|
@ -50,6 +53,7 @@ impl ServiceConfig {
|
||||||
secure,
|
secure,
|
||||||
local_addr,
|
local_addr,
|
||||||
date_service: DateService::new(),
|
date_service: DateService::new(),
|
||||||
|
proxy_protocol,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,6 +77,12 @@ impl ServiceConfig {
|
||||||
self.0.keep_alive
|
self.0.keep_alive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Proxy protocol setting.
|
||||||
|
#[inline]
|
||||||
|
pub fn proxy_protocol(&self) -> bool {
|
||||||
|
self.0.proxy_protocol
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a time object representing the deadline for this connection's keep-alive period, if
|
/// Creates a time object representing the deadline for this connection's keep-alive period, if
|
||||||
/// enabled.
|
/// enabled.
|
||||||
///
|
///
|
||||||
|
@ -143,8 +153,14 @@ mod tests {
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_date_service_update() {
|
async fn test_date_service_update() {
|
||||||
let settings =
|
let settings = ServiceConfig::new(
|
||||||
ServiceConfig::new(KeepAlive::Os, Duration::ZERO, Duration::ZERO, false, None);
|
KeepAlive::Os,
|
||||||
|
Duration::ZERO,
|
||||||
|
Duration::ZERO,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
yield_now().await;
|
yield_now().await;
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ use super::{
|
||||||
encoder, Message, MessageType,
|
encoder, Message, MessageType,
|
||||||
};
|
};
|
||||||
use crate::{body::BodySize, error::ParseError, ConnectionType, Request, Response, ServiceConfig};
|
use crate::{body::BodySize, error::ParseError, ConnectionType, Request, Response, ServiceConfig};
|
||||||
|
#[cfg(feature = "proxy-protocol")]
|
||||||
|
use crate::{http_message::HttpMessage, proxy_protocol::ProxyProtocol};
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
@ -110,6 +112,7 @@ impl Decoder for Codec {
|
||||||
type Error = ParseError;
|
type Error = ParseError;
|
||||||
|
|
||||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||||
|
#[allow(clippy::collapsible_else_if)]
|
||||||
if let Some(ref mut payload) = self.payload {
|
if let Some(ref mut payload) = self.payload {
|
||||||
Ok(match payload.decode(src)? {
|
Ok(match payload.decode(src)? {
|
||||||
Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))),
|
Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))),
|
||||||
|
@ -119,7 +122,19 @@ impl Decoder for Codec {
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
})
|
})
|
||||||
} else if let Some((req, payload)) = self.decoder.decode(src)? {
|
} else {
|
||||||
|
#[cfg(feature = "proxy-protocol")]
|
||||||
|
let proxy_protocol = if self.config.proxy_protocol() {
|
||||||
|
let p = ProxyProtocol::decode(src)?;
|
||||||
|
if p.is_none() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
p
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((req, payload)) = self.decoder.decode(src)? {
|
||||||
let head = req.head();
|
let head = req.head();
|
||||||
self.flags.set(Flags::HEAD, head.method == Method::HEAD);
|
self.flags.set(Flags::HEAD, head.method == Method::HEAD);
|
||||||
self.version = head.version;
|
self.version = head.version;
|
||||||
|
@ -131,6 +146,13 @@ impl Decoder for Codec {
|
||||||
self.conn_type = ConnectionType::Close
|
self.conn_type = ConnectionType::Close
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "proxy-protocol")]
|
||||||
|
// set proxy protocol
|
||||||
|
if let Some(proxy_protocol) = proxy_protocol {
|
||||||
|
let mut extensions = req.extensions_mut();
|
||||||
|
extensions.insert(proxy_protocol);
|
||||||
|
}
|
||||||
|
|
||||||
match payload {
|
match payload {
|
||||||
PayloadType::None => self.payload = None,
|
PayloadType::None => self.payload = None,
|
||||||
PayloadType::Payload(pl) => self.payload = Some(pl),
|
PayloadType::Payload(pl) => self.payload = Some(pl),
|
||||||
|
@ -145,6 +167,7 @@ impl Decoder for Codec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
|
impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
|
||||||
type Error = io::Error;
|
type Error = io::Error;
|
||||||
|
|
|
@ -84,6 +84,7 @@ async fn late_request() {
|
||||||
Duration::ZERO,
|
Duration::ZERO,
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
let services = HttpFlow::new(ok_service(), ExpectHandler, None);
|
let services = HttpFlow::new(ok_service(), ExpectHandler, None);
|
||||||
|
|
||||||
|
@ -151,6 +152,7 @@ async fn oneshot_connection() {
|
||||||
Duration::ZERO,
|
Duration::ZERO,
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||||
|
|
||||||
|
@ -212,6 +214,7 @@ async fn keep_alive_timeout() {
|
||||||
Duration::ZERO,
|
Duration::ZERO,
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||||
|
|
||||||
|
@ -291,6 +294,7 @@ async fn keep_alive_follow_up_req() {
|
||||||
Duration::ZERO,
|
Duration::ZERO,
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||||
|
|
||||||
|
@ -455,6 +459,7 @@ async fn pipelining_ok_then_ok() {
|
||||||
Duration::from_millis(1),
|
Duration::from_millis(1),
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||||
|
@ -525,6 +530,7 @@ async fn pipelining_ok_then_bad() {
|
||||||
Duration::from_millis(1),
|
Duration::from_millis(1),
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||||
|
@ -588,6 +594,7 @@ async fn expect_handling() {
|
||||||
Duration::ZERO,
|
Duration::ZERO,
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let services = HttpFlow::new(echo_payload_service(), ExpectHandler, None);
|
let services = HttpFlow::new(echo_payload_service(), ExpectHandler, None);
|
||||||
|
@ -665,6 +672,7 @@ async fn expect_eager() {
|
||||||
Duration::ZERO,
|
Duration::ZERO,
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||||
|
@ -748,6 +756,7 @@ async fn upgrade_handling() {
|
||||||
Duration::ZERO,
|
Duration::ZERO,
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let services = HttpFlow::new(ok_service(), ExpectHandler, Some(TestUpgrade));
|
let services = HttpFlow::new(ok_service(), ExpectHandler, Some(TestUpgrade));
|
||||||
|
|
|
@ -49,6 +49,8 @@ mod message;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod notify_on_drop;
|
mod notify_on_drop;
|
||||||
mod payload;
|
mod payload;
|
||||||
|
#[cfg(feature = "proxy-protocol")]
|
||||||
|
pub mod proxy_protocol;
|
||||||
mod requests;
|
mod requests;
|
||||||
mod responses;
|
mod responses;
|
||||||
mod service;
|
mod service;
|
||||||
|
|
177
actix-http/src/proxy_protocol.rs
Normal file
177
actix-http/src/proxy_protocol.rs
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
pub use ppp::{
|
||||||
|
v1::Addresses as V1Addresses,
|
||||||
|
v2::{Addresses as V2Addresses, Command, Protocol, Type as TlvType},
|
||||||
|
};
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
use crate::error::ParseError;
|
||||||
|
|
||||||
|
const V1_PREFIX_LEN: usize = 5;
|
||||||
|
const V1_MAX_LEN: usize = 107;
|
||||||
|
const V2_PREFIX_LEN: usize = 12;
|
||||||
|
const V2_MIN_LEN: usize = 16;
|
||||||
|
const V2_LEN_INDEX_1: usize = 14;
|
||||||
|
const V2_LEN_INDEX_2: usize = 15;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum ProxyProtocol {
|
||||||
|
V1(ProxyProtocolV1),
|
||||||
|
V2(ProxyProtocolV2),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProxyProtocol {
|
||||||
|
pub(crate) fn decode(src: &mut BytesMut) -> Result<Option<Self>, ParseError> {
|
||||||
|
if src.len() >= V1_PREFIX_LEN
|
||||||
|
&& &src[..V1_PREFIX_LEN] == ppp::v1::PROTOCOL_PREFIX.as_bytes()
|
||||||
|
{
|
||||||
|
if let Some(line_end) = src.iter().position(|b| *b == b'\r') {
|
||||||
|
if let Some(delimiter) = src.get(line_end + 1) {
|
||||||
|
if delimiter == &b'\n' {
|
||||||
|
let proxy_line = src.split_to(line_end + 2).freeze();
|
||||||
|
|
||||||
|
if proxy_line.len() > V1_MAX_LEN {
|
||||||
|
trace!("proxy protocol header too long");
|
||||||
|
return Err(ParseError::Header);
|
||||||
|
}
|
||||||
|
|
||||||
|
match ppp::v1::Header::try_from(&proxy_line[..]) {
|
||||||
|
Ok(header) => Ok(Some(
|
||||||
|
ProxyProtocolV1 {
|
||||||
|
addresses: header.addresses,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
)),
|
||||||
|
Err(e) => {
|
||||||
|
trace!("error parsing proxy protocol v1 header: {:?}", e);
|
||||||
|
Err(ParseError::Header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trace!("invalid line ending found");
|
||||||
|
Err(ParseError::Header)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trace!("no line ending found, might be a partial request");
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
} else if src.len() > V1_MAX_LEN {
|
||||||
|
trace!("proxy protocol header too long");
|
||||||
|
Err(ParseError::Header)
|
||||||
|
} else {
|
||||||
|
trace!("no line ending found, might be a partial request");
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
} else if src.len() >= V2_PREFIX_LEN && &src[..V2_PREFIX_LEN] == ppp::v2::PROTOCOL_PREFIX {
|
||||||
|
if src.len() < V2_MIN_LEN {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_length = V2_MIN_LEN
|
||||||
|
+ u16::from_be_bytes([src[V2_LEN_INDEX_1], src[V2_LEN_INDEX_2]]) as usize;
|
||||||
|
|
||||||
|
if src.len() < total_length {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let proxy_line = src.split_to(total_length).freeze();
|
||||||
|
|
||||||
|
match ppp::v2::Header::try_from(&proxy_line[..]) {
|
||||||
|
Ok(header) => {
|
||||||
|
let size_hint = header.tlvs().size_hint();
|
||||||
|
let mut converted_tlvs: Vec<Tlv> =
|
||||||
|
Vec::with_capacity(size_hint.1.unwrap_or(size_hint.0));
|
||||||
|
|
||||||
|
for tlv in header.tlvs() {
|
||||||
|
match tlv {
|
||||||
|
Ok(tlv) => {
|
||||||
|
converted_tlvs.push(tlv.into());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
trace!(
|
||||||
|
"error parsing proxy protocol v2 type length value: {:?}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return Err(ParseError::Header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(
|
||||||
|
ProxyProtocolV2 {
|
||||||
|
addresses: header.addresses,
|
||||||
|
command: header.command,
|
||||||
|
protocol: header.protocol,
|
||||||
|
tlvs: converted_tlvs,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
trace!("error parsing proxy protocol v1 header: {:?}", e);
|
||||||
|
return Err(ParseError::Header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if src.len() < V1_PREFIX_LEN || src.len() < V2_PREFIX_LEN {
|
||||||
|
trace!("not enough data to parse proxy protocol header");
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
trace!("invalid proxy protocol header");
|
||||||
|
Err(ParseError::Header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct ProxyProtocolV1 {
|
||||||
|
pub addresses: V1Addresses,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct ProxyProtocolV2 {
|
||||||
|
pub addresses: V2Addresses,
|
||||||
|
pub command: Command,
|
||||||
|
pub protocol: Protocol,
|
||||||
|
pub tlvs: Vec<Tlv>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Tlv {
|
||||||
|
pub kind: TlvType,
|
||||||
|
pub value: bytes::Bytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ProxyProtocolV1> for ProxyProtocol {
|
||||||
|
fn from(v1: ProxyProtocolV1) -> Self {
|
||||||
|
ProxyProtocol::V1(v1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ProxyProtocolV2> for ProxyProtocol {
|
||||||
|
fn from(v2: ProxyProtocolV2) -> Self {
|
||||||
|
ProxyProtocol::V2(v2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ppp::v2::TypeLengthValue<'_>> for Tlv {
|
||||||
|
fn from(tlv: ppp::v2::TypeLengthValue<'_>) -> Self {
|
||||||
|
Tlv {
|
||||||
|
kind: match tlv.kind {
|
||||||
|
0x01 => TlvType::ALPN,
|
||||||
|
0x02 => TlvType::Authority,
|
||||||
|
0x03 => TlvType::CRC32C,
|
||||||
|
0x04 => TlvType::NoOp,
|
||||||
|
0x05 => TlvType::UniqueId,
|
||||||
|
0x20 => TlvType::SSL,
|
||||||
|
0x21 => TlvType::SSLVersion,
|
||||||
|
0x22 => TlvType::SSLCommonName,
|
||||||
|
0x23 => TlvType::SSLCipher,
|
||||||
|
0x24 => TlvType::SSLSignatureAlgorithm,
|
||||||
|
0x25 => TlvType::SSLKeyAlgorithm,
|
||||||
|
0x30 => TlvType::NetworkNamespace,
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
value: Bytes::copy_from_slice(&tlv.value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -173,6 +173,7 @@ where
|
||||||
|
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.client_request_timeout(timeout)
|
.client_request_timeout(timeout)
|
||||||
|
.proxy_protocol(srv_cfg.proxy_protocol)
|
||||||
.h1(map_config(fac, move |_| app_cfg.clone()))
|
.h1(map_config(fac, move |_| app_cfg.clone()))
|
||||||
.tcp()
|
.tcp()
|
||||||
}),
|
}),
|
||||||
|
@ -461,6 +462,7 @@ pub struct TestServerConfig {
|
||||||
client_request_timeout: Duration,
|
client_request_timeout: Duration,
|
||||||
port: u16,
|
port: u16,
|
||||||
workers: usize,
|
workers: usize,
|
||||||
|
proxy_protocol: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TestServerConfig {
|
impl Default for TestServerConfig {
|
||||||
|
@ -478,6 +480,7 @@ impl TestServerConfig {
|
||||||
client_request_timeout: Duration::from_secs(5),
|
client_request_timeout: Duration::from_secs(5),
|
||||||
port: 0,
|
port: 0,
|
||||||
workers: 1,
|
workers: 1,
|
||||||
|
proxy_protocol: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -558,6 +561,14 @@ impl TestServerConfig {
|
||||||
self.workers = workers;
|
self.workers = workers;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enable proxy protocol support.
|
||||||
|
///
|
||||||
|
/// By default, the server does not use proxy protocol.
|
||||||
|
pub fn proxy_protocol(mut self) -> Self {
|
||||||
|
self.proxy_protocol = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A basic HTTP server controller that simplifies the process of writing integration tests for
|
/// A basic HTTP server controller that simplifies the process of writing integration tests for
|
||||||
|
|
|
@ -72,6 +72,9 @@ rustls-0_21 = ["http2", "actix-http/rustls-0_21", "actix-tls/accept", "actix-tls
|
||||||
# TLS via Rustls v0.22
|
# TLS via Rustls v0.22
|
||||||
rustls-0_22 = ["http2", "actix-http/rustls-0_22", "actix-tls/accept", "actix-tls/rustls-0_22"]
|
rustls-0_22 = ["http2", "actix-http/rustls-0_22", "actix-tls/accept", "actix-tls/rustls-0_22"]
|
||||||
|
|
||||||
|
# Proxy protocol support
|
||||||
|
proxy-protocol = ["actix-http/proxy-protocol"]
|
||||||
|
|
||||||
# Internal (PRIVATE!) features used to aid testing and checking feature status.
|
# Internal (PRIVATE!) features used to aid testing and checking feature status.
|
||||||
# Don't rely on these whatsoever. They may disappear at anytime.
|
# Don't rely on these whatsoever. They may disappear at anytime.
|
||||||
__compress = []
|
__compress = []
|
||||||
|
@ -145,6 +148,10 @@ required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cooki
|
||||||
name = "compression"
|
name = "compression"
|
||||||
required-features = ["compress-brotli", "compress-gzip", "compress-zstd"]
|
required-features = ["compress-brotli", "compress-gzip", "compress-zstd"]
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "proxy_protocol"
|
||||||
|
required-features = ["proxy-protocol"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "basic"
|
name = "basic"
|
||||||
required-features = ["compress-gzip"]
|
required-features = ["compress-gzip"]
|
||||||
|
|
|
@ -36,6 +36,7 @@ struct Config {
|
||||||
client_disconnect_timeout: Duration,
|
client_disconnect_timeout: Duration,
|
||||||
#[allow(dead_code)] // only dead when no TLS features are enabled
|
#[allow(dead_code)] // only dead when no TLS features are enabled
|
||||||
tls_handshake_timeout: Option<Duration>,
|
tls_handshake_timeout: Option<Duration>,
|
||||||
|
proxy_protocol: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An HTTP Server.
|
/// An HTTP Server.
|
||||||
|
@ -119,6 +120,7 @@ where
|
||||||
client_request_timeout: Duration::from_secs(5),
|
client_request_timeout: Duration::from_secs(5),
|
||||||
client_disconnect_timeout: Duration::from_secs(1),
|
client_disconnect_timeout: Duration::from_secs(1),
|
||||||
tls_handshake_timeout: None,
|
tls_handshake_timeout: None,
|
||||||
|
proxy_protocol: false,
|
||||||
})),
|
})),
|
||||||
backlog: 1024,
|
backlog: 1024,
|
||||||
sockets: Vec::new(),
|
sockets: Vec::new(),
|
||||||
|
@ -155,6 +157,21 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets if the server should use the PROXY protocol.
|
||||||
|
pub fn proxy_protocol(self, enabled: bool) -> Self {
|
||||||
|
#[cfg(not(feature = "proxy-protocol"))]
|
||||||
|
{
|
||||||
|
if enabled {
|
||||||
|
panic!(
|
||||||
|
"Proxy protocol support is not enabled. Enable the `proxy-protocol` feature."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.config.lock().unwrap().proxy_protocol = enabled;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the maximum number of pending connections.
|
/// Sets the maximum number of pending connections.
|
||||||
///
|
///
|
||||||
/// This refers to the number of clients that can be waiting to be served. Exceeding this number
|
/// This refers to the number of clients that can be waiting to be served. Exceeding this number
|
||||||
|
@ -513,6 +530,7 @@ where
|
||||||
.keep_alive(cfg.keep_alive)
|
.keep_alive(cfg.keep_alive)
|
||||||
.client_request_timeout(cfg.client_request_timeout)
|
.client_request_timeout(cfg.client_request_timeout)
|
||||||
.client_disconnect_timeout(cfg.client_disconnect_timeout)
|
.client_disconnect_timeout(cfg.client_disconnect_timeout)
|
||||||
|
.proxy_protocol(cfg.proxy_protocol)
|
||||||
.local_addr(addr);
|
.local_addr(addr);
|
||||||
|
|
||||||
if let Some(handler) = on_connect_fn.clone() {
|
if let Some(handler) = on_connect_fn.clone() {
|
||||||
|
|
113
actix-web/tests/proxy_protocol.rs
Normal file
113
actix-web/tests/proxy_protocol.rs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
use std::{
|
||||||
|
io::{Read, Write},
|
||||||
|
net::{Ipv4Addr, Shutdown, TcpStream},
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_http::proxy_protocol::{
|
||||||
|
Command, Protocol, ProxyProtocol, ProxyProtocolV1, ProxyProtocolV2, TlvType, V1Addresses,
|
||||||
|
V2Addresses,
|
||||||
|
};
|
||||||
|
use actix_test::TestServerConfig;
|
||||||
|
use actix_web::{get, App, HttpMessage, HttpRequest, HttpResponse, Responder};
|
||||||
|
|
||||||
|
#[get("/v1")]
|
||||||
|
async fn proxy_protocol_v1(req: HttpRequest) -> impl Responder {
|
||||||
|
let extensions = req.extensions();
|
||||||
|
let proxy_protocol = extensions.get::<ProxyProtocol>().unwrap();
|
||||||
|
|
||||||
|
if let ProxyProtocol::V1(ProxyProtocolV1 {
|
||||||
|
addresses: V1Addresses::Tcp4(addr),
|
||||||
|
}) = proxy_protocol
|
||||||
|
{
|
||||||
|
if addr.source_address == Ipv4Addr::new(127, 0, 1, 2)
|
||||||
|
&& addr.destination_address == Ipv4Addr::new(192, 168, 1, 101)
|
||||||
|
&& addr.source_port == 80
|
||||||
|
&& addr.destination_port == 443
|
||||||
|
{
|
||||||
|
return HttpResponse::Ok().body(format!("{:?}", proxy_protocol));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse::NotFound().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/v2")]
|
||||||
|
async fn proxy_protocol_v2(req: HttpRequest) -> impl Responder {
|
||||||
|
let extensions = req.extensions();
|
||||||
|
let proxy_protocol = extensions.get::<ProxyProtocol>().unwrap();
|
||||||
|
|
||||||
|
if let ProxyProtocol::V2(ProxyProtocolV2 {
|
||||||
|
addresses: V2Addresses::IPv4(addr),
|
||||||
|
command,
|
||||||
|
protocol,
|
||||||
|
tlvs,
|
||||||
|
}) = proxy_protocol
|
||||||
|
{
|
||||||
|
if addr.source_address == Ipv4Addr::new(127, 0, 1, 2)
|
||||||
|
&& addr.destination_address == Ipv4Addr::new(192, 168, 1, 101)
|
||||||
|
&& addr.source_port == 80
|
||||||
|
&& addr.destination_port == 443
|
||||||
|
&& matches!(command, Command::Proxy)
|
||||||
|
&& matches!(protocol, Protocol::Datagram)
|
||||||
|
&& tlvs.len() == 1
|
||||||
|
&& tlvs[0].kind == TlvType::NoOp
|
||||||
|
&& tlvs[0].value[..] == [42]
|
||||||
|
{
|
||||||
|
return HttpResponse::Ok().body(format!("{:?}", proxy_protocol));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse::NotFound().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_parse_proxy_protocol_v1() {
|
||||||
|
let srv = actix_test::start_with(TestServerConfig::default().h1().proxy_protocol(), || {
|
||||||
|
App::new().service(proxy_protocol_v1)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut stream = TcpStream::connect(srv.addr()).unwrap();
|
||||||
|
stream
|
||||||
|
.write_all(
|
||||||
|
b"PROXY TCP4 127.0.1.2 192.168.1.101 80 443\r\nGET /v1 HTTP/1.1\r\n\r\n".as_ref(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut buf = [0; 1024];
|
||||||
|
let n = stream.read(&mut buf).unwrap();
|
||||||
|
|
||||||
|
let response = String::from_utf8_lossy(&buf[..n]);
|
||||||
|
println!("{}", response);
|
||||||
|
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
|
||||||
|
|
||||||
|
stream.shutdown(Shutdown::Both).unwrap();
|
||||||
|
|
||||||
|
srv.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_parse_proxy_protocol_v2() {
|
||||||
|
let srv = actix_test::start_with(TestServerConfig::default().h1().proxy_protocol(), || {
|
||||||
|
App::new().service(proxy_protocol_v2)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut stream = TcpStream::connect(srv.addr()).unwrap();
|
||||||
|
stream.write_all(b"\r\n\r\n\0\r\nQUIT\n".as_ref()).unwrap();
|
||||||
|
stream
|
||||||
|
.write_all(&[
|
||||||
|
0x21, 0x12, 0, 16, 127, 0, 1, 2, 192, 168, 1, 101, 0, 80, 1, 187, 4, 0, 1, 42,
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
stream.write_all(b"GET /v2 HTTP/1.1\r\n\r\n").unwrap();
|
||||||
|
|
||||||
|
let mut buf = [0; 1024];
|
||||||
|
let n = stream.read(&mut buf).unwrap();
|
||||||
|
|
||||||
|
let response = String::from_utf8_lossy(&buf[..n]);
|
||||||
|
println!("{}", response);
|
||||||
|
assert!(response.starts_with("HTTP/1.1 200 OK\r\n"));
|
||||||
|
|
||||||
|
stream.shutdown(Shutdown::Both).unwrap();
|
||||||
|
|
||||||
|
srv.stop().await;
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ save_exit_code() {
|
||||||
|
|
||||||
save_exit_code cargo test --lib --tests -p=actix-router --all-features -- --nocapture
|
save_exit_code cargo test --lib --tests -p=actix-router --all-features -- --nocapture
|
||||||
save_exit_code cargo test --lib --tests -p=actix-http --all-features -- --nocapture
|
save_exit_code cargo test --lib --tests -p=actix-http --all-features -- --nocapture
|
||||||
save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --nocapture
|
save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl,proxy-protocol -- --nocapture
|
||||||
save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features -- --nocapture
|
save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features -- --nocapture
|
||||||
save_exit_code cargo test --lib --tests -p=awc --all-features -- --nocapture
|
save_exit_code cargo test --lib --tests -p=awc --all-features -- --nocapture
|
||||||
save_exit_code cargo test --lib --tests -p=actix-http-test --all-features -- --nocapture
|
save_exit_code cargo test --lib --tests -p=actix-http-test --all-features -- --nocapture
|
||||||
|
|
Loading…
Reference in a new issue