/*
 * DHCP Client Runner
 *
 * This test implements a DHCP client. It takes parameters via the command-line
 * and runs a DHCP client. It is mainly meant for testing, as such it allows
 * tweaking that an exported DHCP client should not provide.
 */

#include <assert.h>
#include <c-stdaux.h>
#include <errno.h>
#include <getopt.h>
#include <net/if.h>
#include <netinet/ether.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "n-dhcp4.h"
#include "n-dhcp4-private.h"
#include "test.h"

typedef struct Manager Manager;

enum {
        _MAIN_SUCCESS,
        MAIN_EXIT,
        MAIN_FAILED,
};

struct Manager {
        NDhcp4Client *client;
        NDhcp4ClientProbe *probe;
};

#define MANAGER_NULL(_x) {}

static struct ether_addr        main_arg_broadcast_mac = {};
static bool                     main_arg_broadcast_mac_set = false;
static uint8_t*                 main_arg_client_id = NULL;
static size_t                   main_arg_n_client_id = 0;
static int                      main_arg_ifindex = 0;
static struct in_addr           main_arg_requested_ip = { INADDR_ANY };
static long long int            main_arg_requested_lifetime = -1;
static uint8_t                  main_arg_requested_parameters[UINT8_MAX] = {};
static size_t                   main_arg_n_requested_parameters = 0;
static struct ether_addr        main_arg_mac = {};
static bool                     main_arg_mac_set = false;
static bool                     main_arg_request_broadcast = false;
static bool                     main_arg_test = false;

static Manager *manager_free(Manager *manager) {
        if (!manager)
                return NULL;

        n_dhcp4_client_probe_free(manager->probe);
        n_dhcp4_client_unref(manager->client);
        free(manager);

        return NULL;
}

static void manager_freep(Manager **manager) {
        manager_free(*manager);
}

static int manager_new(Manager **managerp) {
        _c_cleanup_(n_dhcp4_client_config_freep) NDhcp4ClientConfig *config = NULL;
        _c_cleanup_(manager_freep) Manager *manager = NULL;
        int r;

        manager = malloc(sizeof(*manager));
        if (!manager)
                return -ENOMEM;

        *manager = (Manager)MANAGER_NULL(*manager);

        r = n_dhcp4_client_config_new(&config);
        if (r)
                return r;

        n_dhcp4_client_config_set_broadcast_mac(config,
                                                &main_arg_broadcast_mac.ether_addr_octet[0],
                                                sizeof(main_arg_broadcast_mac.ether_addr_octet));
        n_dhcp4_client_config_set_mac(config,
                                      &main_arg_mac.ether_addr_octet[0],
                                      sizeof(main_arg_mac.ether_addr_octet));
        n_dhcp4_client_config_set_client_id(config,
                                            main_arg_client_id,
                                            main_arg_n_client_id);
        n_dhcp4_client_config_set_ifindex(config, main_arg_ifindex);
        n_dhcp4_client_config_set_request_broadcast(config, main_arg_request_broadcast);
        n_dhcp4_client_config_set_transport(config, N_DHCP4_TRANSPORT_ETHERNET);

        r = n_dhcp4_client_new(&manager->client, config);
        if (r)
                return r;

        *managerp = manager;
        manager = NULL;
        return 0;
}

static int manager_lease_get_dns(NDhcp4ClientLease *lease, struct in_addr *dns) {
        uint8_t *data;
        size_t n_data;
        int r;

        r = n_dhcp4_client_lease_query(lease, N_DHCP4_OPTION_DOMAIN_NAME_SERVER, &data, &n_data);
        if (r)
                return r;

        if (n_data < sizeof(dns->s_addr))
                return N_DHCP4_E_MALFORMED;

        memcpy(&dns->s_addr, data, sizeof(dns->s_addr));

        return 0;
}

static int manager_lease_get_router(NDhcp4ClientLease *lease, struct in_addr *router) {
        uint8_t *data;
        size_t n_data;
        int r;

        r = n_dhcp4_client_lease_query(lease, N_DHCP4_OPTION_ROUTER, &data, &n_data);
        if (r)
                return r;

        if (n_data < sizeof(router->s_addr))
                return N_DHCP4_E_MALFORMED;

        memcpy(&router->s_addr, data, sizeof(router->s_addr));

        return 0;
}

