/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/* lib/krad/client.c - Client request code for libkrad */
/*
 * Copyright 2013 Red Hat, Inc.  All rights reserved.
 *
 * 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.
 *
 * 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 OWNER
 * 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 <k5-queue.h>
#include "internal.h"

#include <string.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdio.h>
#include <limits.h>

LIST_HEAD(server_head, server_st);

typedef struct remote_state_st remote_state;
typedef struct request_st request;
typedef struct server_st server;

struct remote_state_st {
    const krad_packet *packet;
    krad_remote *remote;
};

struct request_st {
    krad_client *rc;

    krad_code code;
    krad_attrset *attrs;
    int timeout;
    size_t retries;
    krad_cb cb;
    void *data;

    remote_state *remotes;
    ssize_t current;
    ssize_t count;
};

struct server_st {
    krad_remote *serv;
    time_t last;
    LIST_ENTRY(server_st) list;
};

struct krad_client_st {
    krb5_context kctx;
    verto_ctx *vctx;
    struct server_head servers;
};

/* Return either a pre-existing server that matches the address info and the
 * secret, or create a new one. */
static krb5_error_code
get_server(krad_client *rc, const struct addrinfo *ai, const char *secret,
           krad_remote **out)
{
    krb5_error_code retval;
    time_t currtime;
    server *srv;

    if (time(&currtime) == (time_t)-1)
        return errno;

    LIST_FOREACH(srv, &rc->servers, list) {
        if (kr_remote_equals(srv->serv, ai, secret)) {
            srv->last = currtime;
            *out = srv->serv;
            return 0;
        }
    }

    srv = calloc(1, sizeof(server));
    if (srv == NULL)
        return ENOMEM;
    srv->last = currtime;

    retval = kr_remote_new(rc->kctx, rc->vctx, ai, secret, &srv->serv);
    if (retval != 0) {
        free(srv);
        return retval;
    }

    LIST_INSERT_HEAD(&rc->servers, srv, list);
    *out = srv->serv;
    return 0;
}

/* Free a request. */
static void
request_free(request *req)
{
    krad_attrset_free(req->attrs);
    free(req->remotes);
    free(req);
}

/* Create a request. */
static krb5_error_code
request_new(krad_client *rc, krad_code code, const krad_attrset *attrs,
            const struct addrinfo *ai, const char *secret, int timeout,
            size_t retries, krad_cb cb, void *data, request **req)
{
    const struct addrinfo *tmp;
    krb5_error_code retval;
    request *rqst;
    size_t i;

    if (ai == NULL)
        return EINVAL;

    rqst = calloc(1, sizeof(request));
    if (rqst == NULL)
        return ENOMEM;

    for (tmp = ai; tmp != NULL; tmp = tmp->ai_next)
        rqst->count++;

    rqst->rc = rc;
    rqst->code = code;
    rqst->cb = cb;
    rqst->data = data;
    rqst->timeout = timeout / rqst->count;
    rqst->retries = retries;

    retval = krad_attrset_copy(attrs, &rqst->attrs);
    if (retval != 0) {
        request_free(rqst);
        return retval;
    }

    rqst->remotes = calloc(rqst->count + 1, sizeof(remote_state));
    if (rqst->remotes == NULL) {
        request_free(rqst);
        return ENOMEM;
    }

    i = 0;
    for (tmp = ai; tmp != NULL; tmp = tmp->ai_next) {
        retval = get_server(rc, tmp, secret, &rqst->remotes[i++].remote);
        if (retval != 0) {
            request_free(rqst);
            return retval;
        }
    }

    *req = rqst;
    return 0;
}

/* Close remotes that haven't been used in a while. */
static void
age(struct server_head *head, time_t currtime)
{
    server *srv, *tmp;

    LIST_FOREACH_SAFE(srv, head, list, tmp) {
        if (currtime == (time_t)-1 || currtime - srv->last > 60 * 60) {
            LIST_REMOVE(srv, list);
            kr_remote_free(srv->serv);
            free(srv);
        }
    }
}

