// file      : bbot/agent/tftp.cxx -*- C++ -*-
// license   : MIT; see accompanying LICENSE file

#include <bbot/agent/tftp.hxx>

#include <arpa/inet.h>  // htonl()
#include <netinet/in.h> // sockaddr_in
#include <sys/socket.h>
#include <sys/select.h>

#include <cstring> // memset()

#include <bbot/agent/agent.hxx>

using namespace std;
using namespace butl;

namespace bbot
{
  tftp_server::
  tftp_server (const string& map, uint16_t port)
  {
    int fd (socket (AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0));

    if (fd == -1)
      throw_system_error (errno);

    fd_.reset (fd);

    // Bind to ephemeral port.
    //
    sockaddr_in addr;
    memset (&addr, 0, sizeof (addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl (INADDR_ANY);
    addr.sin_port = htons (port);

    // Not to confuse with std::bind().
    //
    if (::bind (fd,
                reinterpret_cast<sockaddr*> (&addr),
                sizeof (sockaddr_in)) == -1)
      throw_system_error (errno);

    // Create the map file.
    //
    map_ = auto_rmfile (path::temp_path ("bbot-agent-tftp-map"));
    ofdstream ofs (map_.path);
    ofs << map << endl;
    ofs.close ();
  }

  uint16_t tftp_server::
  port () const
  {
    sockaddr_in addr;
    socklen_t size (sizeof (addr));

    if (getsockname (fd_.get (),
                     reinterpret_cast<sockaddr*> (&addr),
                     &size) == -1)
      throw_system_error (errno);

    assert (size == sizeof (addr));
    return ntohs (addr.sin_port);
  }

  bool tftp_server::
  serve (size_t& sec, size_t inc)
  {
    tracer trace ("tftp_server::serve");

    if (inc == 0 || inc > sec)
      inc = sec;

    int fd (fd_.get ());

    // Note: Linux updates the timeout value which we rely upon.
    //
    timeval timeout {static_cast<long> (inc), 0};

    fd_set rd;
    FD_ZERO (&rd);

    for (;;)
    {
      FD_SET (fd, &rd);

      int r (select (fd + 1, &rd, nullptr, nullptr, &timeout));

      if (r == -1)
      {
        if (errno == EINTR)
          continue;

        throw_system_error (errno);
      }
      else if (r == 0) // Timeout.
      {
        sec -= inc;
        return false;
      }

      if (FD_ISSET (fd, &rd))
      {
        // The inetd "protocol" is to pass the socket as stdin/stdout file
        // descriptors.
        //
        // Notes/issues:
        //
        // 1. Writes diagnostics to syslog.
        //
        // 2. The fd file descriptor has the FD_CLOEXEC flag set (see the
        //    socket() call in the constructor for details). While the below
        //    fddup() calls will copy this flag to the returned descriptor,
        //    let's request that explicitly, for good measure (minor
        //    optimization will be a bonus).
        //
        run_io (trace,
                fddup (fd, true /* no_inherit */),
                fddup (fd, true /* no_inherit */),
                2,
                "sudo",                  // Required for --secure (chroot).
                "/usr/sbin/in.tftpd",    // Standard installation location.
                "--verbosity", 1,        // Verbosity level.
                "--timeout", 1,          // Wait for more requests.
                "--permissive",          // Use inherited umask.
                "--create",              // Allow creating new files (PUT).
                "--map-file", map_.path, // Path remapping rules.
                "--user", uname,         // Run as our effective user.
                "--secure",              // Chroot to data directory.
                ops.tftp ());

        // This is not really accurate since tftpd will, for example, serve
        // an upload request until it is complete. But it's close enough for
        // our needs.
        //
        sec -= (inc - static_cast<size_t> (timeout.tv_sec));
        return true;
      }
    }
  }
}
