/** @file

  A brief file description

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you 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.
 */

#include "ts/ink_inet.h"
#include "ts/ink_string.h"
#include "P_EventSystem.h"

#include "LogSock.h"
#include "LogUtils.h"

static const int LS_SOCKTYPE = SOCK_STREAM;
static const int LS_PROTOCOL = 0;

/**
  LogSock

  The constructor establishes the connection table (ct) and initializes the
  first entry of the table (index 0) to be the port on which new
  connections are accepted.
*/
LogSock::LogSock(int max_connects) : ct((ConnectTable *)nullptr), m_accept_connections(false), m_max_connections(max_connects + 1)
{
  ink_assert(m_max_connections > 0);

  //
  // allocate space for the connection table.
  //
  ct = new ConnectTable[m_max_connections];
  ink_assert(ct != nullptr);
  for (int i = 0; i < m_max_connections; ++i) {
    init_cid(i, nullptr, 0, -1, LogSock::LS_STATE_UNUSED);
  }

  Debug("log-sock", "LogSocket established");
}

/**
  LogSock::~LogSock

  Shut down all connections and delete memory for the tables.
*/
LogSock::~LogSock()
{
  Debug("log-sock", "shutting down LogSocket on [%s:%d]", ct[0].host, ct[0].port);

  this->close();  // close all connections
  this->close(0); // close accept socket
  delete[] ct;    // delete the connection table
}

/**
  LogSock::listen

  This routine sets up the LogSock to begin accepting connections on the
  given @param accept port.  A maximum number of connections is also specified,
  which is used to establish the size of the listen queue.

  @Return zero if all goes well, -1 otherwise.
*/
int
LogSock::listen(int accept_port, int family)
{
  IpEndpoint bind_addr;
  int size = sizeof(bind_addr);
  char this_host[MAXDNAME];
  int ret;
  ats_scoped_fd accept_sd;

  Debug("log-sock", "Listening ...");

  // Set up local address for bind.
  bind_addr.setToAnyAddr(family);
  if (!bind_addr.isValid()) {
    Warning("Could not set up socket - invalid address family %d", family);
    return -1;
  }
  bind_addr.port() = htons(accept_port);
  size             = ats_ip_size(&bind_addr.sa);

  //
  // create the socket for accepting new connections
  //
  accept_sd = ::socket(family, LS_SOCKTYPE, LS_PROTOCOL);
  if (accept_sd < 0) {
    Warning("Could not create a socket for family %d: %s", family, strerror(errno));
    return -1;
  }
  //
  // Set socket options (NO_LINGER, TCP_NODELAY, SO_REUSEADDR)
  //
  // CLOSE ON EXEC
  if ((ret = safe_fcntl(accept_sd, F_SETFD, FD_CLOEXEC)) < 0) {
    Warning("Could not set option CLOSE ON EXEC on socket (%d): %s", ret, strerror(errno));
    return -1;
  }
  // NO_LINGER
  struct linger l;
  l.l_onoff  = 0;
  l.l_linger = 0;
  if ((ret = safe_setsockopt(accept_sd, SOL_SOCKET, SO_LINGER, (char *)&l, sizeof(l))) < 0) {
    Warning("Could not set option NO_LINGER on socket (%d): %s", ret, strerror(errno));
    return -1;
  }
  // REUSEADDR
  if ((ret = safe_setsockopt(accept_sd, SOL_SOCKET, SO_REUSEADDR, SOCKOPT_ON, sizeof(int))) < 0) {
    Warning("Could not set option REUSEADDR on socket (%d): %s", ret, strerror(errno));
    return -1;
  }

  // Bind to local address.
  if ((ret = safe_bind(accept_sd, &bind_addr.sa, size)) < 0) {
    Warning("Could not bind port: %s", strerror(errno));
    return -1;
  }

  if ((ret = safe_setsockopt(accept_sd, IPPROTO_TCP, TCP_NODELAY, SOCKOPT_ON, sizeof(int))) < 0) {
    Warning("Could not set option TCP_NODELAY on socket (%d): %s", ret, strerror(errno));
    return -1;
  }

  if ((ret = safe_setsockopt(accept_sd, SOL_SOCKET, SO_KEEPALIVE, SOCKOPT_ON, sizeof(int))) < 0) {
    Warning("Could not set option SO_KEEPALIVE on socket (%d): %s", ret, strerror(errno));
    return -1;
  }

  //
  // if the accept_port argument was zero, then the system just picked
  // one for us, so we need to find out what it was and record it in the
  // connection table correctly.
  //
  if (accept_port == 0) {
    ret = safe_getsockname(accept_sd, &bind_addr.sa, &size);
    if (ret == 0) {
      accept_port = ntohs(bind_addr.port());
    }
  }
  //
  // establish the listen queue for incomming connections
  //
  if ((ret = safe_listen(accept_sd, m_max_connections)) < 0) {
    Warning("Could not establish listen queue: %s", strerror(errno));
    return -1;
  }
  //
  // initialize the first entry of the table for accepting incoming
  // connection requests.
  //
  if (gethostname(&this_host[0], MAXDNAME) != 0) {
    snprintf(this_host, sizeof(this_host), "unknown-host");
  }
  init_cid(0, this_host, accept_port, accept_sd, LogSock::LS_STATE_INCOMING);

  m_accept_connections = true;
  Debug("log-sock", "LogSocket established on [%s:%d]", this_host, accept_port);

  accept_sd.release();
  return 0;
}

