/*
 * Copyright (C) 2021 Purism SPC
 *
 * This file is part of Calls.
 *
 * Calls is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Calls 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 Calls. If not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Evangelos Ribeiro Tzaras <evangelos.tzaras@puri.sm>
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 */

#define G_LOG_DOMAIN "CallsNetworkWatch"

#include "calls-network-watch.h"

#include <gio/gio.h>

#include <arpa/inet.h>
#include <linux/rtnetlink.h>

/**
 * SECTION:calls-network-watch
 * @short_description: Watches network interfaces
 * @Title: CallsNetworkWatch
 *
 * The #CallsNetworkWatch uses rtnetlink messages to keep
 * track of network interfaces.
 *
 * This allows the sofia SIP stack to bind to the
 * correct interface and can later help in deciding which codec to
 * use for media (f.e. lower bandwidth on a metered connection).
 */


typedef struct {
  struct nlmsghdr n;
  struct rtmsg r;
  char buf[1024];
} RequestData;

enum {
  PROP_0,
  PROP_IPV4,
  PROP_IPV6,
  PROP_LAST_PROP
};
static GParamSpec *props[PROP_LAST_PROP];

enum {
  NETWORK_CHANGED,
  N_SIGNALS
};
static guint signals[N_SIGNALS];


typedef struct _CallsNetworkWatch {
  GObject parent;

  RequestData *req;
  int fd;
  unsigned int seq;
  char buf[1024]; /* buffer for responses to rtnetlink requests */

  guint timeout_id;

  char *ipv4;
  char *ipv6;
  char tmp_addr[INET6_ADDRSTRLEN];
} CallsNetworkWatch;


static void initable_iface_init (GInitableIface *iface);

G_DEFINE_TYPE_WITH_CODE (CallsNetworkWatch, calls_network_watch, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init))

#define DUMMY_TARGET_V4 "1.2.3.4"
#define DUMMY_TARGET_V6 "::1.2.3.4"

static gboolean
req_route_v4 (CallsNetworkWatch *self)
{
  int addr_len = 4;
  int len = RTA_LENGTH (addr_len);
  struct rtattr *rta;

  g_assert (CALLS_IS_NETWORK_WATCH (self));

  self->req->n.nlmsg_len = NLMSG_LENGTH (sizeof (struct rtmsg));
  self->req->n.nlmsg_flags = NLM_F_REQUEST;
  self->req->n.nlmsg_type = RTM_GETROUTE;
  self->req->r.rtm_family = AF_INET;


  rta = ((struct rtattr *) (((void *) (&self->req->n)) +
                            NLMSG_ALIGN (self->req->n.nlmsg_len)));
  rta->rta_type = RTA_DST;
  rta->rta_len = len;

  /* use a dummy target destination */
  if (inet_pton (AF_INET, DUMMY_TARGET_V4, RTA_DATA (rta)) != 1)
    return FALSE;

  self->req->n.nlmsg_len = NLMSG_ALIGN (self->req->n.nlmsg_len) + RTA_ALIGN (len);

  return TRUE;
}


static gboolean
req_route_v6 (CallsNetworkWatch *self)
{
  int addr_len = 16;
  int len = RTA_LENGTH (addr_len);
  struct rtattr *rta;

  g_assert (CALLS_IS_NETWORK_WATCH (self));

  self->req->n.nlmsg_len = NLMSG_LENGTH (sizeof (struct rtmsg));
  self->req->n.nlmsg_flags = NLM_F_REQUEST;
  self->req->n.nlmsg_type = RTM_GETROUTE;
  self->req->r.rtm_family = AF_INET6;

  rta = ((struct rtattr *) (((void *) (&self->req->n)) +
                            NLMSG_ALIGN (self->req->n.nlmsg_len)));
  rta->rta_type = RTA_DST;
  rta->rta_len = len;

  /* use a dummy target destination */
  if (inet_pton (AF_INET6, DUMMY_TARGET_V6, RTA_DATA (rta)) != 1)
    return FALSE;

  self->req->n.nlmsg_len = NLMSG_ALIGN (self->req->n.nlmsg_len) + RTA_ALIGN (len);

  return TRUE;
}

