/*
 * DHCP4 Client Leases
 *
 * This implements the public API wrapping DHCP4 client leases. A lease object
 * consists of the information given to us from the server, together with the
 * timestamp recording the start of the validity of the lease.
 *
 * A probe may yield many OFFERS, each of which contains a lease object. One of
 * these offers may be SELECTED, which implicitly rejects all the others.
 * The server may then ACK or NAK the lease which tells us whether or not we
 * are permitted to start using it. Once an ACK has been received, we can
 * configure the address, and only then can we SELECT the lease. If we
 * determine that the offered lease was not appropriate after all we
 * may DECLINE it instead.
 */

#include <assert.h>
#include <c-list.h>
#include <c-stdaux.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include "n-dhcp4.h"
#include "n-dhcp4-private.h"

/*
 * Compute the absolute timeouts from an incoming message. A message contains relative timeouts and the userdata
 * of the incoming message is set to the offset we must apply to get the absolute values.
 *
 * The special value UINT64_MAX is returned to indicate no or infinite timeouts. In case the given timeouts
 * are invalid relative to each other, we recompute T1 and/or T2 to take their default values. Later timeouts
 * take predecende above earlier ones (T1 is adjusted if it conflicts with T2, etc).
 */
static int n_dhcp4_incoming_get_timeouts(NDhcp4Incoming *message, uint64_t *t1p, uint64_t *t2p, uint64_t *lifetimep) {
        uint64_t lifetime, t2, t1;
        uint32_t u32;
        int r;

        r = n_dhcp4_incoming_query_lifetime(message, &u32);
        if (r == N_DHCP4_E_UNSET) {
                lifetime = UINT64_MAX;
        } else if (r) {
                return r;
        } else if (u32 == UINT32_MAX) {
                lifetime = UINT64_MAX;
        } else {
                lifetime = u32 * (1000000000ULL);
        }

        r = n_dhcp4_incoming_query_t2(message, &u32);
        if (r == N_DHCP4_E_UNSET) {
                if (lifetime == UINT64_MAX)
                        t2 = UINT64_MAX;
                else
                        t2 = (lifetime * 7) / 8;
        } else if (r) {
                return r;
        } else {
                if (u32 == UINT32_MAX)
                        t2 = UINT64_MAX;
                else
                        t2 = u32 * (1000000000ULL);

                if (t2 > lifetime)
                        t2 = (lifetime * 7) / 8;
        }

        r = n_dhcp4_incoming_query_t1(message, &u32);
        if (r == N_DHCP4_E_UNSET) {
                if (t2 == UINT64_MAX)
                        t1 = UINT64_MAX;
                else
                        t1 = (t2 * 4) / 7;
        } else if (r) {
                return r;
        } else {
                if (u32 == UINT32_MAX)
                        t1 = UINT64_MAX;
                else
                        t1 = u32 * (1000000000ULL);

                if (t1 > t2)
                        t1 = (t2 * 4) / 7;
        }

        if (lifetime != UINT64_MAX)
                lifetime += message->userdata.base_time;
        if (t2 != UINT64_MAX)
                t2 += message->userdata.base_time;
        if (t1 != UINT64_MAX)
                t1 += message->userdata.base_time;

        *lifetimep = lifetime;
        *t2p = t2;
        *t1p = t1;
        return 0;
}

/**
 * n_dhcp4_client_lease_new() - allocate new client lease object
 * @leasep:                     output argument for new client lease object
 * @message:                    incoming message representing the lease
 *
 * This creates a new client lease object. Client lease objects are simple
 * wrappers around an incoming message representing a lease.
 *
 * Return: 0 on success, negative error code on failure.
 */
