/*
 * pcap-reader.c - reads DBus messages from a pcap stream
 * Copyright © 2011–2012  Collabora Ltd.
 * Copyright © 2018–2020 Will Thompson
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#define _GNU_SOURCE

#include "config.h"
#include "pcap-reader.h"

#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <pcap/pcap.h>

typedef struct _BustlePcapReader {
    GObject parent;

    gchar *filename;
    FILE *filep;

    pcap_t *pcap_in;
} BustlePcapReader;

typedef enum {
    PROP_FILENAME = 1,
    PROP_FILEP
} BustlePcapReaderProp;

static void initable_iface_init (
    gpointer g_class,
    gpointer unused);

G_DEFINE_TYPE_WITH_CODE (BustlePcapReader, bustle_pcap_reader, G_TYPE_OBJECT,
    G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init);
    )

/* A sad echo of the functions in libglnx. */
static inline void *
throw_errno (GError **error,
             const gchar *prefix)
{
  int errsv = errno;
  g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errsv),
               "%s: %s", prefix, g_strerror (errsv));
  return NULL;
}

static void
bustle_pcap_reader_init (BustlePcapReader *self)
{
}

static void
bustle_pcap_reader_set_property (
    GObject *object,
    guint property_id,
    const GValue *value,
    GParamSpec *pspec)
{
  BustlePcapReader *self = BUSTLE_PCAP_READER (object);

  switch ((BustlePcapReaderProp) property_id)
    {
      case PROP_FILENAME:
        self->filename = g_value_dup_string (value);
        break;
      case PROP_FILEP:
        self->filep = g_value_get_pointer (value);
        break;
      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    }
}

static void
bustle_pcap_reader_finalize (GObject *object)
{
  BustlePcapReader *self = BUSTLE_PCAP_READER (object);
  GObjectClass *parent_class = bustle_pcap_reader_parent_class;

  g_clear_pointer (&self->filename, g_free);
  g_clear_pointer (&self->filep, fclose);
  g_clear_pointer (&self->pcap_in, pcap_close);

  if (parent_class->finalize != NULL)
    parent_class->finalize (object);
}

static void
bustle_pcap_reader_class_init (BustlePcapReaderClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GParamSpec *param_spec;

  object_class->set_property = bustle_pcap_reader_set_property;
  object_class->finalize = bustle_pcap_reader_finalize;

  param_spec = g_param_spec_string ("filename", "Filename",
                                    "Path to pcap file to read",
                                    NULL,
      G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
  g_object_class_install_property (object_class, PROP_FILENAME, param_spec);

  param_spec = g_param_spec_pointer ("filep", "FILE *",
                                    "FILE * to read pcap stream from",
      G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
  g_object_class_install_property (object_class, PROP_FILEP, param_spec);
}

/**
 * bustle_pcap_reader_read_one:
 * @self:
 * @hdr: (out) (transfer none): location to store pcap header (or %NULL on EOF)
 * @blob: (out) (transfer none): location to store raw message (or %NULL on EOF)
 * @message: (out) (transfer full): location to store parsed message (or %NULL on EOF)
 * @error:
 *
 * Returns: %FALSE on error; %TRUE on success or end-of-file.
 */
gboolean
bustle_pcap_reader_read_one (BustlePcapReader  *self,
                             glong             *sec,
                             glong             *usec,
                             const guchar     **blob,
                             guint             *length,
                             GDBusMessage     **message,
                             GError           **error)
{
  struct pcap_pkthdr *hdr;
  int ret;

  g_return_val_if_fail (BUSTLE_IS_PCAP_READER (self), FALSE);
  g_return_val_if_fail (sec != NULL, FALSE);
  g_return_val_if_fail (usec != NULL, FALSE);
  g_return_val_if_fail (blob != NULL, FALSE);
  g_return_val_if_fail (length != NULL, FALSE);
  g_return_val_if_fail (message == NULL || *message == NULL, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  if (self->pcap_in == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_CLOSED, "Already closed");
      return FALSE;
    }

  ret = pcap_next_ex (self->pcap_in, &hdr, blob);
  switch (ret)
    {
      case 1:
        if (message != NULL)
          {
            *message = g_dbus_message_new_from_blob ((guchar *) *blob, hdr->caplen, G_DBUS_CAPABILITY_FLAGS_UNIX_FD_PASSING, error);
            if (*message == NULL)
              {
                g_prefix_error (error, "Error while parsing message from dbus-monitor: ");
                return FALSE;
              }
          }

        *sec = hdr->ts.tv_sec;
        *usec = hdr->ts.tv_usec;
        *length = hdr->caplen;
        return TRUE;

      case -2:
        /* EOF */
        *sec = 0;
        *usec = 0;
        *blob = NULL;
        *length = 0;
        if (message != NULL)
          *message = NULL;
        return TRUE;

      default:
        g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
            "Error %i reading dbus-monitor stream: %s",
            ret, pcap_geterr (self->pcap_in));
        return FALSE;
    }
}


/**
 * bustle_pcap_reader_close:
 * @self: a #BustlePcapReader
 *
 * Closes the underlying file or stream.
 *
 * If @self is reading from a pipe to `dbus-monitor`, this will cause
 * `dbus-monitor` to quit in due course when it next tries to write to the
 * pipe.
 */
void
bustle_pcap_reader_close (BustlePcapReader *self)
{
  g_return_if_fail (BUSTLE_IS_PCAP_READER (self));

  g_clear_pointer (&self->pcap_in, pcap_close);
  g_clear_pointer (&self->filep, fclose);
}

static gboolean
initable_init (
    GInitable *initable,
    GCancellable *cancellable,
    GError **error)
{
  BustlePcapReader *self = BUSTLE_PCAP_READER (initable);
  char errbuf[PCAP_ERRBUF_SIZE] = {0};

  g_return_val_if_fail ((self->filename == NULL) ^ (self->filep == NULL),
                        FALSE);

  if (self->filename != NULL)
    {
      self->pcap_in = pcap_open_offline (self->filename, errbuf);
    }
  else /* self->filep != NULL */
    {
      self->pcap_in = pcap_fopen_offline (self->filep, errbuf);
    }

  if (self->pcap_in == NULL)
    {
      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, errbuf);
      return FALSE;
    }

  /* Now owned by pcap_in */
  self->filep = NULL;

  int dlt = pcap_datalink (self->pcap_in);
  if (dlt != DLT_DBUS)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
                   "Unexpected link type %s",
                   pcap_datalink_val_to_name (dlt));
      bustle_pcap_reader_close (self);
      return FALSE;
    }

  return TRUE;
}

static void
initable_iface_init (
    gpointer g_class,
    gpointer unused)
{
  GInitableIface *iface = g_class;

  iface->init = initable_init;
}

BustlePcapReader *
bustle_pcap_reader_open (const gchar *filename,
                         GError **error)
{
  g_return_val_if_fail (filename != NULL, NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);

  return g_initable_new (
      BUSTLE_TYPE_PCAP_READER, NULL, error,
      "filename", filename,
      NULL);
}

/**
 * bustle_pcap_reader_fopen:
 * @filep: (transfer full):
 *
 * Returns: a reader, or %NULL on error
 */
BustlePcapReader *
bustle_pcap_reader_fopen (FILE *filep,
                          GError **error)
{
  g_return_val_if_fail (filep != NULL, NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);

  return g_initable_new (
      BUSTLE_TYPE_PCAP_READER, NULL, error,
      "filep", filep,
      NULL);
}