#undef DUMMY_TARGET_V4
#undef DUMMY_TARGET_V6

static gboolean
talk_rtnl (CallsNetworkWatch *self)
{
  struct iovec iov;
  struct iovec riov;
  struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK };
  struct msghdr msg = {
    .msg_name = &nladdr,
    .msg_namelen = sizeof (nladdr),
    .msg_iov = &iov,
    .msg_iovlen = 1,
  };

  struct nlmsghdr *h;
  int status;

  g_assert (CALLS_IS_NETWORK_WATCH (self));

  iov.iov_base = (void *) &self->req->n;
  iov.iov_len = self->req->n.nlmsg_len;
  self->req->n.nlmsg_seq = self->seq++;

  status = sendmsg (self->fd, &msg, 0);
  if (status < 0) {
    g_warning ("Could not send rtnetlink: %d", errno);
    return FALSE;
  }

  /* change msg to use response iov */
  riov.iov_base = self->buf;
  riov.iov_len = sizeof (self->buf);

  msg.msg_iov = &riov;
  msg.msg_iovlen = 1;

  status = recvmsg (self->fd, &msg, 0);

  if (status == -1) {
    g_warning ("Could not receive rtnetlink: %d", errno);
    return FALSE;
  }

  h = (struct nlmsghdr *) self->buf;
  if (h->nlmsg_type == NLMSG_ERROR) {
    g_warning ("An error occured in the netlink stack");
    return FALSE;
  }

  return TRUE;
}

static gboolean
get_prefsrc (CallsNetworkWatch *self,
             int                family)
{
  struct nlmsghdr *h;
  struct rtmsg *r;
  struct rtattr *rta;
  int len;
  gboolean found = FALSE;

  g_assert (CALLS_IS_NETWORK_WATCH (self));

  h = (struct nlmsghdr *) self->buf;
  r = NLMSG_DATA (self->buf);
  rta = RTM_RTA (r);
  len = h->nlmsg_len - NLMSG_LENGTH (sizeof (*r));

  while (RTA_OK (rta, len)) {
    if (rta->rta_type == RTA_PREFSRC) {
      found = TRUE;
      break;
    }
    rta = RTA_NEXT (rta, len);
  }

  if (!found)
    return FALSE;

  if (family == AF_INET) {
    inet_ntop (AF_INET, RTA_DATA (rta), self->tmp_addr, INET_ADDRSTRLEN);
  } else if (family == AF_INET6) {
    inet_ntop (AF_INET6, RTA_DATA (rta), self->tmp_addr, INET6_ADDRSTRLEN);
  } else {
    return FALSE;
  }

  return TRUE;
}

static gboolean
fetch_ipv4 (CallsNetworkWatch *self)
{
  g_assert (CALLS_IS_NETWORK_WATCH (self));

  if (!req_route_v4 (self))
    return FALSE;

  if (!talk_rtnl (self))
    return FALSE;

  return get_prefsrc (self, AF_INET);
}

static gboolean
fetch_ipv6 (CallsNetworkWatch *self)
{
  g_assert (CALLS_IS_NETWORK_WATCH (self));

  if (!req_route_v6 (self))
    return FALSE;

  if (!talk_rtnl (self))
    return FALSE;

  return get_prefsrc (self, AF_INET6);
}


static gboolean
on_watch_network (CallsNetworkWatch *self)
{
  gboolean changed_v4 = FALSE;
  gboolean changed_v6 = FALSE;

  changed_v4 = fetch_ipv4 (self) && g_strcmp0 (self->tmp_addr, self->ipv4) != 0;
  if (changed_v4) {
    g_free (self->ipv4);
    self->ipv4 = g_strdup (self->tmp_addr);
    g_debug ("New IPv4: %s", self->ipv4);
    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IPV4]);
  }

  changed_v6 = fetch_ipv6 (self) && g_strcmp0 (self->tmp_addr, self->ipv6) != 0;
  if (changed_v6) {
    g_free (self->ipv6);
    self->ipv6 = g_strdup (self->tmp_addr);
    g_debug ("New IPv6: %s", self->ipv6);
    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IPV6]);
  }

  if (changed_v4 || changed_v6)
    g_signal_emit (self, signals[NETWORK_CHANGED], 0);

  return G_SOURCE_CONTINUE;
}