/**
  LogSock::accept

  Accept a new connection.  This is a blocking operation, so you may want
  to use one of the non-blocking pending_XXX calls to see if there is a
  connection first.
  @return This returns the table index for the new connection.
*/
int
LogSock::accept()
{
  int cid, connect_sd;
  IpEndpoint connect_addr;
  socklen_t size = sizeof(connect_addr);
  in_port_t connect_port;

  if (!m_accept_connections || ct[0].sd < 0) {
    return LogSock::LS_ERROR_NO_CONNECTION;
  }

  cid = new_cid();
  if (cid < 0) {
    return LogSock::LS_ERROR_CONNECT_TABLE_FULL;
  }

  Debug("log-sock", "waiting to accept a new connection");

  connect_sd = ::accept(ct[0].sd, &connect_addr.sa, &size);
  if (connect_sd < 0) {
    return LogSock::LS_ERROR_ACCEPT;
  }
  connect_port = ntohs(connect_addr.port());

  init_cid(cid, nullptr, connect_port, connect_sd, LogSock::LS_STATE_INCOMING);

  Debug("log-sock", "new connection accepted, cid = %d, port = %d", cid, connect_port);

  return cid;
}

/**
  LogSock::connect

  Establish a new connection to another machine [host:port], and place this
  information into the connection and poll tables.
*/
int
LogSock::connect(sockaddr const *ip)
{
  int cid, ret;
  ats_scoped_fd connect_sd;
  uint16_t port;

  if (!ats_is_ip(ip)) {
    Note("Invalid host IP or port number for connection");
    return LogSock::LS_ERROR_NO_SUCH_HOST;
  }
  port = ntohs(ats_ip_port_cast(ip));

  ip_port_text_buffer ipstr;
  Debug("log-sock", "connecting to [%s:%d]", ats_ip_nptop(ip, ipstr, sizeof(ipstr)), port);

  // get an index into the connection table
  cid = new_cid();
  if (cid < 0) {
    Note("No more connections allowed for this socket");
    return LogSock::LS_ERROR_CONNECT_TABLE_FULL;
  }
  // initialize a new socket descriptor
  connect_sd = ::socket(ip->sa_family, LS_SOCKTYPE, LS_PROTOCOL);
  if (connect_sd < 0) {
    Note("Error initializing socket for connection: %d", static_cast<int>(connect_sd));
    return LogSock::LS_ERROR_SOCKET;
  }

  if ((ret = safe_setsockopt(connect_sd, IPPROTO_TCP, TCP_NODELAY, SOCKOPT_ON, sizeof(int))) < 0) {
    Note("Could not set option TCP_NODELAY on socket (%d): %s", ret, strerror(errno));
    return -1;
  }

  if ((ret = safe_setsockopt(connect_sd, SOL_SOCKET, SO_KEEPALIVE, SOCKOPT_ON, sizeof(int))) < 0) {
    Note("Could not set option SO_KEEPALIVE on socket (%d): %s", ret, strerror(errno));
    return -1;
  }

  // attempt to connect
  if (::connect(connect_sd, ip, ats_ip_size(ip)) != 0) {
    Note("Failure to connect");
    return LogSock::LS_ERROR_CONNECT;
  }

  init_cid(cid, ipstr, port, connect_sd, LogSock::LS_STATE_OUTGOING);

  Debug("log-sock", "outgoing connection to [%s:%d] established, fd  = %d", ipstr, port, cid);

  connect_sd.release();
  return cid;
}

