use waf_core::{
CompressedPolicy, Config, Decision, GrpcConfig, ParsedBody, Phase, RequestContext, WafModule,
};
use waf_normalizer::grpc::{grpc_extract, GrpcLimits};
#[derive(Default)]
pub struct GrpcModule {
cfg: GrpcConfig,
}
impl GrpcModule {
pub fn new() -> Self {
Self::default()
}
}
fn content_type(ctx: &RequestContext) -> &str {
ctx.normalized
.headers
.iter()
.find(|(k, _)| k == "content-type")
.map(|(_, v)| v.as_str())
.unwrap_or("")
}
fn header_compressed(ctx: &RequestContext) -> bool {
ctx.normalized
.headers
.iter()
.find(|(k, _)| k == "grpc-encoding")
.map(|(_, v)| !v.trim().eq_ignore_ascii_case("identity"))
.unwrap_or(false)
}
fn reject(reason: &'static str) -> Decision {
Decision::Reject {
rule_id: "grpc".to_string(),
reason: reason.to_string(),
status: 400,
retry_after: None,
}
}
impl WafModule for GrpcModule {
fn id(&self) -> &str {
"grpc"
}
fn phase(&self) -> Phase {
Phase::Body
}
fn structural(&self) -> bool {
true
}
fn init(&mut self, cfg: &Config) {
self.cfg = cfg.modules.grpc.clone();
}
fn inspect(&self, ctx: &RequestContext) -> Decision {
if !self.cfg.enabled {
return Decision::Allow;
}
if !content_type(ctx).trim_start().starts_with("application/grpc") {
return Decision::Allow;
}
let ParsedBody::Raw(body) = &ctx.normalized.body else {
return Decision::Allow;
};
let limits = GrpcLimits {
max_depth: self.cfg.max_depth,
max_fields: self.cfg.max_fields,
max_leaves: 0,
};
let ex = grpc_extract(body, limits);
if ex.malformed {
return reject("malformed gRPC framing");
}
if ex.total_payload_bytes > self.cfg.max_message_bytes {
return reject("gRPC message size exceeds limit");
}
if ex.compressed || header_compressed(ctx) {
return match self.cfg.on_compressed {
CompressedPolicy::Reject => reject("gRPC compressed payload is not inspectable"),
CompressedPolicy::Passthrough => Decision::Allow,
};
}
if ex.depth_exceeded {
return reject("gRPC message nesting depth exceeds limit");
}
if ex.fields_exceeded {
return reject("gRPC field count exceeds limit");
}
Decision::Allow
}
}