/* Handle a response from a server (or related errors). */
static void
on_response(krb5_error_code retval, const krad_packet *reqp,
            const krad_packet *rspp, void *data)
{
    request *req = data;
    time_t currtime;
    size_t i;

    /* Do nothing if we are already completed. */
    if (req->count < 0)
        return;

    /* If we have timed out and have more remotes to try, do so. */
    if (retval == ETIMEDOUT && req->remotes[++req->current].remote != NULL) {
        retval = kr_remote_send(req->remotes[req->current].remote, req->code,
                                req->attrs, on_response, req, req->timeout,
                                req->retries,
                                &req->remotes[req->current].packet);
        if (retval == 0)
            return;
    }

    /* Mark the request as complete. */
    req->count = -1;

    /* Inform the callback. */
    req->cb(retval, reqp, rspp, req->data);

    /* Cancel the outstanding packets. */
    for (i = 0; req->remotes[i].remote != NULL; i++)
        kr_remote_cancel(req->remotes[i].remote, req->remotes[i].packet);

    /* Age out servers that haven't been used in a while. */
    if (time(&currtime) != (time_t)-1)
        age(&req->rc->servers, currtime);

    request_free(req);
}

krb5_error_code
krad_client_new(krb5_context kctx, verto_ctx *vctx, krad_client **out)
{
    krad_client *tmp;

    tmp = calloc(1, sizeof(krad_client));
    if (tmp == NULL)
        return ENOMEM;

    tmp->kctx = kctx;
    tmp->vctx = vctx;

    *out = tmp;
    return 0;
}

void
krad_client_free(krad_client *rc)
{
    if (rc == NULL)
        return;

    age(&rc->servers, -1);
    free(rc);
}

static krb5_error_code
resolve_remote(const char *remote, struct addrinfo **ai)
{
    const char *svc = "radius";
    krb5_error_code retval;
    struct addrinfo hints;
    char *sep, *srv;

    /* Isolate the port number if it exists. */
    srv = strdup(remote);
    if (srv == NULL)
        return ENOMEM;

    if (srv[0] == '[') {
        /* IPv6 */
        sep = strrchr(srv, ']');
        if (sep != NULL && sep[1] == ':') {
            sep[1] = '\0';
            svc = &sep[2];
        }
    } else {
        /* IPv4 or DNS */
        sep = strrchr(srv, ':');
        if (sep != NULL && sep[1] != '\0') {
            sep[0] = '\0';
            svc = &sep[1];
        }
    }

    /* Perform the lookup. */
    memset(&hints, 0, sizeof(hints));
    hints.ai_socktype = SOCK_DGRAM;
    retval = gai_error_code(getaddrinfo(srv, svc, &hints, ai));
    free(srv);
    return retval;
}

krb5_error_code
krad_client_send(krad_client *rc, krad_code code, const krad_attrset *attrs,
                 const char *remote, const char *secret, int timeout,
                 size_t retries, krad_cb cb, void *data)
{
    struct addrinfo usock, *ai = NULL;
    krb5_error_code retval;
    struct sockaddr_un ua;
    request *req;

    if (remote[0] == '/') {
        ua.sun_family = AF_UNIX;
        snprintf(ua.sun_path, sizeof(ua.sun_path), "%s", remote);
        memset(&usock, 0, sizeof(usock));
        usock.ai_family = AF_UNIX;
        usock.ai_socktype = SOCK_STREAM;
        usock.ai_addr = (struct sockaddr *)&ua;
        usock.ai_addrlen = sizeof(ua);

        retval = request_new(rc, code, attrs, &usock, secret, timeout, retries,
                             cb, data, &req);
    } else {
        retval = resolve_remote(remote, &ai);
        if (retval == 0) {
            retval = request_new(rc, code, attrs, ai, secret, timeout, retries,
                                 cb, data, &req);
            freeaddrinfo(ai);
        }
    }
    if (retval != 0)
        return retval;

    retval = kr_remote_send(req->remotes[req->current].remote, req->code,
                            req->attrs, on_response, req, req->timeout,
                            req->retries, &req->remotes[req->current].packet);
    if (retval != 0) {
        request_free(req);
        return retval;
    }

    return 0;
}