static int manager_lease_get_subnetmask(NDhcp4ClientLease *lease, struct in_addr *mask) {
        uint8_t *data;
        size_t n_data;
        int r;

        r = n_dhcp4_client_lease_query(lease, N_DHCP4_OPTION_SUBNET_MASK, &data, &n_data);
        if (r)
                return r;

        if (n_data != sizeof(mask->s_addr))
                return N_DHCP4_E_MALFORMED;

        memcpy(&mask->s_addr, data, sizeof(mask->s_addr));

        return 0;
}

static int manager_lease_get_prefix(NDhcp4ClientLease *lease, unsigned int *prefixp) {
        struct in_addr mask = {};
        unsigned int postfix;
        int r;

        r = manager_lease_get_subnetmask(lease, &mask);
        if (r)
                return r;

        postfix =__builtin_ctz(ntohl(mask.s_addr));
        c_assert(postfix <= 32);

        if (postfix < 32) {
                if ((~ntohl(mask.s_addr)) >> postfix != 0)
                        return N_DHCP4_E_MALFORMED;
        }

        *prefixp = 32 - postfix;
        return 0;
}

static int manager_check(Manager *manager, NDhcp4ClientLease *lease) {
        int r;

        r = n_dhcp4_client_lease_query(lease, N_DHCP4_OPTION_ROUTER, NULL, NULL);
        if (r) {
                fprintf(stderr, "No router\n");
                return r;
        }

        r = n_dhcp4_client_lease_query(lease, N_DHCP4_OPTION_SUBNET_MASK, NULL, NULL);
        if (r) {
                fprintf(stderr, "No subnet mask\n");
                return r;
        }

        return r;
}

static int manager_add(Manager *manager, NDhcp4ClientLease *lease) {
        char *p, ifname[IF_NAMESIZE + 1] = {};
        struct in_addr router = {}, yiaddr = {}, dns = {};
        unsigned int prefix;
        uint64_t lifetime;
        int r;

        n_dhcp4_client_lease_get_yiaddr(lease, &yiaddr);
        n_dhcp4_client_lease_get_lifetime(lease, &lifetime);

        r = manager_lease_get_router(lease, &router);
        if (r)
                return r;

        r = manager_lease_get_prefix(lease, &prefix);
        if (r)
                return r;

        p = if_indextoname(main_arg_ifindex, ifname);
        c_assert(p);

        if (lifetime == UINT64_MAX) {
                r = asprintf(&p, "ip addr add %s/%u dev %s preferred_lft forever valid_lft forever", inet_ntoa(yiaddr), prefix, ifname);
                c_assert(r >= 0);
        } else {
                r = asprintf(&p, "ip addr add %s/%u dev %s preferred_lft %llu valid_lft %llu", inet_ntoa(yiaddr), prefix, ifname, lifetime / 1000000000ULL, lifetime / 1000000000ULL);
                c_assert(r >= 0);
        }
        r = system(p);
        c_assert(r == 0);
        free(p);

        r = asprintf(&p, "ip route add %s/32 dev %s", inet_ntoa(router), ifname);
        c_assert(r >= 0);
        r = system(p);
        c_assert(r == 0);
        free(p);

        r = asprintf(&p, "ip route add default via %s dev %s", inet_ntoa(router), ifname);
        c_assert(r >= 0);
        r = system(p);
        c_assert(r == 0);
        free(p);

        r = manager_lease_get_dns(lease, &dns);
        if (r) {
                if (r != N_DHCP4_E_UNSET)
                        return r;
        } else {
                fprintf(stderr, "DNS: %s\n", inet_ntoa(dns));
        }

        return 0;
}

