/* Copyright (C) 2018-2020 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 Victor Julien

use nom7::bytes::streaming::take;
use nom7::number::streaming::be_u32;
use nom7::{Err, IResult};

use crate::direction::Direction;
use crate::nfs::nfs::*;
use crate::flow::Flow;
use crate::nfs::nfs4_records::*;
use crate::nfs::nfs_records::*;
use crate::nfs::rpc_records::*;
use crate::nfs::types::*;

use crate::kerberos::{parse_kerberos5_request, Kerberos5Ticket, SecBlobError};

fn parse_req_gssapi(i: &[u8]) -> IResult<&[u8], Kerberos5Ticket, SecBlobError> {
    let (i, len) = be_u32(i)?;
    let (i, buf) = take(len as usize)(i)?;
    let (_, ap) = parse_kerberos5_request(buf)?;
    Ok((i, ap))
}

impl NFSState {
    /* normal write: PUTFH (file handle), WRITE (write opts/data). File handle
     * is not part of the write record itself so we pass it in here. */
    fn write_v4<'b>(&mut self, r: &RpcPacket<'b>, w: &Nfs4RequestWrite<'b>, fh: &'b [u8]) {
        // for now assume that stable FILE_SYNC flags means a single chunk
        let is_last = w.stable == 2;
        SCLogDebug!("is_last {}", is_last);

        let mut fill_bytes = 0;
        let pad = w.write_len % 4;
        if pad != 0 {
            fill_bytes = 4 - pad;
        }

        // linux defines a max of 1mb. Allow several multiples.
        if w.write_len == 0 || w.write_len > 16777216 {
            return;
        }

        let file_handle = fh.to_vec();
        let file_name = if let Some(name) = self.namemap.get(fh) {
            SCLogDebug!("WRITE name {:?}", name);
            name.to_vec()
        } else {
            SCLogDebug!("WRITE object {:?} not found", w.stateid.data);
            Vec::new()
        };

        let found = match self.get_file_tx_by_handle(&file_handle, Direction::ToServer) {
            Some(tx) => {
                if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
                    filetracker_newchunk(
                        &mut tdf.file_tracker,
                        &file_name,
                        w.data,
                        w.offset,
                        w.write_len,
                        fill_bytes as u8,
                        is_last,
                        &r.hdr.xid,
                    );
                    tdf.chunk_count += 1;
                    if is_last {
                        tdf.file_last_xid = r.hdr.xid;
                        tx.is_last = true;
                        tx.response_done = true;
                    }
                }
                true
            }
            None => false,
        };
        if !found {
            let tx = self.new_file_tx(&file_handle, &file_name, Direction::ToServer);
            if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
                filetracker_newchunk(
                    &mut tdf.file_tracker,
                    &file_name,
                    w.data,
                    w.offset,
                    w.write_len,
                    fill_bytes as u8,
                    is_last,
                    &r.hdr.xid,
                );
                tx.procedure = NFSPROC4_WRITE;
                tx.xid = r.hdr.xid;
                tx.is_first = true;
                tx.nfs_version = r.progver as u16;
                if is_last {
                    tdf.file_last_xid = r.hdr.xid;
                    tx.is_last = true;
                    tx.request_done = true;
                    tx.is_file_closed = true;
                }
            }
        }
        self.ts_chunk_xid = r.hdr.xid;
        debug_validate_bug_on!(w.data.len() as u32 > w.write_len);
        self.ts_chunk_left = w.write_len - w.data.len() as u32;
    }

    fn close_v4<'b>(&mut self, r: &RpcPacket<'b>, fh: &'b [u8]) {
        self.commit_v4(r, fh)
    }

    fn commit_v4<'b>(&mut self, r: &RpcPacket<'b>, fh: &'b [u8]) {
        SCLogDebug!("COMMIT, closing shop");

        let file_handle = fh.to_vec();
        if let Some(tx) = self.get_file_tx_by_handle(&file_handle, Direction::ToServer) {
            if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
                filetracker_close(&mut tdf.file_tracker);
                tdf.file_last_xid = r.hdr.xid;
                tx.is_last = true;
                tx.request_done = true;
                tx.is_file_closed = true;
            }
        }
    }

    fn new_tx_v4(
        &mut self, r: &RpcPacket, xidmap: &NFSRequestXidMap, procedure: u32, _aux_opcodes: &[u32],
    ) {
        let mut tx = self.new_tx();
        tx.xid = r.hdr.xid;
        tx.procedure = procedure;
        tx.request_done = true;
        tx.file_name = xidmap.file_name.to_vec();
        tx.nfs_version = r.progver as u16;
        tx.file_handle = xidmap.file_handle.to_vec();

        tx.auth_type = r.creds_flavor;
        #[allow(clippy::single_match)]
        match r.creds {
            RpcRequestCreds::Unix(ref u) => {
                tx.request_machine_name = u.machine_name_buf.to_vec();
                tx.request_uid = u.uid;
                tx.request_gid = u.gid;
            }
            _ => {}
        }
        SCLogDebug!(
            "NFSv4: TX created: ID {} XID {} PROCEDURE {}",
            tx.id,
            tx.xid,
            tx.procedure
        );
        self.transactions.push(tx);
    }

    /* A normal READ request looks like: PUTFH (file handle) READ (read opts).
     * We need the file handle for the READ.
     */
    fn compound_request<'b>(
        &mut self, r: &RpcPacket<'b>, cr: &Nfs4RequestCompoundRecord<'b>,
        xidmap: &mut NFSRequestXidMap,
    ) {
        let mut last_putfh: Option<&'b [u8]> = None;
        let mut main_opcode: u32 = 0;
        let mut aux_opcodes: Vec<u32> = Vec::new();

        for c in &cr.commands {
            SCLogDebug!("c {:?}", c);
            match *c {
                Nfs4RequestContent::PutFH(ref rd) => {
                    last_putfh = Some(rd.value);
                    aux_opcodes.push(NFSPROC4_PUTFH);
                }
                Nfs4RequestContent::Read(ref rd) => {
                    SCLogDebug!("READv4: {:?}", rd);
                    if let Some(fh) = last_putfh {
                        xidmap.chunk_offset = rd.offset;
                        xidmap.file_handle = fh.to_vec();
                        self.xidmap_handle2name(xidmap);
                    }
                }
                Nfs4RequestContent::Open(ref rd) => {
                    SCLogDebug!("OPENv4: {}", String::from_utf8_lossy(rd.filename));
                    xidmap.file_name = rd.filename.to_vec();
                }
                Nfs4RequestContent::Lookup(ref rd) => {
                    SCLogDebug!("LOOKUPv4: {}", String::from_utf8_lossy(rd.filename));
                    xidmap.file_name = rd.filename.to_vec();
                }
                Nfs4RequestContent::Write(ref rd) => {
                    SCLogDebug!("WRITEv4: {:?}", rd);
                    if let Some(fh) = last_putfh {
                        self.write_v4(r, rd, fh);
                    }
                }
                Nfs4RequestContent::Commit => {
                    SCLogDebug!("COMMITv4");
                    if let Some(fh) = last_putfh {
                        self.commit_v4(r, fh);
                    }
                }
                Nfs4RequestContent::Close(ref _rd) => {
                    SCLogDebug!("CLOSEv4: {:?}", _rd);
                    if let Some(fh) = last_putfh {
                        self.close_v4(r, fh);
                    }
                }
                Nfs4RequestContent::Create(ref rd) => {
                    SCLogDebug!("CREATEv4: {:?}", rd);
                    if let Some(fh) = last_putfh {
                        xidmap.file_handle = fh.to_vec();
                    }
                    xidmap.file_name = rd.filename.to_vec();
                    main_opcode = NFSPROC4_CREATE;
                }
                Nfs4RequestContent::Remove(rd) => {
                    SCLogDebug!("REMOVEv4: {:?}", rd);
                    xidmap.file_name = rd.to_vec();
                    main_opcode = NFSPROC4_REMOVE;
                }
                Nfs4RequestContent::SetClientId(ref _rd) => {
                    SCLogDebug!(
                        "SETCLIENTIDv4: client id {} r_netid {} r_addr {}",
                        String::from_utf8_lossy(_rd.client_id),
                        String::from_utf8_lossy(_rd.r_netid),
                        String::from_utf8_lossy(_rd.r_addr)
                    );
                }
                _ => {}
            }
        }

        if main_opcode != 0 {
            self.new_tx_v4(r, xidmap, main_opcode, &aux_opcodes);
        }
    }

    /// complete request record
    pub fn process_request_record_v4(&mut self, r: &RpcPacket) {
        SCLogDebug!(
            "NFSv4 REQUEST {} procedure {} ({}) blob size {}",
            r.hdr.xid,
            r.procedure,
            self.requestmap.len(),
            r.prog_data.len()
        );

        let mut xidmap = NFSRequestXidMap::new(r.progver, r.procedure, 0);

        if r.procedure == NFSPROC4_NULL {
            if let RpcRequestCreds::GssApi(ref creds) = r.creds {
                if creds.procedure == 1 {
                    let _x = parse_req_gssapi(r.prog_data);
                    SCLogDebug!("RPCSEC_GSS_INIT {:?}", _x);
                }
            }
        } else if r.procedure == NFSPROC4_COMPOUND {
            let mut data = r.prog_data;

            if let RpcRequestCreds::GssApi(ref creds) = r.creds {
                if creds.procedure == 0 && creds.service == 2 {
                    SCLogDebug!("GSS INTEGRITY: {:?}", creds);
                    match parse_rpc_gssapi_integrity(r.prog_data) {
                        Ok((_rem, rec)) => {
                            SCLogDebug!("GSS INTEGRITY wrapper: {:?}", rec);
                            data = rec.data;
                            // store proc and serv for the reply
                            xidmap.gssapi_proc = creds.procedure;
                            xidmap.gssapi_service = creds.service;
                        }
                        Err(Err::Incomplete(_n)) => {
                            SCLogDebug!("NFSPROC4_COMPOUND/GSS INTEGRITY: INCOMPLETE {:?}", _n);
                            self.set_event(NFSEvent::MalformedData);
                            return;
                        }
                        Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
                            SCLogDebug!(
                                "NFSPROC4_COMPOUND/GSS INTEGRITY: Parsing failed: {:?}",
                                _e
                            );
                            self.set_event(NFSEvent::MalformedData);
                            return;
                        }
                    }
                }
            }

            match parse_nfs4_request_compound(data) {
                Ok((_, rd)) => {
                    SCLogDebug!("NFSPROC4_COMPOUND: {:?}", rd);
                    self.compound_request(r, &rd, &mut xidmap);
                }
                Err(Err::Incomplete(_n)) => {
                    SCLogDebug!("NFSPROC4_COMPOUND: INCOMPLETE {:?}", _n);
                    self.set_event(NFSEvent::MalformedData);
                }
                Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
                    SCLogDebug!("NFSPROC4_COMPOUND: Parsing failed: {:?}", _e);
                    self.set_event(NFSEvent::MalformedData);
                }
            };
        }

        self.requestmap.put(r.hdr.xid, xidmap);
    }

    fn compound_response<'b>(
        &mut self, flow: *mut Flow, r: &RpcReplyPacket<'b>, cr: &Nfs4ResponseCompoundRecord<'b>,
        xidmap: &mut NFSRequestXidMap,
    ) {
        let mut insert_filename_with_getfh = false;
        let mut main_opcode_status: u32 = 0;
        let mut main_opcode_status_set: bool = false;

        for c in &cr.commands {
            SCLogDebug!("c {:?}", c);
            match *c {
                Nfs4ResponseContent::ReadDir(_s, Some(ref rd)) => {
                    SCLogDebug!("READDIRv4: status {} eof {}", _s, rd.eof);

                    #[allow(clippy::manual_flatten)]
                    for d in &rd.listing {
                        if let Some(_d) = d {
                            SCLogDebug!("READDIRv4: dir {}", String::from_utf8_lossy(_d.name));
                        }
                    }
                }
                Nfs4ResponseContent::Remove(s) => {
                    SCLogDebug!("REMOVE4: status {}", s);
                    main_opcode_status = s;
                    main_opcode_status_set = true;
                }
                Nfs4ResponseContent::Create(s) => {
                    SCLogDebug!("CREATE4: status {}", s);
                    main_opcode_status = s;
                    main_opcode_status_set = true;
                }
                Nfs4ResponseContent::Read(s, Some(ref rd)) => {
                    SCLogDebug!(
                        "READ4: xidmap {:?} status {} data {}",
                        xidmap,
                        s,
                        rd.data.len()
                    );
                    // convert record to generic read reply
                    let reply = NfsReplyRead {
                        status: s,
                        attr_follows: 0,
                        attr_blob: &[],
                        count: rd.count,
                        eof: rd.eof,
                        data_len: rd.data.len() as u32,
                        data: rd.data,
                    };
                    self.process_read_record(flow, r, &reply, Some(xidmap));
                }
                Nfs4ResponseContent::Open(_s, Some(ref _rd)) => {
                    SCLogDebug!("OPENv4: status {} opendata {:?}", _s, _rd);
                    insert_filename_with_getfh = true;
                }
                Nfs4ResponseContent::GetFH(_s, Some(ref rd)) => {
                    if insert_filename_with_getfh {
                        self.namemap
                            .put(rd.value.to_vec(), xidmap.file_name.to_vec());
                    }
                }
                Nfs4ResponseContent::PutRootFH(s) => {
                    if s == NFS4_OK && xidmap.file_name.is_empty() {
                        xidmap.file_name = b"<mount_root>".to_vec();
                        SCLogDebug!("filename {:?}", xidmap.file_name);
                    }
                }
                _ => {}
            }
        }

        if main_opcode_status_set {
            let resp_handle = Vec::new();
            self.mark_response_tx_done(flow, r.hdr.xid, r.reply_state, main_opcode_status, &resp_handle);
        }
    }

    pub fn process_reply_record_v4(&mut self, flow: *mut Flow, r: &RpcReplyPacket, xidmap: &mut NFSRequestXidMap) {
        if xidmap.procedure == NFSPROC4_COMPOUND {
            let mut data = r.prog_data;

            if xidmap.gssapi_proc == 0 && xidmap.gssapi_service == 2 {
                SCLogDebug!("GSS INTEGRITY as set by call: {:?}", xidmap);
                match parse_rpc_gssapi_integrity(r.prog_data) {
                    Ok((_rem, rec)) => {
                        SCLogDebug!("GSS INTEGRITY wrapper: {:?}", rec);
                        data = rec.data;
                    }
                    Err(Err::Incomplete(_n)) => {
                        SCLogDebug!("NFSPROC4_COMPOUND/GSS INTEGRITY: INCOMPLETE {:?}", _n);
                        self.set_event(NFSEvent::MalformedData);
                        return;
                    }
                    Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
                        SCLogDebug!("NFSPROC4_COMPOUND/GSS INTEGRITY: Parsing failed: {:?}", _e);
                        self.set_event(NFSEvent::MalformedData);
                        return;
                    }
                }
            }
            match parse_nfs4_response_compound(data) {
                Ok((_, rd)) => {
                    SCLogDebug!("COMPOUNDv4: {:?}", rd);
                    self.compound_response(flow, r, &rd, xidmap);
                }
                Err(Err::Incomplete(_)) => {
                    self.set_event(NFSEvent::MalformedData);
                }
                Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
                    SCLogDebug!("Parsing failed: {:?}", _e);
                    self.set_event(NFSEvent::MalformedData);
                }
            };
        }
    }
}
