//--------------------------------------------------------------------------
// Copyright (C) 2017-2025 Cisco and/or its affiliates. All rights reserved.
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License Version 2 as published
// by the Free Software Foundation.  You may not use, modify or distribute
// this program under any other version of the GNU General Public License.
//
// 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 along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//--------------------------------------------------------------------------
// control.cc author Bhagya Tholpady <bbantwal@cisco.com>
//            author Michael Altizer <mialtize@cisco.com>

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "control.h"

#include "log/messages.h"
#include "main.h"
#include "main/shell.h"
#include "main/thread.h"
#include "managers/module_manager.h"
#include "utils/util.h"

#include "control_mgmt.h"

using namespace snort;

std::vector<std::string> ControlConn::log_exclusion_list;
unsigned ControlConn::pending_cmds_count = 0;

ControlConn* ControlConn::query_from_lua(const lua_State* L)
{
#ifdef SHELL
    return ( L ? ControlMgmt::find_control(L) : nullptr );
#else
    UNUSED(L);
    return nullptr;
#endif
}

//------------------------------------------------------------------------
// control channel class
// -----------------------------------------------------------------------

ControlConn::ControlConn(int fd, bool local) : fd(fd), local(local)
{
    shell = new Shell;
    configure();
    if (show_prompt())
        touch();
}

ControlConn::~ControlConn()
{
    shutdown();
    delete shell;
}

void ControlConn::shutdown()
{
    if (closed)
        return;
    if (!local)
        close(fd);
    closed = true;
}

void ControlConn::configure() const
{
    ModuleManager::load_commands(shell);
}

void ControlConn::log_command(const std::string& command, bool log)
{
    if (command.empty())
        return;

    auto it = std::find(log_exclusion_list.begin(), log_exclusion_list.end(), command);
    if (log)
    {
        if (it != log_exclusion_list.end())
            log_exclusion_list.erase(it);
    }
    else
    {
        if (it == log_exclusion_list.end())
            log_exclusion_list.push_back(command);
    }
}

bool ControlConn::loggable(const std::string& command)
{
    if (log_exclusion_list.empty() or command.empty())
        return true;

    for (const auto& m : log_exclusion_list)
    {
        if (command.find(m) != std::string::npos)
            return false;
    }

    return true;
}

int ControlConn::read_commands()
{
    char buf[STD_BUF];
    int commands_found = 0;
    ssize_t n = 0;

    while ((n = read(fd, buf, sizeof(buf) - 1)) > 0)
    {
        buf[n] = '\0';
        char* p = buf;
        char* nl;
        while ((nl = strchr(p, '\n')) != nullptr)
        {
            next_command.append(buf, nl - p);
            if (loggable(next_command))
                LogMessage("Control: received command, %s\n", next_command.c_str());
            pending_commands.push(std::move(next_command));
            next_command.clear();
            p = nl + 1;
            commands_found++;
        }

        if (*p != '\0')
            next_command.append(p);
        else if (local)
        {
            // For stdin, we are only guaranteed to have some amount of input ending in a
            // newline and future read() calls will block.  To avoid blocking, assume that
            // we're done reading if the input ended with a newline.
            break;
        }
    }

    if (n < 0 && errno != EAGAIN && errno != EINTR)
    {
        ErrorMessage("Error reading from control descriptor: %s\n", get_error(errno));
        return -1;
    }

    if (n == 0 && commands_found == 0)
        return -1;

    touch();

    return commands_found;
}

void ControlConn::set_user_network_policy()
{
    shell->set_user_network_policy();
}

int ControlConn::execute_commands()
{
    int executed = 0;
    while (!is_removed() && !blocked && !pending_commands.empty())
    {
        const std::string& command = pending_commands.front();
        if (pending_cmds_count && !ModuleManager::is_parallel_cmd(command))
            break;
        std::string rsp;
        shell->execute(command.c_str(), rsp);
        if (!rsp.empty() && respond("%s", rsp.c_str()))
            touch();
        if (!blocked && show_prompt())
            touch();
        pending_commands.pop();
        executed++;
    }

    return executed;
}

void ControlConn::block()
{
    blocked++;
}

void ControlConn::remove()
{
    removed = true;
}

void ControlConn::touch()
{
    touched = time(nullptr);
}

time_t ControlConn::get_touched() const
{
    return touched;
}

void ControlConn::unblock()
{
    if (blocked)
    {
        blocked--;
        execute_commands();
        if (!blocked && !show_prompt())
            remove();
    }
}

// FIXIT-L would like to flush prompt w/o \n
bool ControlConn::show_prompt()
{
    return is_removed() or respond("%s\n", get_prompt());
}

bool ControlConn::respond(const char* format, va_list& ap)
{
    if (in_main_thread() && write_failed_main)
        return false;
    char buf[STD_BUF];
    int response_len = vsnprintf(buf, sizeof(buf), format, ap);

    if (response_len < 0 || (size_t)response_len >= sizeof(buf))
    {
        LogMessage("ControlConn::respond: Unable to create response buffer. buf_size=%zu,"
            " response_len=%d, format=%s\n", sizeof(buf), response_len, format);
        return false;
    }

    buf[response_len] = '\0';

    int bytes_written = 0;
    while (bytes_written < response_len)
    {
        ssize_t n = write(fd, buf + bytes_written, response_len - bytes_written);

        if (n < 0)
        {
            if (errno != EAGAIN && errno != EINTR)
            {
                if (in_main_thread())
                    write_failed_main = true;
                ErrorMessage("ControlConn: Error in writing response: %s, buf: %s\n", get_error(errno), buf);
                return false;
            }
        }
        else
            bytes_written += n;
    }

    return true;
}

bool ControlConn::respond(const char* format, ...)
{
    va_list ap;
    va_start(ap, format);
    bool ret = respond(format, ap);
    va_end(ap);

    return ret;
}
