/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This program 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 2 of the License, or
 * (at your option) any later version.
 *
 * 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:
 *
 * Copyright (C) 2017 Aleksander Morgado <aleksander@aleksander.es>
 */

#include <config.h>
#include <string.h>

#include <ModemManager.h>
#include <ModemManager-tags.h>

#include "mm-daemon-enums-types.h"
#include "mm-filter.h"
#include "mm-log-object.h"

#define FILTER_PORT_MAYBE_FORBIDDEN "maybe-forbidden"

static void log_object_iface_init (MMLogObjectInterface *iface);

G_DEFINE_TYPE_EXTENDED (MMFilter, mm_filter, G_TYPE_OBJECT, 0,
                        G_IMPLEMENT_INTERFACE (MM_TYPE_LOG_OBJECT, log_object_iface_init))

enum {
    PROP_0,
    PROP_ENABLED_RULES,
    LAST_PROP
};

struct _MMFilterPrivate {
    MMFilterRule  enabled_rules;
    GList        *plugin_whitelist_tags;
    GArray       *plugin_whitelist_vendor_ids;
    GArray       *plugin_whitelist_product_ids;
};

/*****************************************************************************/

void
mm_filter_register_plugin_whitelist_tag (MMFilter    *self,
                                         const gchar *tag)
{
    if (!g_list_find_custom (self->priv->plugin_whitelist_tags, tag, (GCompareFunc) g_strcmp0)) {
        mm_obj_dbg (self, "registered plugin whitelist tag: %s", tag);
        self->priv->plugin_whitelist_tags = g_list_prepend (self->priv->plugin_whitelist_tags, g_strdup (tag));
    }
}

void
mm_filter_register_plugin_whitelist_vendor_id (MMFilter *self,
                                               guint16   vid)
{
    guint i;

    if (!self->priv->plugin_whitelist_vendor_ids)
        self->priv->plugin_whitelist_vendor_ids = g_array_sized_new (FALSE, FALSE, sizeof (guint16), 64);

    for (i = 0; i < self->priv->plugin_whitelist_vendor_ids->len; i++) {
        guint16 item;

        item = g_array_index (self->priv->plugin_whitelist_vendor_ids, guint16, i);
        if (item == vid)
            return;
    }

    g_array_append_val (self->priv->plugin_whitelist_vendor_ids, vid);
    mm_obj_dbg (self, "registered plugin whitelist vendor id: %04x", vid);
}

void
mm_filter_register_plugin_whitelist_product_id (MMFilter *self,
                                                guint16   vid,
                                                guint16   pid)
{
    mm_uint16_pair new_item;
    guint          i;

    if (!self->priv->plugin_whitelist_product_ids)
        self->priv->plugin_whitelist_product_ids = g_array_sized_new (FALSE, FALSE, sizeof (mm_uint16_pair), 10);

    for (i = 0; i < self->priv->plugin_whitelist_product_ids->len; i++) {
        mm_uint16_pair *item;

        item = &g_array_index (self->priv->plugin_whitelist_product_ids, mm_uint16_pair, i);
        if (item->l == vid && item->r == pid)
            return;
    }

    new_item.l = vid;
    new_item.r = pid;
    g_array_append_val (self->priv->plugin_whitelist_product_ids, new_item);
    mm_obj_dbg (self, "registered plugin whitelist product id: %04x:%04x", vid, pid);
}

/*****************************************************************************/

