use std::{net::SocketAddr, sync::Arc, time::SystemTime};
use axum::{
Router,
extract::{ConnectInfo, State},
};
use axum_connect_rpc::{
error::{CrpcError, CrpcErrorCode},
extractor::ConnectRpcAny,
};
use crate::{
pg_wap::crpc::model::{AuthorizeTargetsError, ControlServiceAPIHandler},
proto::anapaya::wap::v1 as rpc,
};
pub const SERVICE_PATH: &str = "/anapaya.wap.v1.WapControl";
pub const AUTHORIZE_TARGETS: &str = "/AuthorizeTargets";
pub fn nest_crpc_api(router: Router, handler: Arc<dyn ControlServiceAPIHandler>) -> Router {
router.nest(
SERVICE_PATH,
Router::new()
.route(
AUTHORIZE_TARGETS,
axum::routing::post(authorize_targets_handler),
)
.with_state(handler),
)
}
async fn authorize_targets_handler(
State(handler): State<Arc<dyn ControlServiceAPIHandler>>,
ConnectInfo(client): ConnectInfo<SocketAddr>,
request: ConnectRpcAny<rpc::AuthorizeTargetsRequest>,
) -> Result<ConnectRpcAny<rpc::AuthorizeTargetsResponse>, CrpcError> {
let now = SystemTime::now();
let codec = request.codec();
let request = request.into_inner().try_into_model().map_err(|err| {
tracing::debug!(%client, "Rejecting an authorization request: {err}");
CrpcError::new(CrpcErrorCode::InvalidArgument, err.to_string())
})?;
match handler.authorize_targets(client.ip(), request, now) {
Ok(res) => Ok(ConnectRpcAny::from_parts(res.into(), codec)),
Err(e) => {
tracing::info!(%client, ?e, "Authorization request failed");
let (code, msg) = match e {
AuthorizeTargetsError::InvalidRequest(msg) => (CrpcErrorCode::InvalidArgument, msg),
AuthorizeTargetsError::LimitReached(msg) => (CrpcErrorCode::PermissionDenied, msg),
AuthorizeTargetsError::Internal(_) => {
(CrpcErrorCode::Internal, "internal error".to_owned())
}
};
Err(CrpcError::new(code, msg))
}
}
}
#[cfg(test)]
mod tests {
use std::{
net::IpAddr,
sync::Mutex,
time::{Duration, UNIX_EPOCH},
};
use axum::{
body::Body,
http::{Request, Response, StatusCode, header},
};
use buffa::Message as _;
use tower::ServiceExt as _;
use super::*;
use crate::pg_wap::{
crpc::model::{AuthorizeTargetsRequest, AuthorizeTargetsResponse},
sni::CustomerDomain,
test_util::{client_ip, sni},
};
const WAP_ID: &str = "wap-1";
const DATA_PLANE_PORT: u16 = 8443;
const EXPIRY_SECS: u64 = 1_800;
struct MockHandler {
request: Mutex<Option<(IpAddr, AuthorizeTargetsRequest)>>,
failing: bool,
}
impl MockHandler {
fn new() -> Arc<Self> {
Arc::new(Self {
request: Mutex::new(None),
failing: false,
})
}
fn failing() -> Arc<Self> {
Arc::new(Self {
request: Mutex::new(None),
failing: true,
})
}
fn request(&self) -> (IpAddr, AuthorizeTargetsRequest) {
self.request
.lock()
.unwrap()
.take()
.expect("the handler was called")
}
}
impl ControlServiceAPIHandler for MockHandler {
fn authorize_targets(
&self,
client_ip: IpAddr,
request: AuthorizeTargetsRequest,
_now: SystemTime,
) -> Result<AuthorizeTargetsResponse, AuthorizeTargetsError> {
*self.request.lock().unwrap() = Some((client_ip, request));
if self.failing {
return Err(AuthorizeTargetsError::Internal(anyhow::anyhow!(
"set to fail"
)));
}
Ok(AuthorizeTargetsResponse {
client_ip,
wap_id: WAP_ID.to_owned(),
data_plane_port: DATA_PLANE_PORT,
expiry_time: UNIX_EPOCH + Duration::from_secs(EXPIRY_SECS),
})
}
}
fn client_addr() -> SocketAddr {
SocketAddr::new(client_ip(), 54321)
}
async fn call(
handler: Arc<MockHandler>,
request: rpc::AuthorizeTargetsRequest,
) -> Response<Body> {
let mut http_request = Request::builder()
.method("POST")
.uri(format!("{SERVICE_PATH}{AUTHORIZE_TARGETS}"))
.header(header::CONTENT_TYPE, "application/proto")
.body(Body::from(request.encode_to_vec()))
.expect("a valid request");
http_request
.extensions_mut()
.insert(ConnectInfo(client_addr()));
nest_crpc_api(Router::new(), handler)
.oneshot(http_request)
.await
.expect("the router is infallible")
}
async fn body_bytes(response: Response<Body>) -> Vec<u8> {
axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("the body can be collected")
.to_vec()
}
async fn call_expecting_error(
handler: Arc<MockHandler>,
request: rpc::AuthorizeTargetsRequest,
want_status: StatusCode,
) -> CrpcError {
let response = call(handler, request).await;
assert_eq!(response.status(), want_status);
serde_json::from_slice(&body_bytes(response).await).expect("a connect RPC error body")
}
fn request_for_sni() -> rpc::AuthorizeTargetsRequest {
rpc::AuthorizeTargetsRequest {
targets: [(
sni().customer_domain().as_str().to_owned(),
rpc::AuthSegments::default(),
)]
.into_iter()
.collect(),
}
}
#[tokio::test]
async fn a_grant_is_handed_out_for_the_address_the_request_came_from() {
let handler = MockHandler::new();
let response = call(handler.clone(), request_for_sni()).await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers().get(header::CONTENT_TYPE).unwrap(),
"application/proto"
);
let response =
rpc::AuthorizeTargetsResponse::decode_from_slice(&body_bytes(response).await)
.expect("a response message");
assert_eq!(
response,
rpc::AuthorizeTargetsResponse {
client_ip: client_ip().to_string(),
wap_id: WAP_ID.to_owned(),
data_plane_port: u32::from(DATA_PLANE_PORT),
expiry_time: EXPIRY_SECS,
}
);
let (authorized_ip, request) = handler.request();
assert_eq!(
authorized_ip,
client_ip(),
"the authorized address is the one the connection came from, not one the request \
could name"
);
assert_eq!(
request.targets.keys().collect::<Vec<_>>(),
vec![&CustomerDomain::from(sni().customer_domain())],
"the target is authorized under its customer domain"
);
}
#[tokio::test]
async fn a_request_that_cannot_be_converted_is_an_invalid_argument() {
let error = call_expecting_error(
MockHandler::new(),
rpc::AuthorizeTargetsRequest::default(),
StatusCode::BAD_REQUEST,
)
.await;
assert_eq!(error.code, CrpcErrorCode::InvalidArgument);
}
#[tokio::test]
async fn handler_passes_error_from_failing_handler() {
let error = call_expecting_error(
MockHandler::failing(),
request_for_sni(),
StatusCode::INTERNAL_SERVER_ERROR,
)
.await;
assert_eq!(error.code, CrpcErrorCode::Internal);
println!("error message: {}", error.message);
assert_eq!(
error.message, "internal error",
"the endpoint returns the message of the handler"
);
}
}