coraza 0.1.0

Safe Rust bindings to OWASP Coraza WAF
/*
 * Copyright 2022 OWASP Coraza contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Callback trampolines for FFI.
//!
//! This module provides the infrastructure for passing Rust closures as C
//! function pointers to the Coraza FFI. The pattern is:
//!
//! 1. The user registers a closure via `WafConfig::with_debug_log_callback` or
//!    `WafConfig::with_error_callback`.
//! 2. The closure is boxed and stored in a `CallbackContext`.
//! 3. A C-compatible trampoline function is passed to the FFI, with a raw
//!    pointer to the context as the user data.
//! 4. When Coraza calls the trampoline, it casts the pointer back and invokes
//!    the Rust closure.

use std::ffi::CStr;
use std::os::raw::{c_char, c_void};

use coraza_sys::*;

use crate::matched_rule::{LogLevel, MatchedRule, Severity};

/// Context for storing Rust closures that are passed to Coraza as C callbacks.
///
/// This is leaked during WAF creation and freed afterwards. The closures must
/// not outlive the WAF configuration phase.
pub(crate) struct CallbackContext {
    #[allow(clippy::type_complexity)]
    pub debug_log: Option<Box<dyn Fn(LogLevel, &str, &str) + Send>>,
    pub error: Option<Box<dyn Fn(MatchedRule) + Send>>,
}

impl CallbackContext {
    pub fn new() -> Self {
        Self {
            debug_log: None,
            error: None,
        }
    }
}

/// C-compatible trampoline for the debug log callback.
///
/// # Safety
///
/// `ctx` must be a valid pointer to a `CallbackContext` that was leaked via
/// `Box::into_raw` and has not yet been freed.
pub(crate) unsafe extern "C" fn debug_log_trampoline(
    ctx: *mut c_void,
    level: coraza_debug_log_level_t,
    msg: *const c_char,
    fields: *const c_char,
) {
    if ctx.is_null() {
        return;
    }
    let ctx = unsafe { &*(ctx as *const CallbackContext) };
    if let Some(ref cb) = ctx.debug_log {
        let msg_str;
        let msg = if msg.is_null() {
            ""
        } else {
            msg_str = unsafe { CStr::from_ptr(msg) }.to_string_lossy();
            &msg_str
        };
        let fields_str;
        let fields = if fields.is_null() {
            ""
        } else {
            fields_str = unsafe { CStr::from_ptr(fields) }.to_string_lossy();
            &fields_str
        };
        cb(LogLevel::from(level), msg, fields);
    }
}

/// C-compatible trampoline for the error callback.
///
/// # Safety
///
/// `ctx` must be a valid pointer to a `CallbackContext` that was leaked via
/// `Box::into_raw` and has not yet been freed. `rule` must be a valid
/// `coraza_matched_rule_t` handle.
pub(crate) unsafe extern "C" fn error_trampoline(ctx: *mut c_void, rule: coraza_matched_rule_t) {
    if ctx.is_null() {
        return;
    }
    let ctx = unsafe { &mut *(ctx as *mut CallbackContext) };
    if let Some(ref cb) = ctx.error {
        let matched = extract_matched_rule(rule);
        cb(matched);
    }
}

/// Extract a `MatchedRule` from an FFI handle.
///
/// # Safety
///
/// `rule` must be a valid `coraza_matched_rule_t` handle obtained from Coraza.
unsafe fn extract_matched_rule(rule: coraza_matched_rule_t) -> MatchedRule {
    let severity = unsafe { coraza_matched_rule_get_severity(rule) };
    let rule_id = unsafe { coraza_matched_rule_get_id(rule) };

    let error_log = unsafe { coraza_matched_rule_get_error_log(rule) };
    let message = if error_log.is_null() {
        String::new()
    } else {
        let s = unsafe { CStr::from_ptr(error_log) }
            .to_string_lossy()
            .into_owned();
        unsafe { coraza_free_string(error_log) };
        s
    };

    MatchedRule {
        message,
        severity: Severity::from(severity),
        rule_id,
        uri: String::new(),       // Not exposed via C API
        client_ip: String::new(), // Not exposed via C API
        server_ip: String::new(), // Not exposed via C API
        disruptive: rule_id != 0, // Approximation
    }
}