gboolean
mm_filter_port (MMFilter        *self,
                MMKernelDevice  *port,
                gboolean         manual_scan)
{
    const gchar *subsystem;
    const gchar *name;

    subsystem = mm_kernel_device_get_subsystem (port);
    name      = mm_kernel_device_get_name      (port);

    /* If the device is explicitly whitelisted, we process every port. Also
     * allow specifying this flag per-port instead of for the full device, e.g.
     * for platform tty ports where there's only one port anyway. */
    if ((self->priv->enabled_rules & MM_FILTER_RULE_EXPLICIT_WHITELIST) &&
        (mm_kernel_device_get_global_property_as_boolean (port, ID_MM_DEVICE_PROCESS) ||
         mm_kernel_device_get_property_as_boolean (port, ID_MM_DEVICE_PROCESS))) {
        mm_obj_dbg (self, "(%s/%s) port allowed: device is whitelisted", subsystem, name);
        return TRUE;
    }

    /* If the device is explicitly blacklisted, we ignore every port. */
    if ((self->priv->enabled_rules & MM_FILTER_RULE_EXPLICIT_BLACKLIST) &&
        (mm_kernel_device_get_global_property_as_boolean (port, ID_MM_DEVICE_IGNORE))) {
        mm_obj_dbg (self, "(%s/%s): port filtered: device is blacklisted", subsystem, name);
        return FALSE;
    }

    /* If the device is whitelisted by a plugin, we allow it. */
    if (self->priv->enabled_rules & MM_FILTER_RULE_PLUGIN_WHITELIST) {
        GList   *l;
        guint16  vid = 0;
        guint16  pid = 0;

        for (l = self->priv->plugin_whitelist_tags; l; l = g_list_next (l)) {
            if (mm_kernel_device_get_global_property_as_boolean (port, (const gchar *)(l->data)) ||
                mm_kernel_device_get_property_as_boolean (port, (const gchar *)(l->data))) {
                mm_obj_dbg (self, "(%s/%s) port allowed: device is whitelisted by plugin (tag)", subsystem, name);
                return TRUE;
            }
        }

        vid = mm_kernel_device_get_physdev_vid (port);
        if (vid)
            pid = mm_kernel_device_get_physdev_pid (port);

        if (vid && pid && self->priv->plugin_whitelist_product_ids) {
            guint i;

            for (i = 0; i < self->priv->plugin_whitelist_product_ids->len; i++) {
                mm_uint16_pair *item;

                item = &g_array_index (self->priv->plugin_whitelist_product_ids, mm_uint16_pair, i);
                if (item->l == vid && item->r == pid) {
                    mm_obj_dbg (self, "(%s/%s) port allowed: device is whitelisted by plugin (vid/pid)", subsystem, name);
                    return TRUE;
                }
            }
        }

        if (vid && self->priv->plugin_whitelist_vendor_ids) {
            guint i;

            for (i = 0; i < self->priv->plugin_whitelist_vendor_ids->len; i++) {
                guint16 item;

                item = g_array_index (self->priv->plugin_whitelist_vendor_ids, guint16, i);
                if (item == vid) {
                    mm_obj_dbg (self, "(%s/%s) port allowed: device is whitelisted by plugin (vid)", subsystem, name);
                    return TRUE;
                }
            }
        }
    }

    /* If this is a virtual device, don't allow it */
    if ((self->priv->enabled_rules & MM_FILTER_RULE_VIRTUAL) &&
        (!mm_kernel_device_get_physdev_sysfs_path (port))) {
        mm_obj_dbg (self, "(%s/%s) port filtered: virtual device", subsystem, name);
        return FALSE;
    }

    /* If this is a net device, we always allow it */
    if ((self->priv->enabled_rules & MM_FILTER_RULE_NET) &&
        (g_strcmp0 (subsystem, "net") == 0)) {
        mm_obj_dbg (self, "(%s/%s) port allowed: net device", subsystem, name);
        return TRUE;
    }

    /* If this is a cdc-wdm device, we always allow it */
    if ((self->priv->enabled_rules & MM_FILTER_RULE_CDC_WDM) &&
        (g_strcmp0 (subsystem, "usb") == 0 || g_strcmp0 (subsystem, "usbmisc") == 0) &&
        (name && g_str_has_prefix (name, "cdc-wdm"))) {
        mm_obj_dbg (self, "(%s/%s) port allowed: cdc-wdm device", subsystem, name);
        return TRUE;
    }

    /* If this is a tty device, we may allow it */
    if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY) &&
        (g_strcmp0 (subsystem, "tty") == 0)) {
        const gchar *physdev_subsystem;
        const gchar *driver;

        /* Blacklist rules first */

        /* Ignore blacklisted tty devices. */
        if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_BLACKLIST) &&
            (mm_kernel_device_get_global_property_as_boolean (port, ID_MM_TTY_BLACKLIST))) {
            mm_obj_dbg (self, "(%s/%s): port filtered: tty is blacklisted", subsystem, name);
            return FALSE;
        }

        /* Is the device in the manual-only greylist? If so, return if this is an
         * automatic scan. */
        if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_MANUAL_SCAN_ONLY) &&
            (!manual_scan && mm_kernel_device_get_global_property_as_boolean (port, ID_MM_TTY_MANUAL_SCAN_ONLY))) {
            mm_obj_dbg (self, "(%s/%s): port filtered: tty probed only in manual scan", subsystem, name);
            return FALSE;
        }

        /* Mixed blacklist/whitelist rules */

        /* If the physdev is a 'platform' or 'pnp' device that's not whitelisted, ignore it */
        physdev_subsystem = mm_kernel_device_get_physdev_subsystem (port);
        if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_PLATFORM_DRIVER) &&
            (!g_strcmp0 (physdev_subsystem, "platform") ||
             !g_strcmp0 (physdev_subsystem, "pci") ||
             !g_strcmp0 (physdev_subsystem, "pnp") ||
             !g_strcmp0 (physdev_subsystem, "sdio"))) {
            mm_obj_dbg (self, "(%s/%s): port filtered: tty platform driver", subsystem, name);
            return FALSE;
        }

        /* Default allowed? */
        if (self->priv->enabled_rules & MM_FILTER_RULE_TTY_DEFAULT_ALLOWED) {
            mm_obj_dbg (self, "(%s/%s) port allowed", subsystem, name);
            return TRUE;
        }

        /* Whitelist rules last */

        /* If the TTY kernel driver is one expected modem kernel driver, allow it */
        driver = mm_kernel_device_get_driver (port);
        if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_DRIVER) &&
            (!g_strcmp0 (driver, "option1") ||
             !g_strcmp0 (driver, "qcserial") ||
             !g_strcmp0 (driver, "qcaux") ||
             !g_strcmp0 (driver, "nozomi") ||
             !g_strcmp0 (driver, "sierra"))) {
            mm_obj_dbg (self, "(%s/%s): port allowed: modem-specific kernel driver detected", subsystem, name);
            return TRUE;
        }

        /*
         * If the TTY kernel driver is cdc-acm and the interface is not
         * class=2/subclass=2/protocol=[1-6], forbidden.
         *
         * Otherwise, we'll require the modem to have more ports other
         * than the ttyACM one (see mm_filter_device_and_port()), because
         * there are lots of Arduino devices out there exposing a single
         * ttyACM port and wrongly claiming AT protocol support...
         *
         * Class definitions for Communication Devices 1.2
         * Communications Interface Class Control Protocol Codes:
         *     00h     | USB specification | No class specific protocol required
         *     01h     | ITU-T V.250       | AT Commands: V.250 etc
         *     02h     | PCCA-101          | AT Commands defined by PCCA-101
         *     03h     | PCCA-101          | AT Commands defined by PCCA-101 & Annex O
         *     04h     | GSM 7.07          | AT Commands defined by GSM 07.07
         *     05h     | 3GPP 27.07        | AT Commands defined by 3GPP 27.007
         *     06h     | C-S0017-0         | AT Commands defined by TIA for CDMA
         *     07h     | USB EEM           | Ethernet Emulation Model
         *     08h-FDh |                   | RESERVED (future use)
         *     FEh     |                   | External Protocol: Commands defined by Command Set Functional Descriptor
         *     FFh     | USB Specification | Vendor-specific
         */
        if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_ACM_INTERFACE) &&
            (!g_strcmp0 (driver, "cdc_acm")) &&
            ((mm_kernel_device_get_interface_class (port) != 2)    ||
             (mm_kernel_device_get_interface_subclass (port) != 2) ||
             (mm_kernel_device_get_interface_protocol (port) < 1)  ||
             (mm_kernel_device_get_interface_protocol (port) > 6))) {
            mm_obj_dbg (self, "(%s/%s): port filtered: cdc-acm interface is not AT-capable", subsystem, name);
            return FALSE;
        }

        /* Default forbidden? flag the port as maybe-forbidden, and go on */
        if (self->priv->enabled_rules & MM_FILTER_RULE_TTY_DEFAULT_FORBIDDEN) {
            g_object_set_data (G_OBJECT (port), FILTER_PORT_MAYBE_FORBIDDEN, GUINT_TO_POINTER (TRUE));
            return TRUE;
        }

        g_assert_not_reached ();
    }

    /* Otherwise forbidden */
    mm_obj_dbg (self, "(%s/%s) port filtered: forbidden port type", subsystem, name);
    return FALSE;
}

