Skip to content
Clone
ec0bdb8
116 lines 3.6 KB Rust
use std::sync::Arc;
use tonic::metadata::MetadataMap;
use tonic::service::Interceptor as TonicInterceptor;
use tonic::{Request, Status};
use uuid::Uuid;
pub const RPC_KEY_HEADER: &str = "x-rpc-key";
pub const REQUEST_ID_HEADER: &str = "x-request-id";
pub const LEGACY_REQUEST_ID_HEADER: &str = "gitdata-request-id";
pub const CALLER_SERVICE_HEADER: &str = "gitdata-service-id";
pub const MAX_REQUEST_ID_LEN: usize = 64;
/// 门禁通过后挂到请求扩展上的元信息;只用于日志与审计。
#[derive(Debug, Clone, Default)]
pub struct RequestMeta {
pub request_id: String,
pub caller_service: String,
}
impl RequestMeta {
pub fn request_id(&self) -> Option<&str> {
(!self.request_id.is_empty()).then_some(self.request_id.as_str())
}
}
pub fn validate_request_id(value: &str) -> Option<String> {
let trimmed = value.trim();
if trimmed.is_empty() || trimmed.len() > MAX_REQUEST_ID_LEN {
return None;
}
trimmed
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':'))
.then(|| trimmed.to_owned())
}
pub fn request_id_from(metadata: &MetadataMap) -> Option<String> {
metadata
.get(REQUEST_ID_HEADER)
.or_else(|| metadata.get(LEGACY_REQUEST_ID_HEADER))
.and_then(|value| value.to_str().ok())
.and_then(validate_request_id)
}
pub fn new_request_id() -> String {
Uuid::now_v7().to_string()
}
/// 共享密钥门禁:校验 `x-rpc-key`,通过后把 `RequestMeta` 挂到请求扩展。
#[derive(Clone)]
pub struct Gate {
key: Arc<str>,
}
impl Gate {
pub fn new(key: impl Into<Arc<str>>) -> Self {
Self { key: key.into() }
}
/// 空密钥会让所有请求通过,所以装配后必须先确认门禁真的能拒绝。
pub fn is_armed(&self) -> bool {
!self.key.trim().is_empty()
}
}
impl TonicInterceptor for Gate {
fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
let metadata = request.metadata().clone();
let request_id = request_id_from(&metadata);
if !self.is_armed() {
return Err(unauthorized(
"rpc gate is not configured",
request_id.as_deref(),
));
}
let presented = metadata
.get(RPC_KEY_HEADER)
.and_then(|value| value.to_str().ok())
.map(str::trim)
.unwrap_or_default();
if presented.is_empty()
|| !crypt::constant_time_eq(presented.as_bytes(), self.key.as_bytes())
{
return Err(unauthorized(
"rpc key is missing or invalid",
request_id.as_deref(),
));
}
let meta = RequestMeta {
request_id: request_id.unwrap_or_else(new_request_id),
caller_service: metadata
.get(CALLER_SERVICE_HEADER)
.and_then(|value| value.to_str().ok())
.map(|value| value.trim().to_owned())
.filter(|value| !value.is_empty())
.unwrap_or_default(),
};
request.extensions_mut().insert(meta);
Ok(request)
}
}
fn unauthorized(message: &str, request_id: Option<&str>) -> Status {
let _ = request_id;
Status::unauthenticated(message)
}
/// 读取门禁挂上的元信息;拦截器已校验密钥,这里只做扩展读取。
pub fn meta<T>(request: &Request<T>) -> Result<RequestMeta, Status> {
request
.extensions()
.get::<RequestMeta>()
.cloned()
.ok_or_else(|| Status::unauthenticated("rpc gate did not attach request metadata"))
}