/**
  LogSock::pending_data

  This private routine checks for incoming data on some of the socket
  descriptors.
  @return Returns true if there is something incoming, with *cid
  set to the index corresponding to the incoming socket.
*/
bool
LogSock::pending_data(int *cid, int timeout_msec, bool include_connects)
{
  int start_index, ret, n_poll_fds, i;
  static struct pollfd fds[LS_CONST_CLUSTER_MAX_MACHINES];
  int fd_to_cid[LS_CONST_CLUSTER_MAX_MACHINES];

  ink_assert(m_max_connections <= (LS_CONST_CLUSTER_MAX_MACHINES + 1));
  ink_assert(cid != nullptr);
  ink_assert(timeout_msec >= 0);

  //
  // we'll use the poll() routine, which replaces the select routine
  // to support a larger number of socket descriptors.  to use poll,
  // we need to set-up a pollfd array for the socket descriptors
  // that will be polled.
  //

  if (*cid >= 0) { // look for data on this specific socket

    ink_assert(*cid < m_max_connections);
    fds[0].fd      = ct[*cid].sd;
    fds[0].events  = POLLIN;
    fds[0].revents = 0;
    fd_to_cid[0]   = *cid;
    n_poll_fds     = 1;

  } else { // look for data on any INCOMING socket

    if (include_connects) {
      start_index = 0;
    } else {
      start_index = 1;
    }
    n_poll_fds = 0;
    for (i = start_index; i < m_max_connections; i++) {
      if (ct[i].state == LogSock::LS_STATE_INCOMING) {
        fds[n_poll_fds].fd      = ct[i].sd;
        fds[n_poll_fds].events  = POLLIN;
        fds[n_poll_fds].revents = 0;
        fd_to_cid[n_poll_fds]   = i;
        n_poll_fds++;
      }
    }
  }

  if (n_poll_fds == 0) {
    return false;
  }

  ret = ::poll(fds, n_poll_fds, timeout_msec);

  if (ret == 0) {
    return false; // timeout
  } else if (ret < 0) {
    Debug("log-sock", "error on poll");
    return false; // error
  }
  //
  // a positive return value indicates how many descriptors had something
  // waiting on them.  We only care about finding one of them, so we'll
  // look for the first one with an revents flag set to POLLIN.
  //

  for (i = 0; i < n_poll_fds; i++) {
    if (fds[i].revents & POLLIN) {
      *cid = fd_to_cid[i];
      Debug("log-sock", "poll successful on index %d", *cid);
      return true;
    }
  }

  Debug("log-sock", "invalid revents in the poll table");
  return false;
}

/**
  LogSock::pending_any

  Check for incomming data on any of the INCOMING sockets.
*/
bool
LogSock::pending_any(int *cid, int timeout_msec)
{
  ink_assert(cid != nullptr);
  *cid = -1;
  if (m_accept_connections) {
    return pending_data(cid, timeout_msec, true);
  } else {
    return pending_data(cid, timeout_msec, false);
  }
}

/*-------------------------------------------------------------------------
  LogSock::pending_message_any

  Check for an incomming message on any of the INCOMING sockets, aside from
  the socket reserved for accepting new connections.
  -------------------------------------------------------------------------*/

bool
LogSock::pending_message_any(int *cid, int timeout_msec)
{
  ink_assert(cid != nullptr);
  *cid = -1;
  return pending_data(cid, timeout_msec, false);
}

/**
  LogSock::pending_message_on

  Check for incomming data on the specified socket.
*/
bool
LogSock::pending_message_on(int cid, int timeout_msec)
{
  return pending_data(&cid, timeout_msec, false);
}