/*****************************************************************************/

static gboolean
device_has_net_port (MMDevice *device)
{
    GList *l;

    for (l = mm_device_peek_port_probe_list (device); l; l = g_list_next (l)) {
        if (!g_strcmp0 (mm_port_probe_get_port_subsys (MM_PORT_PROBE (l->data)), "net"))
            return TRUE;
    }
    return FALSE;
}

static gboolean
device_has_multiple_ports (MMDevice *device)
{
    return (g_list_length (mm_device_peek_port_probe_list (device)) > 1);
}

gboolean
mm_filter_device_and_port (MMFilter       *self,
                           MMDevice       *device,
                           MMKernelDevice *port)
{
    const gchar *subsystem;
    const gchar *name;
    const gchar *driver;

    /* If it wasn't flagged as maybe forbidden, there's nothing to do */
    if (!GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (port), FILTER_PORT_MAYBE_FORBIDDEN)))
        return TRUE;

    subsystem = mm_kernel_device_get_subsystem (port);
    name      = mm_kernel_device_get_name      (port);

    /* Check whether this device holds a NET port in addition to this TTY */
    if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_WITH_NET) &&
        device_has_net_port (device)) {
        mm_obj_dbg (self, "(%s/%s): port allowed: device also exports a net interface", subsystem, name);
        return TRUE;
    }

    /* Check whether this device holds any other port in addition to the ttyACM port */
    driver = mm_kernel_device_get_driver (port);
    if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_ACM_INTERFACE) &&
        (!g_strcmp0 (driver, "cdc_acm")) &&
        device_has_multiple_ports (device)) {
        mm_obj_dbg (self, "(%s/%s): port allowed: device exports multiple interfaces", subsystem, name);
        return TRUE;
    }

    mm_obj_dbg (self, "(%s/%s) port filtered: forbidden", subsystem, name);
    return FALSE;
}