static void
calls_network_watch_get_property (GObject *object,
                                  guint    property_id,
                                  GValue  *value,
                                  GParamSpec *pspec)
{
  CallsNetworkWatch *self = CALLS_NETWORK_WATCH (object);

  switch (property_id) {
  case PROP_IPV4:
    g_value_set_string (value, calls_network_watch_get_ipv4 (self));
    break;

  case PROP_IPV6:
    g_value_set_string (value, calls_network_watch_get_ipv6 (self));
    break;

  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}


static void
calls_network_watch_finalize (GObject *object)
{
  CallsNetworkWatch *self = CALLS_NETWORK_WATCH (object);

  g_source_remove (self->timeout_id);
  g_free (self->req);
  g_free (self->ipv4);
  g_free (self->ipv6);
  close (self->fd);

  G_OBJECT_CLASS (calls_network_watch_parent_class)->finalize (object);
}

static void
calls_network_watch_class_init (CallsNetworkWatchClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->get_property = calls_network_watch_get_property;
  object_class->finalize = calls_network_watch_finalize;

  signals[NETWORK_CHANGED] = g_signal_new ("network-changed",
                                           CALLS_TYPE_NETWORK_WATCH,
                                           G_SIGNAL_RUN_LAST,
                                           0,
                                           NULL, NULL, NULL,
                                           G_TYPE_NONE,
                                           0);

  props[PROP_IPV4] =
    g_param_spec_string ("ipv4",
                         "IPv4",
                         "The preferred source address for IPv4",
                         NULL,
                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  props[PROP_IPV6] =
    g_param_spec_string ("ipv6",
                         "IPv6",
                         "The preferred source address for IPv6",
                         NULL,
                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
}


static void
calls_network_watch_init (CallsNetworkWatch *self)
{
  self->seq = time (NULL);
  self->req = g_new0 (RequestData, 1);
  self->timeout_id = g_timeout_add_seconds (15,
                                            G_SOURCE_FUNC (on_watch_network),
                                            self);
}


static gboolean
calls_network_watch_initable_init (GInitable    *initable,
                                   GCancellable *cancelable,
                                   GError      **error)
{
  CallsNetworkWatch *self = CALLS_NETWORK_WATCH (initable);
  gboolean ret = FALSE;

  self->fd = socket (AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
  if (self->fd == -1) {
    int errsv = errno;
    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                 "Failed to create netlink socket: %d", errsv);
    return FALSE;
  }

  if (fetch_ipv4 (self)) {
    ret = TRUE;
    self->ipv4 = g_strdup (self->tmp_addr);
  }
  if (fetch_ipv6 (self)) {
    ret = TRUE;
    self->ipv6 = g_strdup (self->tmp_addr);
  }

  return ret;
}


static void
initable_iface_init (GInitableIface *iface)
{
  iface->init = calls_network_watch_initable_init;
}


CallsNetworkWatch *
calls_network_watch_get_default (void)
{
  static CallsNetworkWatch *instance;

  if (instance == NULL) {
    g_autoptr (GError) error = NULL;
    instance = g_initable_new (CALLS_TYPE_NETWORK_WATCH, NULL, &error, NULL);

    if (!instance)
      g_warning ("Network watch could not be initialized: %s", error->message);
  }
  return instance;
}


const char *
calls_network_watch_get_ipv4 (CallsNetworkWatch *self)
{
  g_return_val_if_fail (CALLS_IS_NETWORK_WATCH (self), NULL);

  return self->ipv4;
}


const char *
calls_network_watch_get_ipv6 (CallsNetworkWatch *self)
{
  g_return_val_if_fail (CALLS_IS_NETWORK_WATCH (self), NULL);

  return self->ipv6;
}