static int manager_dispatch(Manager *manager) {
        NDhcp4ClientEvent *event;
        int r;

        r = n_dhcp4_client_dispatch(manager->client);
        if (r) {
                if (r != N_DHCP4_E_PREEMPTED) {
                        /*
                         * We are level-triggered, so we do not need to react
                         * to preemption. We simply continue the mainloop.
                         */
                        return r;
                }
        }

        for (;;) {
                r = n_dhcp4_client_pop_event(manager->client, &event);
                if (r)
                        return r;

                if (!event)
                        break;

                switch (event->event) {
                case N_DHCP4_CLIENT_EVENT_DOWN:
                        fprintf(stderr, "DOWN\n");

                        break;

                case N_DHCP4_CLIENT_EVENT_OFFER:
                        fprintf(stderr, "OFFER\n");

                        r = manager_check(manager, event->granted.lease);
                        if (r) {
                                if (r == N_DHCP4_E_UNSET) {
                                        fprintf(stderr, "Missing mandatory option, ignoring lease.\n");
                                } else {
                                        return r;
                                }
                        } else {
                                r = n_dhcp4_client_lease_select(event->offer.lease);
                                if (r)
                                        return r;
                        }

                        break;

                case N_DHCP4_CLIENT_EVENT_GRANTED:
                        fprintf(stderr, "GRANTED\n");

                        r = manager_add(manager, event->granted.lease);
                        if (r) {
                                if (r == N_DHCP4_E_UNSET) {
                                        fprintf(stderr, "Missing mandatory option, declining lease.\n");

                                        r = n_dhcp4_client_lease_decline(event->granted.lease, "Missing mandatory option.");
                                        if (r)
                                                return r;
                                } else {
                                        return r;
                                }
                        } else {
                                r = n_dhcp4_client_lease_accept(event->granted.lease);
                                if (r)
                                        return r;
                        }

                        break;

                case N_DHCP4_CLIENT_EVENT_RETRACTED:
                        fprintf(stderr, "RETRACTED\n");

                        break;

                case N_DHCP4_CLIENT_EVENT_EXTENDED:
                        fprintf(stderr, "EXTENDED\n");

                        break;

                case N_DHCP4_CLIENT_EVENT_EXPIRED:
                        fprintf(stderr, "EXPIRED\n");

                        break;

                case N_DHCP4_CLIENT_EVENT_CANCELLED:
                        fprintf(stderr, "CANCELLED\n");

                        break;

                default:
                        fprintf(stderr, "Unexpected event: %u\n", event->event);

                        break;
                }
        }

        return 0;
}

static int manager_run(Manager *manager) {
        _c_cleanup_(n_dhcp4_client_probe_config_freep) NDhcp4ClientProbeConfig *config = NULL;
        int r;

        r = n_dhcp4_client_probe_config_new(&config);
        if (r)
                return r;

        /*
         * Let's speed up our tests, while still making sure the code-path
         * for the deferrment is actually tested (so don't set it to zero).
         */
        n_dhcp4_client_probe_config_set_start_delay(config, 10);

        n_dhcp4_client_probe_config_set_requested_ip(config, main_arg_requested_ip);

        if (main_arg_n_requested_parameters > 0) {
                for (unsigned int i = 0; i < main_arg_n_requested_parameters; ++i)
                        n_dhcp4_client_probe_config_request_option(config, main_arg_requested_parameters[i]);
        } else {
                n_dhcp4_client_probe_config_request_option(config, N_DHCP4_OPTION_ROUTER);
                n_dhcp4_client_probe_config_request_option(config, N_DHCP4_OPTION_SUBNET_MASK);
                n_dhcp4_client_probe_config_request_option(config, N_DHCP4_OPTION_DOMAIN_NAME_SERVER);
        }

        if (main_arg_requested_lifetime >= 0) {
                uint32_t lifetime = ntohl(main_arg_requested_lifetime);

                r = n_dhcp4_client_probe_config_append_option(config, N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME, &lifetime, sizeof(lifetime));
                if (r)
                        return r;
        }

        r = n_dhcp4_client_probe(manager->client, &manager->probe, config);
        if (r)
                return r;

        /*
         * The test-suite runs this with the --test argument. So far, we do not
         * perform any fancy runtime tests, but simply exit the main-loop
         * immediately. We can add more elaborate tests in the future.
         */
        if (main_arg_test)
                return 0;

        for (;;) {
                struct pollfd pfds[] = {
                        { .fd = -1, .events = POLLIN },
                };
                size_t i;
                int n;

                n_dhcp4_client_get_fd(manager->client, &pfds[0].fd);

                n = poll(pfds, sizeof(pfds) / sizeof(*pfds), -1);
                if (n < 0)
                        return -errno;

                for (i = 0; i < (size_t)n; ++i) {
                        if (pfds[i].revents & ~POLLIN)
                                return -ENOTRECOVERABLE;

                        if (!(pfds[i].revents & POLLIN))
                                continue;

                        r = manager_dispatch(manager);
                        if (r)
                                return r;
                }
        }

        return 0;
}