/*****************************************************************************/
/* Use filter rule names as environment variables to control them on startup:
 *  - MM_FILTER_RULE_XXX=1 to explicitly enable the rule.
 *  - MM_FILTER_RULE_XXX=0 to explicitly disable the rule.
 */

static MMFilterRule
filter_rule_env_process (MMFilterRule enabled_rules)
{
    MMFilterRule  updated_rules = enabled_rules;
    GFlagsClass  *flags_class;
    guint         i;

    flags_class = g_type_class_ref (MM_TYPE_FILTER_RULE);

    for (i = 0; (1 << i) & MM_FILTER_RULE_ALL; i++) {
        GFlagsValue *flags_value;
        const gchar *env_value;

        flags_value = g_flags_get_first_value (flags_class, (1 << i));
        g_assert (flags_value);

        env_value = g_getenv (flags_value->value_name);
        if (!env_value)
            continue;

        if (g_str_equal (env_value, "0"))
            updated_rules &= ~(1 << i);
        else if (g_str_equal (env_value, "1"))
            updated_rules |= (1 << i);
    }

    g_type_class_unref (flags_class);

    return updated_rules;
}

/*****************************************************************************/

gboolean
mm_filter_check_rule_enabled (MMFilter     *self,
                              MMFilterRule  rule)
{
    return !!(self->priv->enabled_rules & rule);
}

/*****************************************************************************/

static gchar *
log_object_build_id (MMLogObject *_self)
{
    return g_strdup ("filter");
}

/*****************************************************************************/

/* If TTY rule enabled, either DEFAULT_ALLOWED or DEFAULT_FORBIDDEN must be set. */
#define VALIDATE_RULE_TTY(rules) (!(rules & MM_FILTER_RULE_TTY) || \
                                  ((rules & (MM_FILTER_RULE_TTY_DEFAULT_ALLOWED | MM_FILTER_RULE_TTY_DEFAULT_FORBIDDEN)) && \
                                   ((rules & (MM_FILTER_RULE_TTY_DEFAULT_ALLOWED | MM_FILTER_RULE_TTY_DEFAULT_FORBIDDEN)) != \
                                    (MM_FILTER_RULE_TTY_DEFAULT_ALLOWED | MM_FILTER_RULE_TTY_DEFAULT_FORBIDDEN))))

MMFilter *
mm_filter_new (MMFilterRule   enabled_rules,
               GError       **error)
{
    MMFilter     *self;
    MMFilterRule  updated_rules;

    /* The input enabled rules are coming from predefined filter profiles. */
    g_assert (VALIDATE_RULE_TTY (enabled_rules));
    updated_rules = filter_rule_env_process (enabled_rules);
    if (!VALIDATE_RULE_TTY (updated_rules)) {
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
                     "Invalid rules after processing envvars");
        return NULL;
    }

    self = g_object_new (MM_TYPE_FILTER,
                         MM_FILTER_ENABLED_RULES, updated_rules,
                         NULL);