/**
  LogSock::pending_connect

  Check for an incoming connection request on the socket reserved for that
  (cid = 0).
*/
bool
LogSock::pending_connect(int timeout_msec)
{
  int cid = 0;
  if (m_accept_connections) {
    return pending_data(&cid, timeout_msec, true);
  } else {
    return false;
  }
}

/**
  LogSock::close

  Close one (cid specified) or all (no argument) sockets, except for the
  incomming connection socket.
*/
void
LogSock::close(int cid)
{
  ink_assert(cid >= 0 && cid < m_max_connections);

  Debug("log-sock", "closing connection for cid %d", cid);

  if (ct[cid].state != LogSock::LS_STATE_UNUSED) {
    ::close(ct[cid].sd);
    delete ct[cid].host;
    ct[cid].state = LogSock::LS_STATE_UNUSED;
  }
}

void
LogSock::close()
{
  for (int i = 1; i < m_max_connections; i++) {
    this->close(i);
  }
}

/**
  LogSock::write

  Write data onto the socket corresponding to the given cid.  Return the
  number of bytes actually written.
*/
int
LogSock::write(int cid, void *buf, int bytes)
{
  LogSock::MsgHeader header = {0};
  header.msg_bytes          = 0;
  int ret;

  ink_assert(cid >= 0 && cid < m_max_connections);

  if (buf == nullptr || bytes == 0) {
    return 0;
  }

  if (ct[cid].state != LogSock::LS_STATE_OUTGOING) {
    return LogSock::LS_ERROR_STATE;
  }

  Debug("log-sock", "Sending %d bytes to cid %d", bytes, cid);

  //
  // send the message header
  //
  Debug("log-sock", "   sending header (%zu bytes)", sizeof(LogSock::MsgHeader));
  header.msg_bytes = bytes;
  ret              = ::send(ct[cid].sd, (char *)&header, sizeof(LogSock::MsgHeader), 0);
  if (ret != sizeof(LogSock::MsgHeader)) {
    return LogSock::LS_ERROR_WRITE;
  }
  //
  // send the actual data
  //
  Debug("log-sock", "   sending data (%d bytes)", bytes);
  return ::send(ct[cid].sd, (char *)buf, bytes, 0);
}

/**
  LogSock::read

  Read data from the specified connection.  This is a blocking call, so you
  may want to use one of the pending_XXX calls to see if there is anything
  to read first.  Returns number of bytes read.
*/
int
LogSock::read(int cid, void *buf, unsigned maxsize)
{
  LogSock::MsgHeader header;
  unsigned size;

  ink_assert(cid >= 0 && cid < m_max_connections);
  ink_assert(buf != nullptr);

  if (ct[cid].state != LogSock::LS_STATE_INCOMING) {
    return LogSock::LS_ERROR_STATE;
  }

  Debug("log-sock", "reading data from cid %d", cid);

  if (read_header(ct[cid].sd, &header) < 0) {
    return LogSock::LS_ERROR_READ;
  }

  size = ((unsigned)header.msg_bytes < maxsize) ? (unsigned)header.msg_bytes : maxsize;
  return read_body(ct[cid].sd, buf, size);
}

/**
  LogSock::read_alloc

  This routine reads data from the specified connection, and returns a
  pointer to newly allocated space (allocated with new) containing the
  data.  The number of bytes read is set in the argument size, which is
  expected to be a pointer to an int.
*/
void *
LogSock::read_alloc(int cid, int *size)
{
  LogSock::MsgHeader header;
  char *data;

  ink_assert(cid >= 0 && cid < m_max_connections);

  if (ct[cid].state != LogSock::LS_STATE_INCOMING) {
    return nullptr;
  }

  Debug("log-sock", "reading data from cid %d", cid);

  if (read_header(ct[cid].sd, &header) < 0) {
    return nullptr;
  }

  data = new char[header.msg_bytes];
  ink_assert(data != nullptr);

  if ((*size = read_body(ct[cid].sd, data, header.msg_bytes)) < 0) {
    delete[] data;
    data = nullptr;
  }

  return data;
}