static int run(void) {
        _c_cleanup_(manager_freep) Manager *manager = NULL;
        int r;

        r = manager_new(&manager);
        if (r)
                return r;

        return manager_run(manager);
}

static void print_help(void) {
        printf("%s [GLOBALS...] ...\n\n"
               "DHCP Test Client\n\n"
               "  -h --help                            Show this help\n"
               "     --test                            Run as part of the test suite\n"
               "     --ifindex IDX                     Index of interface to run on\n"
               "     --mac HEX                         Hardware address to use\n"
               "     --broadcast-mac HEX               Broadcast hardware address to use\n"
               "     --requested-ip IP                 Requested IP address\n"
               "     --requested-lifetime SECS         Requested lease lifetime in seconds\n"
               "     --requested-parameters P1,P2,...  Requested parameters\n"
               "     --client-id HEX                   Client Identifier to use\n"
               , program_invocation_short_name);
}

static int setup_test(void) {
        test_setup();

        /* --broadcast-mac */
        {
                main_arg_broadcast_mac_set = true;
        }

        /* --ifindex */
        {
                main_arg_ifindex = 1;
        }

        /* --mac */
        {
                main_arg_mac_set = true;
        }

        return 0;
}

static int parse_hexstr(const char *in, uint8_t **outp, size_t *n_outp) {
        _c_cleanup_(c_freep) uint8_t *out = NULL;
        size_t i, n_in, n_out;

        n_in = strlen(in);
        n_out = (n_in + 1) / 2;

        out = malloc(n_out);
        if (!out)
                return -ENOMEM;

        for (i = 0; i < n_in; ++i) {
                uint8_t v = 0;

                switch (in[i]) {
                case '0'...'9':
                        v = in[i] - '0';
                        break;
                case 'a'...'f':
                        v = in[i] - 'a' + 0xa;
                        break;
                case 'A'...'F':
                        v = in[i] - 'A' + 0xa;
                        break;
                }

                if (i % 2) {
                        out[i / 2] <<= 4;
                        out[i / 2] |= v;
                } else {
                        out[i / 2] = v;
                }
        }

        *outp = out;
        out = NULL;
        *n_outp = n_out;
        return 0;
}