#define RULE_ENABLED_STR(flag) ((self->priv->enabled_rules & flag) ? "yes" : "no")

    mm_obj_dbg (self, "created");
    mm_obj_dbg (self, "  explicit whitelist:         %s", RULE_ENABLED_STR (MM_FILTER_RULE_EXPLICIT_WHITELIST));
    mm_obj_dbg (self, "  explicit blacklist:         %s", RULE_ENABLED_STR (MM_FILTER_RULE_EXPLICIT_BLACKLIST));
    mm_obj_dbg (self, "  plugin whitelist:           %s", RULE_ENABLED_STR (MM_FILTER_RULE_PLUGIN_WHITELIST));
    mm_obj_dbg (self, "  virtual devices forbidden:  %s", RULE_ENABLED_STR (MM_FILTER_RULE_VIRTUAL));
    mm_obj_dbg (self, "  net devices allowed:        %s", RULE_ENABLED_STR (MM_FILTER_RULE_NET));
    mm_obj_dbg (self, "  cdc-wdm devices allowed:    %s", RULE_ENABLED_STR (MM_FILTER_RULE_CDC_WDM));
    if (self->priv->enabled_rules & MM_FILTER_RULE_TTY) {
        mm_obj_dbg (self, "  tty devices:");
        mm_obj_dbg (self, "      blacklist applied:        %s", RULE_ENABLED_STR (MM_FILTER_RULE_TTY_BLACKLIST));
        mm_obj_dbg (self, "      manual scan only applied: %s", RULE_ENABLED_STR (MM_FILTER_RULE_TTY_MANUAL_SCAN_ONLY));
        mm_obj_dbg (self, "      platform driver check:    %s", RULE_ENABLED_STR (MM_FILTER_RULE_TTY_PLATFORM_DRIVER));
        mm_obj_dbg (self, "      driver check:             %s", RULE_ENABLED_STR (MM_FILTER_RULE_TTY_DRIVER));
        mm_obj_dbg (self, "      cdc-acm interface check:  %s", RULE_ENABLED_STR (MM_FILTER_RULE_TTY_ACM_INTERFACE));
        mm_obj_dbg (self, "      with net check:           %s", RULE_ENABLED_STR (MM_FILTER_RULE_TTY_WITH_NET));
        if (self->priv->enabled_rules & MM_FILTER_RULE_TTY_DEFAULT_ALLOWED)
            mm_obj_dbg (self, "      default:                  allowed");
        else if (self->priv->enabled_rules & MM_FILTER_RULE_TTY_DEFAULT_FORBIDDEN)
            mm_obj_dbg (self, "      default:                  forbidden");
        else
            g_assert_not_reached ();
    } else
        mm_obj_dbg (self, "  tty devices:                no");

#undef RULE_ENABLED_STR

    return self;
}

static void
set_property (GObject      *object,
              guint         prop_id,
              const GValue *value,
              GParamSpec   *pspec)
{
    MMFilter *self = MM_FILTER (object);

    switch (prop_id) {
    case PROP_ENABLED_RULES:
        self->priv->enabled_rules = g_value_get_flags (value);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
get_property (GObject    *object,
              guint       prop_id,
              GValue     *value,
              GParamSpec *pspec)
{
    MMFilter *self = MM_FILTER (object);

    switch (prop_id) {
    case PROP_ENABLED_RULES:
        g_value_set_flags (value, self->priv->enabled_rules);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
mm_filter_init (MMFilter *self)
{
    self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, MM_TYPE_FILTER, MMFilterPrivate);
}

static void
finalize (GObject *object)
{
    MMFilter *self = MM_FILTER (object);

    g_clear_pointer (&self->priv->plugin_whitelist_vendor_ids, g_array_unref);
    g_clear_pointer (&self->priv->plugin_whitelist_product_ids, g_array_unref);
    g_list_free_full (self->priv->plugin_whitelist_tags, g_free);

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

static void
log_object_iface_init (MMLogObjectInterface *iface)
{
    iface->build_id = log_object_build_id;
}

static void
mm_filter_class_init (MMFilterClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);

    g_type_class_add_private (object_class, sizeof (MMFilterPrivate));

    /* Virtual methods */
    object_class->set_property = set_property;
    object_class->get_property = get_property;
    object_class->finalize     = finalize;

    g_object_class_install_property (
        object_class, PROP_ENABLED_RULES,
        g_param_spec_flags (MM_FILTER_ENABLED_RULES,
                            "Enabled rules",
                            "Mask of rules enabled in the filter",
                            MM_TYPE_FILTER_RULE,
                            MM_FILTER_RULE_NONE,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
}