/**
*/
bool
LogSock::is_connected(int cid, bool ping) const
{
  int i, j, flags;

  ink_assert(cid >= 0 && cid < m_max_connections);

  if (ct[cid].state == LogSock::LS_STATE_UNUSED) {
    return false;
  }

  if (ping) {
    flags = fcntl(ct[cid].sd, F_GETFL);
    ::fcntl(ct[cid].sd, F_SETFL, O_NONBLOCK);
    j = ::recv(ct[cid].sd, (char *)&i, sizeof(int), MSG_PEEK);
    ::fcntl(ct[cid].sd, F_SETFL, flags);
    if (j != 0) {
      return true;
    } else {
      return false;
    }
  } else {
    return (ct[cid].sd >= 0);
  }
}

/**
*/
void
LogSock::check_connections()
{
  for (int i = 1; i < m_max_connections; i++) {
    if (ct[i].state == LogSock::LS_STATE_INCOMING) {
      if (!is_connected(i, true)) {
        Debug("log-sock", "Connection %d is no longer connected", i);
        close(i);
      }
    }
  }
}

/**
  This routine will check to ensure that the client connecting is
  authorized to use the log collation port.  To authorize, the client is
  expected to send the logging secret string.
*/
bool
LogSock::authorized_client(int cid, char *key)
{
  //
  // Wait for up to 5 seconds for the client to authenticate
  //
  if (!pending_message_on(cid, 5000)) {
    return false;
  }
  //
  // Ok, the client has a pending message, so check to see if it matches
  // the given key.
  //
  char buf[1024];
  int size = this->read(cid, buf, 1024);
  ink_assert(size >= 0 && size <= 1024);

  if (strncmp(buf, key, size) == 0) {
    return true;
  }

  return false;
}

/**
*/
char *
LogSock::connected_host(int cid)
{
  ink_assert(cid >= 0 && cid < m_max_connections);
  return ct[cid].host;
}

/**
*/
int
LogSock::connected_port(int cid)
{
  ink_assert(cid >= 0 && cid < m_max_connections);
  return ct[cid].port;
}

/*-------------------------------------------------------------------------
  LOCAL ROUTINES
  -------------------------------------------------------------------------*/

/**
  LogSock::new_cid
*/
int
LogSock::new_cid()
{
  int cid = -1;

  for (int i = 1; i < m_max_connections; i++) {
    if (ct[i].state == LogSock::LS_STATE_UNUSED) {
      cid = i;
      break;
    }
  }

  return cid;
}

/**
  LogSock::init_cid
*/
void
LogSock::init_cid(int cid, char *host, int port, int sd, LogSock::State state)
{
  ink_assert(cid >= 0 && cid < m_max_connections);
  // host can be NULL if it's not known
  ink_assert(port >= 0);
  // sd can be -1 to indicate no connection yet
  ink_assert(state >= 0 && state < LogSock::LS_N_STATES);

  if (host != nullptr) {
    const size_t host_size = strlen(host) + 1;
    ct[cid].host           = new char[host_size];
    ink_strlcpy(ct[cid].host, host, host_size);
  } else {
    ct[cid].host = nullptr;
  }

  ct[cid].port  = port;
  ct[cid].sd    = sd;
  ct[cid].state = state;
}

/**
*/
int
LogSock::read_header(int sd, LogSock::MsgHeader *header)
{
  ink_assert(sd >= 0);
  ink_assert(header != nullptr);

  int bytes = ::recv(sd, (char *)header, sizeof(LogSock::MsgHeader), 0);
  if (bytes != sizeof(LogSock::MsgHeader)) {
    return -1;
  }

  return bytes;
}

/**
*/
int
LogSock::read_body(int sd, void *buf, int bytes)
{
  ink_assert(sd >= 0);
  ink_assert(buf != nullptr);
  ink_assert(bytes >= 0);

  if (bytes == 0) {
    return 0;
  }

  unsigned bytes_left = bytes;
  unsigned bytes_read;
  char *to = (char *)buf;

  while (bytes_left) {
    bytes_read = ::recv(sd, to, bytes_left, 0);
    to += bytes_read;
    bytes_left -= bytes_read;
  }

  return bytes;
}