int n_dhcp4_client_lease_new(NDhcp4ClientLease **leasep, NDhcp4Incoming *message) {
        _c_cleanup_(n_dhcp4_client_lease_unrefp) NDhcp4ClientLease *lease = NULL;
        int r;

        c_assert(leasep);

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

        *lease = (NDhcp4ClientLease)N_DHCP4_CLIENT_LEASE_NULL(*lease);

        r = n_dhcp4_incoming_get_timeouts(message, &lease->t1, &lease->t2, &lease->lifetime);
        if (r)
                return r;

        lease->message = message;
        *leasep = lease;
        lease = NULL;
        return 0;
}

static void n_dhcp4_client_lease_free(NDhcp4ClientLease *lease) {
        n_dhcp4_client_lease_unlink(lease);
        n_dhcp4_incoming_free(lease->message);
        free(lease);
}

/**
 * n_dhcp4_client_lease_ref() - reference client lease
 * @lease:                      the client lease object to reference
 *
 * Take a new reference to a client lease.
 *
 * Return: the lease.
 */
_c_public_ NDhcp4ClientLease *n_dhcp4_client_lease_ref(NDhcp4ClientLease *lease) {
        if (lease)
                ++lease->n_refs;
        return lease;
}

/**
 * n_dhcp4_client_lease_unref() - dereference client lease
 * @lease:                      the client lease object to dereference
 *
 * Relase a reference to a client lease.
 *
 * Return: NULL.
 */
_c_public_ NDhcp4ClientLease *n_dhcp4_client_lease_unref(NDhcp4ClientLease *lease) {
        if (lease && !--lease->n_refs)
                n_dhcp4_client_lease_free(lease);
        return NULL;
}

/**
 * n_dhcp4_client_lease_link() - link lease into probe
 * @lease:                      the lease to operate on
 * @probe:                      the probe to link the lease into
 *
 * Associate a lease with a probe. The lease may not already be linked.
 */
void n_dhcp4_client_lease_link(NDhcp4ClientLease *lease, NDhcp4ClientProbe *probe) {
        c_assert(!lease->probe);
        c_assert(!c_list_is_linked(&lease->probe_link));

        lease->probe = probe;
        c_list_link_tail(&probe->lease_list, &lease->probe_link);
}

/**
 * n_dhcp4_client_lease_unlink() - unlinke lease from its probe
 * @lease:                      the lease to operate on
 *
 * Dissassociate a lease from a probe if it is associated with one. Otherwise,
 * this is a noop.
 */
void n_dhcp4_client_lease_unlink(NDhcp4ClientLease *lease) {
        lease->probe = NULL;
        c_list_unlink(&lease->probe_link);
}

/**
 * n_dhcp4_client_lease_get_yiaddr() - get the IP address
 * @lease:                      the lease to operate on
 * @yiaddr:                     return argument for the IP address
 *
 * Gets the IP address contained in the lease. Or INADDR_ANY if the lease
 * does not contain an IP address.
 */
_c_public_ void n_dhcp4_client_lease_get_yiaddr(NDhcp4ClientLease *lease, struct in_addr *yiaddr) {
        NDhcp4Header *header = n_dhcp4_incoming_get_header(lease->message);

        yiaddr->s_addr = header->yiaddr;
}

/**
 * n_dhcp4_client_lease_get_siaddr() - get the server IP address
 * @lease:                      the lease to operate on
 * @siaddr:                     return argument for the IP address
 *
 * Gets the server IP address contained in the lease. Or INADDR_ANY if the
 * lease does not contain an IP address.
 */
_c_public_ void n_dhcp4_client_lease_get_siaddr(NDhcp4ClientLease *lease, struct in_addr *siaddr) {
        NDhcp4Header *header = n_dhcp4_incoming_get_header(lease->message);

        siaddr->s_addr = header->siaddr;
}

/**
 * n_dhcp4_client_lease_get_basetime() - get the timestamp when the lease was received.
 * @lease:                      the lease to operate on
 * @ns_basetimep:               return argument for the base time in nano seconds
 *
 * Gets the timestamp when the lease was received in CLOCK_BOOTTIME. This
 * is also the base timestamp for the expiration of the lifetime and t1/t2.
 */
