ntex
Add Nyoxis threat detection to an ntex application using a service middleware transform and reqwest for async HTTP calls.
Prerequisites
- Rust 1.75 or later
- An ntex application (0.7+)
- A Nyoxis workspace API key — get one here
Add dependencies
toml
# Cargo.toml
[dependencies]
ntex = { version = "0.7", features = ["tokio"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }Middleware
Create src/middleware/nyoxis.rs:
rust
use std::{future::Future, pin::Pin, rc::Rc};
use ntex::{
service::{Middleware, Service, ServiceCtx},
web::{HttpRequest, HttpResponse, WebRequest, WebResponse},
};
use reqwest::Client;
use serde_json::{json, Value};
/// Nyoxis WAF middleware — calls /v0/predict for every request.
pub struct NyoxisWAF {
api_key: Rc<String>,
block_on_high: bool,
}
impl NyoxisWAF {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
api_key: Rc::new(api_key.into()),
block_on_high: false,
}
}
pub fn block_on_high(mut self) -> Self {
self.block_on_high = true;
self
}
}
impl<S> Middleware<S> for NyoxisWAF {
type Service = NyoxisWAFService<S>;
fn create(&self, service: S) -> Self::Service {
NyoxisWAFService {
service,
api_key: self.api_key.clone(),
block_on_high: self.block_on_high,
client: Client::new(),
}
}
}
pub struct NyoxisWAFService<S> {
service: S,
api_key: Rc<String>,
block_on_high: bool,
client: Client,
}
impl<S, E> Service<WebRequest<E>> for NyoxisWAFService<S>
where
S: Service<WebRequest<E>, Response = WebResponse>,
E: ntex::web::ErrorRenderer,
{
type Response = WebResponse;
type Error = S::Error;
type Future<'f> = Pin<Box<dyn Future<Output = Result<WebResponse, S::Error>> + 'f>>
where
Self: 'f;
ntex::forward_poll_ready!(service);
ntex::forward_poll_shutdown!(service);
fn call<'a>(&'a self, req: WebRequest<E>, ctx: ServiceCtx<'a, Self>) -> Self::Future<'a> {
Box::pin(async move {
let ip = req
.connection_info()
.realip_remote_addr()
.map(str::to_string);
let payload = json!({
"method": req.method().as_str(),
"path": req.path(),
"query": req.query_string().is_empty()
.then(|| Option::<String>::None)
.unwrap_or_else(|| Some(req.query_string().to_string())),
"ip_addr": ip,
});
let url = format!(
"https://api.nyoxis.com/v0/predict?api_key={}",
self.api_key
);
match self
.client
.post(&url)
.json(&payload)
.timeout(std::time::Duration::from_secs(3))
.send()
.await
{
Ok(resp) => {
if let Ok(verdict) = resp.json::<Value>().await {
if self.block_on_high {
let risk = verdict
.pointer("/prediction/risk")
.and_then(Value::as_str)
.unwrap_or("none");
if risk == "high" {
let http_res = HttpResponse::Forbidden()
.json(&json!({ "error": "Forbidden" }));
return Ok(req.into_response(http_res));
}
}
// Store verdict in request extensions for handlers
req.extensions_mut().insert(verdict);
}
}
Err(e) => {
// Fail open — log and continue
eprintln!("[nyoxis] prediction error: {e}");
}
}
ctx.call(&self.service, req).await
})
}
}Register the middleware
rust
// src/main.rs
mod middleware;
use middleware::nyoxis::NyoxisWAF;
use ntex::web;
use std::env;
#[ntex::main]
async fn main() -> std::io::Result<()> {
let api_key = env::var("NYOXIS_API_KEY").expect("NYOXIS_API_KEY not set");
web::HttpServer::new(move || {
web::App::new()
.wrap(NyoxisWAF::new(api_key.clone()).block_on_high())
.route("/", web::get().to(index))
})
.bind("0.0.0.0:8080")?
.run()
.await
}
async fn index(req: web::HttpRequest) -> web::HttpResponse {
// Read verdict from request extensions
let risk = req
.extensions()
.get::<serde_json::Value>()
.and_then(|v| v.pointer("/prediction/risk"))
.and_then(serde_json::Value::as_str)
.unwrap_or("none")
.to_string();
web::HttpResponse::Ok().json(&serde_json::json!({ "ok": true, "risk": risk }))
}Next steps
- API Reference — complete field descriptions and status codes.
- Overview — how the classifier and redaction pipeline work.