Skip to content

Commit

Permalink
Restructure to appsec + debug log for appsec
Browse files Browse the repository at this point in the history
  • Loading branch information
tiberiuv committed Oct 28, 2024
1 parent df44198 commit dbfda50
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 166 deletions.
4 changes: 2 additions & 2 deletions src/api.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::net::{IpAddr, SocketAddr};

use crate::crowdsec::CrowdsecLapi;
use crate::crowdsec::CrowdsecAppsecApi;
use axum::extract::{ConnectInfo, Request, State};
use axum::http::HeaderValue;
use axum::response::IntoResponse;
Expand Down Expand Up @@ -94,7 +94,7 @@ async fn check_ip(
);

let result = app_state
.lapi_client
.appsec_client
.appsec_request(request, real_client_ip)
.await;
match result {
Expand Down
166 changes: 6 additions & 160 deletions src/crowdsec.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
use std::net::IpAddr;
mod appsec;
mod headers;

pub use appsec::{AppsecClient, CrowdsecAppsecApi};

use anyhow::anyhow;
use axum::body::{Body, HttpBody};
use axum::extract::Request;
use axum::http::{HeaderMap, HeaderName, HeaderValue, Uri};
use hyper_rustls::{ConfigBuilderExt, HttpsConnector};
use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
use reqwest::{header, Certificate, Identity, StatusCode, Url};
use reqwest::{Certificate, Identity};
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls::{ClientConfig, RootCertStore};

use crate::cli::ClientCerts;
use crate::USER_AGENT;

#[allow(unused)]
#[derive(Debug, Clone)]
pub struct CertAuthReqwest {
pub root_ca: Certificate,
Expand Down Expand Up @@ -53,154 +50,3 @@ impl TryFrom<ClientCerts> for CertAuthRustls {
})
}
}

type Client = hyper_util::client::legacy::Client<HttpsConnector<HttpConnector>, Body>;
#[derive(Debug, Clone)]
pub struct LapiClient {
client: Client,
url: Url,
apikey: String,
}

impl LapiClient {
pub fn new(url: Url, certs: Option<CertAuthRustls>, apikey: String) -> Self {
let tls_config = if let Some(certs) = certs {
let mut cert_store = RootCertStore::empty();
cert_store.add(certs.root_ca).unwrap();
ClientConfig::builder()
.with_root_certificates(cert_store)
.with_client_auth_cert(vec![certs.client_cert], certs.client_key.clone_key())
.unwrap()
} else {
ClientConfig::builder()
.with_webpki_roots()
.with_no_client_auth()
};

let connector = HttpsConnector::<HttpConnector>::builder()
.with_tls_config(tls_config)
.https_or_http()
.enable_http2()
.build();

let client = hyper_util::client::legacy::Client::<(), ()>::builder(TokioExecutor::new())
.build(connector);

Self {
client,
url,
apikey,
}
}
}
pub const X_CROWDSEC_APPSEC_IP_HEADER: HeaderName = HeaderName::from_static("x-crowdsec-appsec-ip");
pub const X_CROWDSEC_APPSEC_URI_HEADER: HeaderName =
HeaderName::from_static("x-crowdsec-appsec-uri");
pub const X_CROWDSEC_APPSEC_HOST_HEADER: HeaderName =
HeaderName::from_static("x-crowdsec-appsec-host");
pub const X_CROWDSEC_APPSEC_VERB_HEADER: HeaderName =
HeaderName::from_static("x-crowdsec-appsec-verb");
pub const X_CROWDSEC_APPSEC_API_KEY_HEADER: HeaderName =
HeaderName::from_static("x-crowdsec-appsec-api-key");
pub const X_CROWDSEC_APPSEC_USER_AGENT_HEADER: HeaderName =
HeaderName::from_static("x-crowdsec-appsec-user-agent");
pub const X_FORWARDED_METHOD: HeaderName = HeaderName::from_static("x-forwarded-method");
pub const X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
pub const X_FORWARDED_URI: HeaderName = HeaderName::from_static("x-forwarded-uri");

#[allow(async_fn_in_trait)]
pub trait CrowdsecLapi {
async fn appsec_request(
&self,
request: Request,
real_client_ip: IpAddr,
) -> anyhow::Result<bool>;
}