static int parse_argv(int argc, char **argv) {
        enum {
                _ARG_0 = 0x100,
                ARG_BROADCAST_MAC,
                ARG_CLIENT_ID,
                ARG_IFINDEX,
                ARG_MAC,
                ARG_REQUEST_BROADCAST,
                ARG_REQUESTED_IP,
                ARG_REQUESTED_LIFETIME,
                ARG_REQUESTED_PARAMETERS,
                ARG_TEST,
        };
        static const struct option options[] = {
                { "help",                       no_argument,            NULL,   'h'                             },
                { "broadcast-mac",              required_argument,      NULL,   ARG_BROADCAST_MAC               },
                { "client-id",                  required_argument,      NULL,   ARG_CLIENT_ID                   },
                { "ifindex",                    required_argument,      NULL,   ARG_IFINDEX                     },
                { "mac",                        required_argument,      NULL,   ARG_MAC                         },
                { "request-broadcast",          no_argument,            NULL,   ARG_REQUEST_BROADCAST           },
                { "requested-ip",               required_argument,      NULL,   ARG_REQUESTED_IP                },
                { "requested-lifetime",         required_argument,      NULL,   ARG_REQUESTED_LIFETIME          },
                { "requested-parameters",       required_argument,      NULL,   ARG_REQUESTED_PARAMETERS        },
                { "test",                       no_argument,            NULL,   ARG_TEST                        },
                {}
        };
        struct ether_addr *addr;
        long long int lli;
        size_t n;
        void *t;
        int r, c;

        /*
         * Most of the argument-parsers are short-and-dirty hacks to make the
         * conversions work. This is sufficient for a test-client, but needs
         * proper error-checking if done outside of tests.
         */

        while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) {
                switch (c) {
                case 'h':
                        print_help();
                        return MAIN_EXIT;

                case ARG_BROADCAST_MAC:
                        addr = ether_aton_r(optarg, &main_arg_broadcast_mac);
                        if (!addr) {
                                fprintf(stderr,
                                        "%s: invalid broadcast mac address -- '%s'\n",
                                        program_invocation_name,
                                        optarg);
                                return MAIN_FAILED;
                        }

                        main_arg_broadcast_mac_set = true;
                        break;

                case ARG_CLIENT_ID:
                        r = parse_hexstr(optarg, (uint8_t **)&t, &n);
                        if (r)
                                return r;

                        free(main_arg_client_id);
                        main_arg_client_id = t;
                        main_arg_n_client_id = n;
                        break;

                case ARG_IFINDEX:
                        main_arg_ifindex = atoi(optarg);
                        break;

                case ARG_MAC:
                        addr = ether_aton_r(optarg, &main_arg_mac);
                        if (!addr) {
                                fprintf(stderr,
                                        "%s: invalid mac address -- '%s'\n",
                                        program_invocation_name,
                                        optarg);
                                return MAIN_FAILED;
                        }

                        main_arg_mac_set = true;
                        break;

                case ARG_REQUEST_BROADCAST:
                        main_arg_request_broadcast = true;
                        break;

                case ARG_REQUESTED_IP:
                        r = inet_pton(AF_INET, optarg, &main_arg_requested_ip);
                        if (r != 1) {
                                fprintf(stderr,
                                        "%s: invalid requested IP -- '%s'\n",
                                        program_invocation_name,
                                        optarg);
                                return MAIN_FAILED;
                        }
                        break;

                case ARG_REQUESTED_LIFETIME:
                        lli = atoll(optarg);
                        if (lli < 0 || lli > UINT32_MAX) {
                                fprintf(stderr,
                                        "%s: invalid requested lifetime -- '%s'\n",
                                        program_invocation_name,
                                        optarg);
                                return MAIN_FAILED;
                        }
                        main_arg_requested_lifetime = lli;
                        break;

                case ARG_REQUESTED_PARAMETERS:
                        for (const char *param = optarg; param; param = strchr(param, ',') ? strchr(param, ',')  + 1 : NULL) {
                                c_assert(main_arg_n_requested_parameters <= UINT8_MAX);

                                lli = atoll(param);
                                if (lli < 0 || lli > UINT8_MAX) {
                                        fprintf(stderr,
                                                "%s: invalid requested parameters -- '%s'\n",
                                                program_invocation_name,
                                                optarg);
                                        return MAIN_FAILED;
                                }
                                main_arg_requested_parameters[main_arg_n_requested_parameters++] = lli;
                        }
                        break;

                case ARG_TEST:
                        r = setup_test();
                        if (r)
                                return r;

                        main_arg_test = true;
                        break;

                case '?':
                        /* getopt_long() prints warning */
                        return MAIN_FAILED;

                default:
                        return -ENOTRECOVERABLE;
                }
        }

        if (optind != argc) {
                fprintf(stderr,
                        "%s: invalid arguments -- '%s'\n",
                        program_invocation_name,
                        argv[optind]);
                return MAIN_FAILED;
        }

        if (!main_arg_broadcast_mac_set ||
            !main_arg_ifindex ||
            !main_arg_mac_set) {
                fprintf(stderr,
                        "%s: required arguments: broadcast-mac, ifindex, mac\n",
                        program_invocation_name);
                return MAIN_FAILED;
        }

        return 0;
}

int main(int argc, char **argv) {
        int r;

        /* --client-id */
        {
                uint8_t *b;
                size_t n;

                n = strlen("client-id");
                b = malloc(n);
                c_assert(b);
                memcpy(b, "client-id", n);

                free(main_arg_client_id);
                main_arg_client_id = b;
                main_arg_n_client_id = n;
        }

        r = parse_argv(argc, argv);
        if (r)
                goto exit;

        r = run();

exit:
        if (r == MAIN_EXIT) {
                r = 0;
        } else if (r < 0) {
                errno = -r;
                fprintf(stderr, "Failed with system errno %d: %m\n", r);
                r = 127;
        } else if (r > 0) {
                fprintf(stderr, "Failed with internal error %d\n", r);
        }

        free(main_arg_client_id);

        return r;
}
