/*
 * Ouroboros - Copyright (C) 2016 - 2021
 *
 * Bootstrap IPC Processes
 *
 *    Dimitri Staessens <dimitri@ouroboros.rocks>
 *    Sander Vrijders   <sander@ouroboros.rocks>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided
 * with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its
 * contributors may be used to endorse or promote products derived
 * from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <ouroboros/irm.h>

#include "irm_ops.h"
#include "irm_utils.h"

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#ifdef __FreeBSD__
#include <sys/socket.h>
#endif

#define UNICAST                "unicast"
#define BROADCAST              "broadcast"
#define UDP                    "udp"
#define ETH_LLC                "eth-llc"
#define ETH_DIX                "eth-dix"
#define LOCAL                  "local"

#define MD5                    "MD5"
#define SHA3_224               "SHA3_224"
#define SHA3_256               "SHA3_256"
#define SHA3_384               "SHA3_384"
#define SHA3_512               "SHA3_512"

#define DEFAULT_ADDR_SIZE      4
#define DEFAULT_EID_SIZE       8
#define DEFAULT_DDNS           0
#define DEFAULT_TTL            60
#define DEFAULT_ADDR_AUTH      ADDR_AUTH_FLAT_RANDOM
#define DEFAULT_ROUTING        ROUTING_LINK_STATE
#define DEFAULT_CONG_AVOID     CA_MB_ECN
#define DEFAULT_HASH_ALGO      DIR_HASH_SHA3_256
#define DEFAULT_ETHERTYPE      0xA000
#define DEFAULT_UDP_PORT       0x0D6B /* 3435 */

#define FLAT_RANDOM_ADDR_AUTH  "flat"
#define LINK_STATE_ROUTING     "link_state"
#define LINK_STATE_LFA_ROUTING "lfa"
#define LINK_STATE_ECM_ROUTING "ecmp"
#define NONE_CA                "none"
#define MB_ECN_CA              "mb-ecn"

static void usage(void)
{
        /* FIXME: Add ipcp_config stuff. */
        printf("Usage: irm ipcp bootstrap\n"
               "                name <ipcp name>\n"
               "                layer <layer name>\n"
               "                [type [TYPE]]\n"
               "where TYPE = {" UNICAST " " BROADCAST " " LOCAL " "
               UDP " " ETH_LLC " " ETH_DIX "},\n\n"
               "if TYPE == " UNICAST "\n"
               "                [addr <address size> (default: %d)]\n"
               "                [eid <eid size> (default: %d)]\n"
               "                [ttl (max time-to-live value, default: %d)]\n"
               "                [addr_auth <ADDRESS_POLICY> (default: %s)]\n"
               "                [routing <ROUTING_POLICY> (default: %s)]\n"
               "                [congestion <CONG_POLICY> (default: %s)]\n"
               "                [hash [ALGORITHM] (default: %s)]\n"
               "                [autobind]\n"
               "where ADDRESS_POLICY = {" FLAT_RANDOM_ADDR_AUTH "}\n"
               "      ROUTING_POLICY = {" LINK_STATE_ROUTING " "
               LINK_STATE_LFA_ROUTING " " LINK_STATE_ECM_ROUTING "}\n"
               "      CONG_POLICY = {" NONE_CA " " MB_ECN_CA "}\n"
               "      ALGORITHM = {" SHA3_224 " " SHA3_256 " "
               SHA3_384 " " SHA3_512 "}\n\n"
               "if TYPE == " UDP "\n"
               "                ip <IP address in dotted notation>\n"
               "                [port <UDP port> (default: %d)]\n"
               "                [dns <DDNS IP address in dotted notation>"
               " (default: none)]\n\n"
               "if TYPE == " ETH_LLC "\n"
               "                dev <interface name>\n"
               "                [hash [ALGORITHM] (default: %s)]\n"
               "where ALGORITHM = {" SHA3_224 " " SHA3_256 " "
               SHA3_384 " " SHA3_512 "}\n\n"
               "if TYPE == " ETH_DIX "\n"
               "                dev <interface name>\n"
               "                [ethertype <ethertype> (default: 0x%4X)]\n"
               "                [hash [ALGORITHM] (default: %s)]\n"
               "where ALGORITHM = {" SHA3_224 " " SHA3_256 " "
               SHA3_384 " " SHA3_512 "}\n\n"
               "if TYPE == " LOCAL "\n"
               "                [hash [ALGORITHM] (default: %s)]\n"
               "where ALGORITHM = {" SHA3_224 " " SHA3_256 " "
               SHA3_384 " " SHA3_512 "}\n\n"
               "if TYPE == " BROADCAST "\n"
               "                [autobind]\n\n",
               DEFAULT_ADDR_SIZE, DEFAULT_EID_SIZE, DEFAULT_TTL,
               FLAT_RANDOM_ADDR_AUTH, LINK_STATE_ROUTING, MB_ECN_CA,
               SHA3_256, DEFAULT_UDP_PORT, SHA3_256, 0xA000, SHA3_256,
               SHA3_256);
}