impl CrowdsecLapi for LapiClient {
async fn appsec_request(
&self,
mut request: Request,
real_client_ip: IpAddr,
) -> anyhow::Result<bool> {
let forwarded_host = request
.headers()
.get(X_FORWARDED_HOST)
.and_then(|x| x.to_str().ok().map(|x| x.to_string()))
.unwrap_or_else(|| {
request
.headers()
.get(header::HOST)
.and_then(|x| x.to_str().ok().map(|x| x.to_string()))
.unwrap_or_default()
});
let user_agent_header = request
.headers()
.get(header::USER_AGENT)
.and_then(|x| x.to_str().ok())
.unwrap_or_default();
let forwarded_uri = request
.headers()
.get(X_FORWARDED_URI)
.and_then(|x| x.to_str().ok().map(|x| x.to_string()))
.unwrap_or(request.uri().to_string());
let forwarded_method = request
.headers()
.get(X_FORWARDED_METHOD)
.and_then(|x| x.to_str().ok().map(|x| x.to_string()))
.unwrap_or(request.method().to_string());

let headers = HeaderMap::from_iter([
(
X_CROWDSEC_APPSEC_IP_HEADER,
HeaderValue::from_str(&real_client_ip.to_string())?,
),
(
X_CROWDSEC_APPSEC_API_KEY_HEADER,
HeaderValue::from_str(&self.apikey)?,
),
(
X_CROWDSEC_APPSEC_HOST_HEADER,
HeaderValue::from_str(&forwarded_host)?,
),
(
X_CROWDSEC_APPSEC_VERB_HEADER,
HeaderValue::from_str(&forwarded_method)?,
),
(
X_CROWDSEC_APPSEC_URI_HEADER,
HeaderValue::from_str(&forwarded_uri)?,
),
(
X_CROWDSEC_APPSEC_USER_AGENT_HEADER,
HeaderValue::from_str(user_agent_header)?,
),
(
reqwest::header::USER_AGENT,
HeaderValue::from_str(USER_AGENT)?,
),
]);

let mut_headers = request.headers_mut();
mut_headers.extend(headers);

*request.uri_mut() = Uri::try_from(self.url.to_string())?;
*request.method_mut() = if request.body().is_end_stream() {
reqwest::Method::GET
} else {
reqwest::Method::POST
};

let response = self.client.request(request).await?;
let is_ok = response.status() == StatusCode::OK;
tracing::info!(
status = ?response.status(),
original_uri = forwarded_uri,
original_method = forwarded_method,
original_host = forwarded_host,
original_ip = real_client_ip.to_string(),
"appsec request"
);
Ok(is_ok)
}
}
154 changes: 154 additions & 0 deletions src/crowdsec/appsec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use std::net::IpAddr;

use axum::body::{Body, HttpBody};
use axum::extract::Request;
use axum::http::{HeaderMap, HeaderValue, Uri};
use hyper_rustls::{ConfigBuilderExt, HttpsConnector};
use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
use reqwest::{header, StatusCode, Url};
use rustls::{ClientConfig, RootCertStore};

use super::headers::{
X_CROWDSEC_APPSEC_API_KEY_HEADER, X_CROWDSEC_APPSEC_HOST_HEADER, X_CROWDSEC_APPSEC_IP_HEADER,
X_CROWDSEC_APPSEC_URI_HEADER, X_CROWDSEC_APPSEC_USER_AGENT_HEADER,
X_CROWDSEC_APPSEC_VERB_HEADER, X_FORWARDED_HOST, X_FORWARDED_METHOD, X_FORWARDED_URI,
};
use super::CertAuthRustls;
use crate::USER_AGENT;

type Client = hyper_util::client::legacy::Client<HttpsConnector<HttpConnector>, Body>;
#[derive(Debug, Clone)]
pub struct AppsecClient {
client: Client,
url: Url,
apikey: String,
}

impl AppsecClient {
pub fn new(url: Url, certs: Option<CertAuthRustls>, apikey: String) -> Self {
let tls_config = if let Some(certs) = certs {
let mut cert_store = RootCertStore::empty();
cert_store.add(certs.root_ca).unwrap();
ClientConfig::builder()
.with_root_certificates(cert_store)
.with_client_auth_cert(vec![certs.client_cert], certs.client_key.clone_key())
.unwrap()
} else {
ClientConfig::builder()
.with_webpki_roots()
.with_no_client_auth()
};

let connector = HttpsConnector::<HttpConnector>::builder()
.with_tls_config(tls_config)
.https_or_http()
.enable_http2()
.build();

let client = hyper_util::client::legacy::Client::<(), ()>::builder(TokioExecutor::new())
.build(connector);

Self {
client,
url,
apikey,
}
}
}

#[allow(async_fn_in_trait)]
pub trait CrowdsecAppsecApi {
async fn appsec_request(
&self,
request: Request,
real_client_ip: IpAddr,
) -> anyhow::Result<bool>;
}