_c_public_ void n_dhcp4_client_lease_get_basetime(NDhcp4ClientLease *lease, uint64_t *ns_basetimep) {
        *ns_basetimep = lease->message->userdata.base_time;
}

/**
 * n_dhcp4_client_lease_get_lifetime() - get the lifetime
 * @lease:                      the lease to operate on
 * @ns_lifetimep:               return argument for the lifetime in nano seconds
 *
 * Gets the end of the lease's lifetime in nanoseconds according to CLOCK_BOOTTIME,
 * or (uint64_t)-1 for permanent leases.
 */
_c_public_ void n_dhcp4_client_lease_get_lifetime(NDhcp4ClientLease *lease, uint64_t *ns_lifetimep) {
        *ns_lifetimep = lease->lifetime;
}

/**
 * n_dhcp4_client_lease_get_server_identifier() - get the server identifier
 * @lease:                      the lease to operate on
 * @addr:                       return argument for the server identifier
 *
 * Gets the address contained in the server-identifier DHCP option, in network
 * byte order.
 *
 * Return: 0 on success,
 *         N_DHCP4_E_UNSET if the lease doesn't contain a server-identifier, or
 *         N_DCHP4_E_INTERNAL if the server-identifier is not valid.
 */
_c_public_ int n_dhcp4_client_lease_get_server_identifier(NDhcp4ClientLease *lease, struct in_addr *addr) {
        uint8_t *data;
        size_t n_data;
        int r;

        r = n_dhcp4_incoming_query(lease->message, N_DHCP4_OPTION_SERVER_IDENTIFIER, &data, &n_data);
        if (r)
                return r;
        if (n_data < sizeof(struct in_addr))
                return N_DHCP4_E_INTERNAL;

        memcpy(addr, data, sizeof(struct in_addr));

        return 0;
}

/**
 * n_dhcp4_client_lease_get_file() - query the lease for the boot file name
 * @lease:                      the lease to operate on
 * @file:                       return argument for the file name
 *
 * Query the lease for the boot file name from the DHCP header. The file name
 * is returned as a NULL-terminated string.
 *
 * Return: 0 on success,
 *         N_DHCP4_E_UNSET if the lease does not contain a file name, or
 *         N_DCHP4_E_INTERNAL if the file name is invalid.
 */
_c_public_ int n_dhcp4_client_lease_get_file(NDhcp4ClientLease *lease, const char **file) {
        NDhcp4Message *message;

        if (lease->message->options[N_DHCP4_OPTION_OVERLOAD].size > 0
            && ((*lease->message->options[N_DHCP4_OPTION_OVERLOAD].value) & N_DHCP4_OVERLOAD_FILE)) {
            /* The field is overloaded to contain other options */
            return N_DHCP4_E_UNSET;
        }

        message = &lease->message->message;

        if (message->file[0] == '\0')
            return N_DHCP4_E_UNSET;

        if (!memchr(message->file, '\0', sizeof(message->file))) {
            /* The field is NULL-terminated (RFC 2131 section 2) */
            return N_DHCP4_E_INTERNAL;
        }

        *file = (const char *) message->file;

        return 0;
}

/**
 * n_dhcp4_client_lease_query() - query the lease for an option
 * @lease:                      the lease to operate on
 * @option:                     the DHCP4 option code
 * @datap:                      return argument of the data pointer
 * @n_datap:                    return argument of data length in bytes
 *
 * Query the lease for a given option. Options internal to the DHCP protocol cannot
 * be queried, and only options that were explicitly requested can be queried.
 *
 * Return: 0 on success,
 *         N_DCHP4_E_INTERNAL if an invalid option is queried,
 *         N_DHCP4_E_UNSET if the lease did not contain the option, or
 *         a negative error code on failure.
 */
