/* Copyright (C) 2024 Open Information Security Foundation
 *
 * You can copy, redistribute or modify this Program under the terms of
 * the GNU General Public License version 2 as published by the Free
 * Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * version 2 along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

// written by Giuseppe Longo <giuseppe@glongo.it>

use crate::detect::EnumString;
use crate::jsonbuilder::{JsonBuilder, JsonError};
use crate::ldap::ldap::LdapTransaction;
use crate::ldap::types::*;
use ldap_parser::filter::*;
use ldap_parser::ldap::*;

fn log_ldap(tx: &LdapTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("ldap")?;

    if let Some(req) = &tx.request {
        let protocol_op_str = ldap_protocol_op_as_str(&req.protocol_op);
        js.open_object("request")?;
        js.set_uint("message_id", req.message_id.0)?;
        js.set_string("operation", protocol_op_str)?;

        match &req.protocol_op {
            ProtocolOp::SearchRequest(msg) => log_search_request(msg, js)?,
            ProtocolOp::BindRequest(msg) => log_bind_request(msg, js)?,
            ProtocolOp::UnbindRequest => (),
            ProtocolOp::ModifyRequest(msg) => log_modify_request(msg, js)?,
            ProtocolOp::AddRequest(msg) => log_add_request(msg, js)?,
            ProtocolOp::DelRequest(msg) => log_del_request(msg, js)?,
            ProtocolOp::ModDnRequest(msg) => log_mod_dn_request(msg, js)?,
            ProtocolOp::CompareRequest(msg) => log_compare_request(msg, js)?,
            ProtocolOp::ExtendedRequest(msg) => log_extended_request(msg, js)?,
            _ => {}
        };

        log_controls(&req.controls, js)?;

        js.close()?;
    }

    if !tx.responses.is_empty() {
        js.open_array("responses")?;

        for response in &tx.responses {
            js.start_object()?;

            let protocol_op_str = ldap_protocol_op_as_str(&response.protocol_op);
            js.set_string("operation", protocol_op_str)?;

            if tx.request.is_none() {
                js.set_uint("message_id", response.message_id.0)?;
            }

            match &response.protocol_op {
                ProtocolOp::SearchResultEntry(msg) => log_search_result_entry(msg, js)?,
                ProtocolOp::SearchResultDone(msg) => log_search_result_done(msg, js)?,
                ProtocolOp::BindResponse(msg) => log_bind_response(msg, js)?,
                ProtocolOp::ModifyResponse(msg) => log_modify_response(msg, js)?,
                ProtocolOp::AddResponse(msg) => log_add_response(msg, js)?,
                ProtocolOp::DelResponse(msg) => log_del_response(msg, js)?,
                ProtocolOp::ModDnResponse(msg) => log_mod_dn_response(msg, js)?,
                ProtocolOp::CompareResponse(msg) => log_compare_response(msg, js)?,
                ProtocolOp::ExtendedResponse(msg) => log_extended_response(msg, js)?,
                ProtocolOp::IntermediateResponse(msg) => log_intermediate_response(msg, js)?,
                _ => {}
            }
            log_controls(&response.controls, js)?;
            js.close()?;
        }
        js.close()?;
    }

    js.close()?;
    Ok(())
}

fn log_search_request(msg: &SearchRequest, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("search_request")?;
    js.set_string("base_object", &msg.base_object.0)?;
    js.set_uint("scope", msg.scope.0)?;
    js.set_uint("deref_alias", msg.deref_aliases.0)?;
    js.set_uint("size_limit", msg.size_limit)?;
    js.set_uint("time_limit", msg.time_limit)?;
    js.set_bool("types_only", msg.types_only)?;
    if let Filter::Present(val) = &msg.filter {
        js.open_object("filter")?;
        js.set_string("type", "present")?;
        js.set_string("value", &val.0)?;
        js.close()?;
    }
    if !msg.attributes.is_empty() {
        js.open_array("attributes")?;
        for attr in &msg.attributes {
            js.append_string(&attr.0)?;
        }
        js.close()?;
    }

    js.close()?;
    Ok(())
}

fn log_bind_request(msg: &BindRequest, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("bind_request")?;
    js.set_uint("version", msg.version)?;
    js.set_string("name", &msg.name.0)?;
    if let AuthenticationChoice::Sasl(sasl) = &msg.authentication {
        js.open_object("sasl")?;
        js.set_string("mechanism", &sasl.mechanism.0)?;
        if let Some(credentials) = &sasl.credentials {
            js.set_hex("credentials", credentials)?;
        }
        js.close()?;
    }
    js.close()?;
    Ok(())
}

fn log_modify_request(msg: &ModifyRequest, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("modify_request")?;
    js.set_string("object", &msg.object.0)?;
    if !msg.changes.is_empty() {
        js.open_array("changes")?;
        for change in &msg.changes {
            js.start_object()?;
            js.set_string(
                "operation",
                ldap_operation_to_string(&change.operation).as_str(),
            )?;
            js.open_object("modification")?;
            js.set_string("attribute_type", &change.modification.attr_type.0)?;
            if !change.modification.attr_vals.is_empty() {
                js.open_array("attribute_values")?;
                for attr in &change.modification.attr_vals {
                    js.append_string_from_bytes(&attr.0[..])?;
                }
                js.close()?;
            }
            js.close()?;
            js.close()?;
        }
        js.close()?;
    }
    js.close()?;
    Ok(())
}

fn log_add_request(msg: &AddRequest, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("add_request")?;
    js.set_string("entry", &msg.entry.0)?;
    if !msg.attributes.is_empty() {
        js.open_array("attributes")?;
        for attr in &msg.attributes {
            js.start_object()?;
            js.set_string("name", &attr.attr_type.0)?;
            if !attr.attr_vals.is_empty() {
                js.open_array("values")?;
                for val in &attr.attr_vals {
                    js.append_string_from_bytes(&val.0)?;
                }
                js.close()?;
            }
            js.close()?;
        }
        js.close()?;
    }
    js.close()?;
    Ok(())
}

fn log_del_request(msg: &LdapDN, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("del_request")?;
    js.set_string("dn", &msg.0)?;
    js.close()?;
    Ok(())
}

fn log_mod_dn_request(msg: &ModDnRequest, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("mod_dn_request")?;
    js.set_string("entry", &msg.entry.0)?;
    js.set_string("new_rdn", &msg.newrdn.0)?;
    js.set_bool("delete_old_rdn", msg.deleteoldrdn)?;
    if let Some(newsuperior) = &msg.newsuperior {
        js.set_string("new_superior", &newsuperior.0)?;
    }
    js.close()?;
    Ok(())
}

fn log_compare_request(msg: &CompareRequest, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("compare_request")?;
    js.set_string("entry", &msg.entry.0)?;
    js.open_object("attribute_value_assertion")?;
    js.set_string("description", &msg.ava.attribute_desc.0)?;
    js.set_string_from_bytes("value", &msg.ava.assertion_value[..])?;
    js.close()?;
    js.close()?;
    Ok(())
}

fn log_extended_request(msg: &ExtendedRequest, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("extended_request")?;
    js.set_string("name", &msg.request_name.0)?;
    if let Some(value) = &msg.request_value {
        js.set_string_from_bytes("value", &value[..])?;
    }
    js.close()?;
    Ok(())
}

fn log_search_result_entry(msg: &SearchResultEntry, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("search_result_entry")?;
    js.set_string("base_object", &msg.object_name.0)?;
    if !msg.attributes.is_empty() {
        js.open_array("attributes")?;
        for attr in &msg.attributes {
            js.start_object()?;
            js.set_string("type", &attr.attr_type.0)?;
            if !attr.attr_vals.is_empty() {
                js.open_array("values")?;
                for val in &attr.attr_vals {
                    js.append_string_from_bytes(&val.0)?;
                }
                js.close()?;
            }
            js.close()?;
        }
        js.close()?;
    }
    js.close()?;
    Ok(())
}

fn log_search_result_done(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("search_result_done")?;
    log_ldap_result(msg, js)?;
    js.close()?;
    Ok(())
}

fn log_bind_response(msg: &BindResponse, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("bind_response")?;
    log_ldap_result(&msg.result, js)?;
    if let Some(creds) = &msg.server_sasl_creds {
        js.set_hex("server_sasl_creds", creds)?;
    };
    js.close()?;
    Ok(())
}

fn log_modify_response(msg: &ModifyResponse, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("modify_response")?;
    log_ldap_result(&msg.result, js)?;
    js.close()?;
    Ok(())
}

fn log_add_response(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("add_response")?;
    log_ldap_result(msg, js)?;
    js.close()?;
    Ok(())
}

fn log_del_response(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("del_response")?;
    log_ldap_result(msg, js)?;
    js.close()?;
    Ok(())
}

fn log_mod_dn_response(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("mod_dn_response")?;
    log_ldap_result(msg, js)?;
    js.close()?;
    Ok(())
}

fn log_compare_response(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("compare_response")?;
    log_ldap_result(msg, js)?;
    js.close()?;
    Ok(())
}

fn log_extended_response(msg: &ExtendedResponse, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("extended_response")?;
    log_ldap_result(&msg.result, js)?;
    if let Some(name) = &msg.response_name {
        js.set_string("name", &name.0)?;
    }
    if let Some(value) = &msg.response_value {
        js.set_string_from_bytes("value", &value[..])?;
    }
    js.close()?;
    Ok(())
}

fn log_intermediate_response(
    msg: &IntermediateResponse, js: &mut JsonBuilder,
) -> Result<(), JsonError> {
    js.open_object("intermediate_response")?;
    if let Some(name) = &msg.response_name {
        js.set_string("name", &name.0)?;
    }
    if let Some(value) = &msg.response_value {
        js.set_string_from_bytes("value", &value[..])?;
    }
    js.close()?;
    Ok(())
}

fn log_ldap_result(msg: &LdapResult, js: &mut JsonBuilder) -> Result<(), JsonError> {
    if let Some(rc) = LdapResultCode::from_u(msg.result_code.0) {
        js.set_string("result_code", rc.to_str())?;
    } else {
        js.set_string("result_code", &format!("unknown-{}", msg.result_code.0))?;
    }
    js.set_string("matched_dn", &msg.matched_dn.0)?;
    js.set_string("message", &msg.diagnostic_message.0)?;
    Ok(())
}

fn log_controls(controls: &Option<Vec<Control>>, js: &mut JsonBuilder) -> Result<(), JsonError> {
    if let Some(ctls) = controls {
        js.open_array("controls")?;
        for ctl in ctls {
            js.start_object()?;
            js.set_string("control_type", &ctl.control_type.0)?;
            js.set_bool("criticality", ctl.criticality)?;
            if let Some(ctl_val) = &ctl.control_value {
                js.set_hex("control_value", ctl_val)?;
            };
            js.close()?;
        }
        js.close()?;
    }
    Ok(())
}

#[no_mangle]
pub unsafe extern "C" fn SCLdapLoggerLog(
    tx: *mut std::os::raw::c_void, js: &mut JsonBuilder,
) -> bool {
    let tx = cast_pointer!(tx, LdapTransaction);
    log_ldap(tx, js).is_ok()
}