impl CrowdsecAppsecApi for AppsecClient {
async fn appsec_request(
&self,
mut request: Request,
real_client_ip: IpAddr,
) -> anyhow::Result<bool> {
let forwarded_host = request
.headers()
.get(X_FORWARDED_HOST)
.and_then(|x| x.to_str().ok().map(|x| x.to_string()))
.unwrap_or_else(|| {
request
.headers()
.get(header::HOST)
.and_then(|x| x.to_str().ok().map(|x| x.to_string()))
.unwrap_or_default()
});
let user_agent_header = request
.headers()
.get(header::USER_AGENT)
.and_then(|x| x.to_str().ok())
.unwrap_or_default();
let forwarded_uri = request
.headers()
.get(X_FORWARDED_URI)
.and_then(|x| x.to_str().ok().map(|x| x.to_string()))
.unwrap_or(request.uri().to_string());
let forwarded_method = request
.headers()
.get(X_FORWARDED_METHOD)
.and_then(|x| x.to_str().ok().map(|x| x.to_string()))
.unwrap_or(request.method().to_string());

let headers = HeaderMap::from_iter([
(
X_CROWDSEC_APPSEC_IP_HEADER,
HeaderValue::from_str(&real_client_ip.to_string())?,
),
(
X_CROWDSEC_APPSEC_API_KEY_HEADER,
HeaderValue::from_str(&self.apikey)?,
),
(
X_CROWDSEC_APPSEC_HOST_HEADER,
HeaderValue::from_str(&forwarded_host)?,
),
(
X_CROWDSEC_APPSEC_VERB_HEADER,
HeaderValue::from_str(&forwarded_method)?,
),
(
X_CROWDSEC_APPSEC_URI_HEADER,
HeaderValue::from_str(&forwarded_uri)?,
),
(
X_CROWDSEC_APPSEC_USER_AGENT_HEADER,
HeaderValue::from_str(user_agent_header)?,
),
(
reqwest::header::USER_AGENT,
HeaderValue::from_str(USER_AGENT)?,
),
]);

let mut_headers = request.headers_mut();
mut_headers.extend(headers);

*request.uri_mut() = Uri::try_from(self.url.to_string())?;
*request.method_mut() = if request.body().is_end_stream() {
reqwest::Method::GET
} else {
reqwest::Method::POST
};

let response = self.client.request(request).await?;
let is_ok = response.status() == StatusCode::OK;
tracing::debug!(
status = ?response.status(),
original_uri = forwarded_uri,
original_method = forwarded_method,
original_host = forwarded_host,
original_ip = real_client_ip.to_string(),
"appsec query"
);
Ok(is_ok)
}
}
16 changes: 16 additions & 0 deletions src/crowdsec/headers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use axum::http::HeaderName;

pub const X_CROWDSEC_APPSEC_IP_HEADER: HeaderName = HeaderName::from_static("x-crowdsec-appsec-ip");
pub const X_CROWDSEC_APPSEC_URI_HEADER: HeaderName =
HeaderName::from_static("x-crowdsec-appsec-uri");
pub const X_CROWDSEC_APPSEC_HOST_HEADER: HeaderName =
HeaderName::from_static("x-crowdsec-appsec-host");
pub const X_CROWDSEC_APPSEC_VERB_HEADER: HeaderName =
HeaderName::from_static("x-crowdsec-appsec-verb");
pub const X_CROWDSEC_APPSEC_API_KEY_HEADER: HeaderName =
HeaderName::from_static("x-crowdsec-appsec-api-key");
pub const X_CROWDSEC_APPSEC_USER_AGENT_HEADER: HeaderName =
HeaderName::from_static("x-crowdsec-appsec-user-agent");
pub const X_FORWARDED_METHOD: HeaderName = HeaderName::from_static("x-forwarded-method");
pub const X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
pub const X_FORWARDED_URI: HeaderName = HeaderName::from_static("x-forwarded-uri");
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod utils;
pub mod api;
pub mod cli;
pub mod trace_sub;
pub use crowdsec::{CertAuthReqwest, CertAuthRustls, CrowdsecLapi, LapiClient};
pub use crowdsec::{AppsecClient, CertAuthRustls, CrowdsecAppsecApi};

pub const USER_AGENT: &str = "waf-bouncer/v0.0.1";

Expand All @@ -18,5 +18,5 @@ pub struct Config {
#[derive(Clone)]
pub struct AppState {
pub config: Config,
pub lapi_client: LapiClient,
pub appsec_client: AppsecClient,
}
Loading

0 comments on commit dbfda50

Please sign in to comment.