_c_public_ int n_dhcp4_client_lease_query(NDhcp4ClientLease *lease, uint8_t option, uint8_t **datap, size_t *n_datap) {
        switch (option) {
        case N_DHCP4_OPTION_PAD:
        case N_DHCP4_OPTION_REQUESTED_IP_ADDRESS:
        case N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME:
        case N_DHCP4_OPTION_OVERLOAD:
        case N_DHCP4_OPTION_MESSAGE_TYPE:
        case N_DHCP4_OPTION_SERVER_IDENTIFIER:
        case N_DHCP4_OPTION_PARAMETER_REQUEST_LIST:
        case N_DHCP4_OPTION_ERROR_MESSAGE:
        case N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE:
        case N_DHCP4_OPTION_RENEWAL_T1_TIME:
        case N_DHCP4_OPTION_REBINDING_T2_TIME:
        case N_DHCP4_OPTION_END:
                return N_DHCP4_E_INTERNAL;
        }

        /* XXX: refuse to return options that were not requested */

        return n_dhcp4_incoming_query(lease->message, option, datap, n_datap);
}

/**
 * n_dhcp4_client_lease_select() - select an offered lease
 * @lease:                      lease to operate on
 *
 * Select a lease. This must be a lease that was offered, once
 * one of the leases that were offered in response to a probe was
 * selected none of the others can be.
 *
 * Return: 0 on success, or a negative error code on failure.
 */
_c_public_ int n_dhcp4_client_lease_select(NDhcp4ClientLease *lease) {
        NDhcp4ClientLease *l, *t_l;
        NDhcp4ClientProbe *probe;
        int r;

        /* XXX error handling, this must be an OFFER */

        if (!lease->probe)
                return -ENOTRECOVERABLE;
        if (lease->probe->current_lease)
                return -ENOTRECOVERABLE;

        r = n_dhcp4_client_probe_transition_select(lease->probe, lease->message, n_dhcp4_gettime(CLOCK_BOOTTIME));
        if (r)
                return r;

        /*
         * Only one of the offered leases can be selected, so flush the list.
         * All offered lease, including this one are now dead.
         */
        probe = lease->probe;
        c_list_for_each_entry_safe(l, t_l, &probe->lease_list, probe_link)
                n_dhcp4_client_lease_unlink(l);

        return 0;
}

/**
 * n_dhcp4_client_lease_accept() - accept an ack'ed lease
 * @lease:                      lease to operate on
 *
 * Accept a lease. This must be a lease that was ack'ed by the
 * server.
 *
 * The offered IP address must be fully configured before the lease
 * can be accepted.
 *
 * Return: 0 on success, or a negative error code on failure.
 */
_c_public_ int n_dhcp4_client_lease_accept(NDhcp4ClientLease *lease) {
        int r;

        /* XXX error handling, this must be an ACK */

        if (!lease->probe)
                return -ENOTRECOVERABLE;
        if (lease->probe->current_lease != lease)
                return -ENOTRECOVERABLE;

        r = n_dhcp4_client_probe_transition_accept(lease->probe, lease->message);
        if (r)
                return r;

        n_dhcp4_client_lease_unlink(lease);

        return 0;
}

/**
 * n_dhcp4_client_lease_decline() - decline an ack'ed lease
 * @lease:                      lease to operate on
 *
 * Decline a lease. This must be a lease that was ack'ed by the
 * server.
 *
 * The offered IP address must not be used once the lease has been
 * decline.
 *
 * Return: 0 on success, or a negative error code on failure.
 */
_c_public_ int n_dhcp4_client_lease_decline(NDhcp4ClientLease *lease, const char *error) {
        int r;

        /* XXX: error handling, this must be an ACK */

        if (!lease->probe)
                return -ENOTRECOVERABLE;
        if (lease->probe->current_lease != lease)
                return -ENOTRECOVERABLE;

        r = n_dhcp4_client_probe_transition_decline(lease->probe, lease->message, error, n_dhcp4_gettime(CLOCK_BOOTTIME));
        if (r)
                return r;

        lease->probe->current_lease = n_dhcp4_client_lease_unref(lease->probe->current_lease);
        n_dhcp4_client_lease_unlink(lease);

        return 0;
}