int do_bootstrap_ipcp(int     argc,
                      char ** argv)
{
        char *              ipcp           = NULL;
        pid_t               pid            = -1;
        struct ipcp_config  conf;
        uint8_t             addr_size      = DEFAULT_ADDR_SIZE;
        uint8_t             eid_size       = DEFAULT_EID_SIZE;
        uint8_t             max_ttl        = DEFAULT_TTL;
        enum pol_addr_auth  addr_auth_type = DEFAULT_ADDR_AUTH;
        enum pol_routing    routing_type   = DEFAULT_ROUTING;
        enum pol_dir_hash   hash_algo      = DEFAULT_HASH_ALGO;
        enum pol_cong_avoid cong_avoid     = DEFAULT_CONG_AVOID;
        uint32_t            ip_addr        = 0;
        uint32_t            dns_addr       = DEFAULT_DDNS;
        char *              ipcp_type      = NULL;
        enum ipcp_type      type           = IPCP_INVALID;
        char *              layer          = NULL;
        char *              dev            = NULL;
        uint16_t            ethertype      = DEFAULT_ETHERTYPE;
        struct ipcp_info *  ipcps;
        ssize_t             len            = 0;
        int                 i              = 0;
        bool                autobind       = false;
        int                 cargs;
        int                 port           = DEFAULT_UDP_PORT;

        while (argc > 0) {
                cargs = 2;
                if (matches(*argv, "type") == 0) {
                        ipcp_type = *(argv + 1);
                } else if (matches(*argv, "layer") == 0) {
                        layer = *(argv + 1);
                } else if (matches(*argv, "name") == 0) {
                        ipcp = *(argv + 1);
                } else if (matches(*argv, "hash") == 0) {
                        if (strcmp(*(argv + 1), SHA3_224) == 0)
                                hash_algo = DIR_HASH_SHA3_224;
                        else if (strcmp(*(argv + 1), SHA3_256) == 0)
                                hash_algo = DIR_HASH_SHA3_256;
                        else if (strcmp(*(argv + 1), SHA3_384) == 0)
                                hash_algo = DIR_HASH_SHA3_384;
                        else if (strcmp(*(argv + 1), SHA3_512) == 0)
                                hash_algo = DIR_HASH_SHA3_512;
                        else
                                goto unknown_param;
                } else if (matches(*argv, "ip") == 0) {
                        if (inet_pton (AF_INET, *(argv + 1), &ip_addr) != 1)
                                goto unknown_param;
                } else if (matches(*argv, "dns") == 0) {
                        if (inet_pton(AF_INET, *(argv + 1), &dns_addr) != 1)
                                goto unknown_param;
                } else if (matches(*argv, "device") == 0) {
                        dev = *(argv + 1);
                } else if (matches(*argv, "ethertype") == 0) {
                        /* NOTE: We might do some more checks on strtol. */
                        if (matches(*(argv + 1), "0x") == 0)
                                ethertype = strtol(*(argv + 1), NULL, 0);
                        else
                                ethertype = strtol(*(argv + 1), NULL, 16);
                        if (ethertype < 0x0600 || ethertype == 0xFFFF) {
                                printf("Invalid Ethertype: \"%s\".\n"
                                       "Recommended range: 0xA000-0xEFFF.\n",
                                       *(argv + 1));
                                return -1;
                        }
                } else if (matches(*argv, "addr") == 0) {
                        addr_size = atoi(*(argv + 1));
                } else if (matches(*argv, "eid") == 0) {
                        eid_size = atoi(*(argv + 1));
                } else if (matches(*argv, "ttl") == 0) {
                        max_ttl = atoi(*(argv + 1));
                } else if (matches(*argv, "port") == 0) {
                        port = atoi(*(argv + 1));
                } else if (matches(*argv, "autobind") == 0) {
                        autobind = true;
                        cargs = 1;
                } else if (matches(*argv, "addr_auth") == 0) {
                        if (strcmp(FLAT_RANDOM_ADDR_AUTH, *(argv + 1)) == 0)
                                addr_auth_type = ADDR_AUTH_FLAT_RANDOM;
                        else
                                goto unknown_param;
                } else if (matches(*argv, "routing") == 0) {
                        if (strcmp(LINK_STATE_ROUTING, *(argv + 1)) == 0)
                                routing_type = ROUTING_LINK_STATE;
                        else if (strcmp(LINK_STATE_LFA_ROUTING,
                                        *(argv + 1)) == 0)
                                routing_type = ROUTING_LINK_STATE_LFA;
                        else if (strcmp(LINK_STATE_ECM_ROUTING,
                                        *(argv + 1)) == 0)
                                routing_type = ROUTING_LINK_STATE_ECMP;
                        else
                                goto unknown_param;
                } else if (matches(*argv, "congestion") == 0) {
                        if (strcmp(NONE_CA, *(argv + 1)) == 0)
                                cong_avoid = CA_NONE;
                        else if (strcmp(MB_ECN_CA,
                                        *(argv + 1)) == 0)
                                cong_avoid = CA_MB_ECN;
                        else
                                goto unknown_param;
                } else {
                        printf("Unknown option: \"%s\".\n", *argv);
                        return -1;
                }

                argc -= cargs;
                argv += cargs;
        }

        if (ipcp == NULL || layer == NULL) {
                usage();
                return -1;
        }

        len = irm_list_ipcps(&ipcps);
        for (i = 0; i < len; i++) {
                if (wildcard_match(ipcps[i].name, ipcp) == 0) {
                        pid = ipcps[i].pid;
                        break;
                }
        }

        if (ipcp_type != NULL) {
                if (strcmp(ipcp_type, UNICAST) == 0)
                        type = IPCP_UNICAST;
                else if (strcmp(ipcp_type, BROADCAST) == 0)
                        type = IPCP_BROADCAST;
                else if (strcmp(ipcp_type, UDP) == 0)
                        type = IPCP_UDP;
                else if (strcmp(ipcp_type, ETH_LLC) == 0)
                        type = IPCP_ETH_LLC;
                else if (strcmp(ipcp_type, ETH_DIX) == 0)
                        type = IPCP_ETH_DIX;
                else if (strcmp(ipcp_type, LOCAL) == 0)
                        type = IPCP_LOCAL;
                else goto fail_usage;
        }

        if (pid == -1) {
                if (ipcp_type == NULL) {
                        printf("No IPCPs matching %s found.\n\n", ipcp);
                        goto fail;
                }

                pid = irm_create_ipcp(ipcp, type);
                if (pid < 0)
                        goto fail;
                free(ipcps);
                len = irm_list_ipcps(&ipcps);
        }

        for (i = 0; i < len; i++) {
                if (wildcard_match(ipcps[i].name, ipcp) == 0) {
                        pid = ipcps[i].pid;
                        if (ipcp_type != NULL && type != ipcps[i].type) {
                                printf("Types do not match.\n\n");
                                goto fail;
                        }
                        conf.type = ipcps[i].type;

                        if (autobind && (conf.type != IPCP_UNICAST &&
                                         conf.type != IPCP_BROADCAST)) {
                                printf("Can not bind this IPCP type,"
                                       "autobind disabled.\n\n");
                                autobind = false;
                        }

                        if (strlen(layer) > LAYER_NAME_SIZE) {
                                printf("Layer name too big.\n\n");
                                goto fail_usage;
                        }

                        strcpy(conf.layer_info.layer_name, layer);
                        if (conf.type != IPCP_UDP)
                                conf.layer_info.dir_hash_algo = hash_algo;

                        switch (conf.type) {
                        case IPCP_UNICAST:
                                conf.addr_size      = addr_size;
                                conf.eid_size       = eid_size;
                                conf.max_ttl        = max_ttl;
                                conf.addr_auth_type = addr_auth_type;
                                conf.routing_type   = routing_type;
                                conf.cong_avoid     = cong_avoid;
                                break;
                        case IPCP_UDP:
                                if (ip_addr == 0)
                                        goto fail_usage;
                                conf.ip_addr  = ip_addr;
                                conf.dns_addr = dns_addr;
                                conf.port     = port;
                                break;
                        case IPCP_ETH_LLC:
                                if (dev == NULL)
                                        goto fail_usage;
                                conf.dev = dev;
                                break;
                        case IPCP_ETH_DIX:
                                if (dev == NULL)
                                        goto fail_usage;
                                conf.dev = dev;
                                conf.ethertype = ethertype;
                                break;
                        case IPCP_BROADCAST:
                                /* FALLTHRU */
                        case IPCP_LOCAL:
                                break;
                        default:
                                assert(false);
                                break;
                        }

                        if (autobind && irm_bind_process(pid, ipcp)) {
                                printf("Failed to bind %d to %s.\n", pid, ipcp);
                                goto fail;
                        }

                        if (autobind && irm_bind_process(pid, layer)) {
                                printf("Failed to bind %d to %s.\n",
                                       pid, layer);
                                irm_unbind_process(pid, ipcp);
                                goto fail;
                        }

                        if (irm_bootstrap_ipcp(pid, &conf)) {
                                if (autobind) {
                                        irm_unbind_process(pid, ipcp);
                                        irm_unbind_process(pid, layer);
                                }
                                goto fail;
                        }
                }
        }

        free(ipcps);

        return 0;

 unknown_param:
        printf("Unknown parameter for %s: \"%s\".\n", *argv, *(argv + 1));
        return -1;

 fail_usage:
        usage();
 fail:
        free(ipcps);
        return -1;
}