/* Icecast
 *
 * This program is distributed under the GNU General Public License, version 2.
 * A copy of this license is included with this source.
 *
 * Copyright 2000-2004, Jack Moffitt <jack@xiph.org>,
 *                      Michael Smith <msmith@xiph.org>,
 *                      oddsock <oddsock@xiph.org>,
 *                      Karl Heyes <karl@xiph.org>
 *                      and others (see AUTHORS for details).
 * Copyright 2011-2018, Philipp "ph3-der-loewe" Schafft <lion@lion.leolix.org>,
 */

/*
 * Client authentication via URL functions
 *
 * authenticate user via a URL, this is done via libcurl so https can also
 * be handled. The request will have POST information about the request in
 * the form of
 *
 * action=listener_add&client=1&server=host&port=8000&mount=/live&user=fred&pass=mypass&ip=127.0.0.1&agent=""
 *
 * For a user to be accecpted the following HTTP header needs
 * to be returned (the actual string can be specified in the xml file)
 *
 * icecast-auth-user: 1
 *
 * A listening client may also be configured as only to stay connected for a
 * certain length of time. eg The auth server may only allow a 15 minute
 * playback by sending back.
 *
 * icecast-auth-timelimit: 900
 *
 * On client disconnection another request can be sent to a URL with the POST
 * information of
 *
 * action=listener_remove&server=host&port=8000&client=1&mount=/live&user=fred&pass=mypass&duration=3600
 *
 * client refers to the icecast client identification number. mount refers
 * to the mountpoint (beginning with / and may contain query parameters eg ?&
 * encoded) and duration is the amount of time in seconds. user and pass
 * setting can be blank
 *
 * On source client connection, a request can be made to trigger a URL request
 * to verify the details externally. Post info is
 *
 * action=stream_auth&mount=/stream&ip=IP&server=SERVER&port=8000&user=fred&pass=pass
 *
 * As admin requests can come in for a stream (eg metadata update) these requests
 * can be issued while stream is active. For these &admin=1 is added to the POST
 * details.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#ifndef _WIN32
#   include <sys/wait.h>
#   include <strings.h>
#else
#   define snprintf _snprintf
#   define strncasecmp strnicmp
#endif

#include "icecasttypes.h"
#include <igloo/ro.h>
#include <igloo/error.h>

#include "util.h"
#include "curl.h"
#include "ping.h"
#include "auth.h"
#include "source.h"
#include "client.h"
#include "cfgfile.h"
#include "global.h"
#include "string_renderer.h"
#include "connection.h"
#include "common/httpp/httpp.h"

#include "logging.h"
#define CATMODULE "auth_url"

/* Default headers */
#define DEFAULT_HEADER_OLD_RESULT           "icecast-auth-user: 1\r\n"
#define DEFAULT_HEADER_OLD_TIMELIMIT        "icecast-auth-timelimit:"
#define DEFAULT_HEADER_OLD_MESSAGE          "icecast-auth-message"
#define DEFAULT_HEADER_NEW_RESULT           "x-icecast-auth-result"
#define DEFAULT_HEADER_NEW_TIMELIMIT        "x-icecast-auth-timelimit"
#define DEFAULT_HEADER_NEW_MESSAGE          "x-icecast-auth-message"
#define DEFAULT_HEADER_NEW_ALTER_ACTION     "x-icecast-auth-alter-action"
#define DEFAULT_HEADER_NEW_ALTER_ARGUMENT   "x-icecast-auth-alter-argument"

typedef struct {
    char       *pass_headers; // headers passed from client to addurl.
    char       *prefix_headers; // prefix for passed headers.
    char       *addurl;
    char       *removeurl;
    char       *addaction;
    char       *removeaction;
    char       *username;
    char       *password;

    /* old style */
    char       *auth_header;
    size_t      auth_header_len;
    char       *timelimit_header;
    size_t      timelimit_header_len;
    /* new style */
    char       *header_auth;
    char       *header_timelimit;
    char       *header_message;
    char       *header_alter_action;
    char       *header_alter_argument;

    char       *userpwd;
    char        errormsg[CURL_ERROR_SIZE];
    auth_result result;
} auth_url;

typedef struct {
    char *all_headers;
    size_t all_headers_len;
    http_parser_t *parser;
} auth_user_url_t;

static inline const char * __str_or_default(const char *str, const char *def)
{
    if (str)
        return str;
    return def;
}

static void auth_url_clear(auth_t *self)
{
    auth_url *url;

    ICECAST_LOG_INFO("Doing auth URL cleanup");
    url = self->state;
    self->state = NULL;
    free(url->username);
    free(url->password);
    free(url->pass_headers);
    free(url->prefix_headers);
    free(url->removeurl);
    free(url->addurl);
    free(url->addaction);
    free(url->removeaction);
    free(url->auth_header);
    free(url->timelimit_header);
    free(url->header_auth);
    free(url->header_timelimit);
    free(url->header_message);
    free(url->header_alter_action);
    free(url->header_alter_argument);
    free(url->userpwd);
    free(url);
}

static void auth_user_url_clear(auth_client *auth_user)
{
    auth_user_url_t *au_url = auth_user->authbackend_userdata;

    if (!au_url)
        return;

    free(au_url->all_headers);
    if (au_url->parser)
        httpp_destroy(au_url->parser);

    free(au_url);
    auth_user->authbackend_userdata = NULL;
}

static void handle_returned_header__complete(auth_client *auth_user)
{
    auth_user_url_t *au_url = auth_user->authbackend_userdata;
    const char *tmp;
    const char *action;
    const char *argument;
    auth_url *url = auth_user->client->auth->state;

    if (!au_url)
        return;

    if (au_url->parser)
        return;

    au_url->parser = httpp_create_parser();
    httpp_initialize(au_url->parser, NULL);

    if (!httpp_parse_response(au_url->parser, au_url->all_headers, au_url->all_headers_len, NULL)) {
        ICECAST_LOG_ERROR("Can not parse auth backend reply.");
        return;
    }

    tmp = httpp_getvar(au_url->parser, HTTPP_VAR_ERROR_CODE);
    if (tmp[0] == '2') {
        ICECAST_LOG_DEBUG("Got final status: %#H", tmp);
    } else {
        ICECAST_LOG_DEBUG("Got non-final status: %#H", tmp);
        httpp_destroy(au_url->parser);
        au_url->parser = NULL;
        au_url->all_headers_len = 0;
        return;
    }

    if (url->header_auth) {
        tmp = httpp_getvar(au_url->parser, url->header_auth);
        if (tmp) {
            url->result = auth_str2result(tmp);
        }
    }

    if (url->header_timelimit) {
        tmp = httpp_getvar(au_url->parser, url->header_timelimit);
        if (tmp) {
            long long int ret;
            char *endptr;

            errno = 0;
            ret = strtoll(tmp, &endptr, 0);
            if (endptr != tmp && errno == 0) {
                auth_user->client->con->discon_time = time(NULL) + (time_t)ret;
            } else {
                ICECAST_LOG_ERROR("Auth backend returned invalid new style timelimit header: % #H", tmp);
            }
        }
    }

    action   = httpp_getvar(au_url->parser, __str_or_default(url->header_alter_action, DEFAULT_HEADER_NEW_ALTER_ACTION));
    argument = httpp_getvar(au_url->parser, __str_or_default(url->header_alter_argument, DEFAULT_HEADER_NEW_ALTER_ARGUMENT));

    if (action && argument) {
        if (auth_alter_client(auth_user->client->auth, auth_user, auth_str2alter(action), argument) != 0) {
            ICECAST_LOG_ERROR("Auth backend returned invalid alter action/argument.");
        }
    } else if (action || argument) {
        ICECAST_LOG_ERROR("Auth backend returned incomplete alter action/argument.");
    }

    if (url->header_message) {
        tmp = httpp_getvar(au_url->parser, url->header_message);
    } else {
        tmp = httpp_getvar(au_url->parser, DEFAULT_HEADER_NEW_MESSAGE);
        if (!tmp)
            tmp = httpp_getvar(au_url->parser, DEFAULT_HEADER_OLD_MESSAGE);
    }
    if (tmp) {
        snprintf(url->errormsg, sizeof(url->errormsg), "%s", tmp);
    }
}

static size_t handle_returned_header(void      *ptr,
                                     size_t    size,
                                     size_t    nmemb,
                                     void      *stream)
{
    size_t len = size * nmemb;
    auth_client *auth_user = stream;
    client_t *client = auth_user->client;
    auth_t *auth;
    auth_url *url;

    if (!client)
        return len;

    auth = client->auth;
    url = auth->state;

    if (!auth_user->authbackend_userdata) {
        auth_user->authbackend_userdata = calloc(1, sizeof(auth_user_url_t));
    }

    if (auth_user->authbackend_userdata) {
        auth_user_url_t *au_url = auth_user->authbackend_userdata;
        char *n = realloc(au_url->all_headers, au_url->all_headers_len + len);
        if (n) {
            au_url->all_headers = n;
            memcpy(n + au_url->all_headers_len, ptr, len);
            au_url->all_headers_len += len;
        } else {
            ICECAST_LOG_ERROR("Can not allocate buffer for auth backend reply headers. BAD.");
        }
    } else {
        ICECAST_LOG_ERROR("Can not allocate authbackend_userdata. BAD.");
    }

    ICECAST_LOG_DEBUG("Got header: %* #H", (int)(size * nmemb + 2), ptr);

    if (url->auth_header && len >= url->auth_header_len && strncasecmp(ptr, url->auth_header, url->auth_header_len) == 0) {
        url->result = AUTH_OK;
    }

    if (url->timelimit_header && len > url->timelimit_header_len && strncasecmp(ptr, url->timelimit_header, url->timelimit_header_len) == 0) {
        const char *input = ptr;
        unsigned int limit = 0;

        if (len >= 2 && input[len - 2] == '\r' && input[len - 1] == '\n') {
            input += url->timelimit_header_len;

            if (sscanf(input, "%u\r\n", &limit) == 1) {
                client->con->discon_time = time(NULL) + limit;
            } else {
                ICECAST_LOG_ERROR("Auth backend returned invalid timeline header: Can not parse limit");
            }
        } else {
            ICECAST_LOG_ERROR("Auth backend returned invalid timelimit header.");
        }
    }

    if (len == 1) {
        const char *c = ptr;
        if (c[0] == '\r' || c[0] == '\n') {
            handle_returned_header__complete(auth_user);
        }
    } else if (len == 2) {
        const char *c = ptr;
        if ((c[0] == '\r' || c[0] == '\n') && (c[1] == '\r' || c[1] == '\n')) {
            handle_returned_header__complete(auth_user);
        }
    }


    return len;
}

static string_renderer_t * url_add_params(auth_client *auth_user, bool is_remove)
{
    client_t          *client      = auth_user->client;
    auth_t            *auth        = client->auth;
    auth_url          *url         = auth->state;
    string_renderer_t *renderer;
    const char        *agent;
    const char        *mountreq;
    ice_config_t      *config;
    char              *pass_headers,
                      *cur_header,
                      *next_header;

    if (igloo_ro_new(&renderer, string_renderer_t, igloo_instance) != igloo_ERROR_NONE)
        return NULL;

    string_renderer_start_list_formdata(renderer);

    if (is_remove) {
        string_renderer_add_kv_with_options(renderer, "action", url->removeaction, STRING_RENDERER_ENCODING_PLAIN, false, false);
        string_renderer_add_ki_with_options(renderer, "duration", time(NULL) - client->con->con_time, STRING_RENDERER_ENCODING_PLAIN, true, false);
    } else {
        string_renderer_add_kv_with_options(renderer, "action", url->addaction, STRING_RENDERER_ENCODING_PLAIN, false, false);
    }

    config = config_get_config();
    string_renderer_add_kv_with_options(renderer, "server", config->hostname, STRING_RENDERER_ENCODING_PLAIN, false, false);
    string_renderer_add_ki_with_options(renderer, "port", config->port, STRING_RENDERER_ENCODING_PLAIN, false, false);
    config_release_config();
    string_renderer_add_kv_with_options(renderer, "server-instance", global_instance_uuid(), STRING_RENDERER_ENCODING_PLAIN, false, false);

    string_renderer_add_ki_with_options(renderer, "client", client->con->id, STRING_RENDERER_ENCODING_PLAIN, false, false);

    /* get the full uri (with query params if available) */
    mountreq = httpp_getvar(client->parser, HTTPP_VAR_RAWURI);
    if (mountreq == NULL)
        mountreq = httpp_getvar(client->parser, HTTPP_VAR_URI);
    string_renderer_add_kv_with_options(renderer, "mount", mountreq, STRING_RENDERER_ENCODING_PLAIN, false, false);

    string_renderer_add_kv_with_options(renderer, "user", client->username, STRING_RENDERER_ENCODING_PLAIN, false, false);
    string_renderer_add_kv_with_options(renderer, "pass", client->password, STRING_RENDERER_ENCODING_PLAIN, false, false);
    string_renderer_add_kv_with_options(renderer, "ip", client->con->ip, STRING_RENDERER_ENCODING_PLAIN, false, false);

    agent = httpp_getvar(client->parser, "user-agent");
    string_renderer_add_kv_with_options(renderer, "agent", agent ? agent : "-", STRING_RENDERER_ENCODING_PLAIN, false, false);

    if (!is_remove) {
        pass_headers = NULL;
        if (url->pass_headers)
            pass_headers = strdup(url->pass_headers);
        if (pass_headers) {
            cur_header = pass_headers;
            while (cur_header) {
                const char *header_val;

                next_header = strstr(cur_header, ",");
                if (next_header) {
                    *next_header=0;
                    next_header++;
                }

                header_val = httpp_getvar (client->parser, cur_header);
                if (header_val) {
                    char buf[64];

                    snprintf(buf, sizeof(buf), "%s%s",  url->prefix_headers ? url->prefix_headers : "", cur_header);

                    string_renderer_add_kv_with_options(renderer, buf, header_val, STRING_RENDERER_ENCODING_URI, false, false);
                }

                cur_header = next_header;
            }
            free(pass_headers);
        }
    }

    string_renderer_end_list(renderer);

    return renderer;
}

static auth_result url_remove_client(auth_client *auth_user)
{
    client_t       *client      = auth_user->client;
    auth_t         *auth        = client->auth;
    auth_url       *url         = auth->state;
    string_renderer_t *renderer;

    if (url->removeurl == NULL)
        return AUTH_OK;

    if (!(renderer = url_add_params(auth_user, true)))
        return AUTH_FAILED;

    if (strchr (url->removeurl, '@') != NULL) {
        ping_simple(url->removeurl, NULL, NULL, renderer);
    } else {
        if (url->username) {
            ping_simple(url->removeurl, url->username, url->password, renderer);
        } else {
            ping_simple(url->removeurl, client->username, client->password, renderer);
        }
    }

    igloo_ro_unref(&renderer);

    return AUTH_OK;
}


static auth_result url_add_client(auth_client *auth_user)
{
    client_t       *client      = auth_user->client;
    auth_t         *auth        = client->auth;
    auth_url       *url         = auth->state;
    CURLcode        res         = CURLE_OK;
    char           *userpwd     = NULL;
    CURL           *handle;
    string_renderer_t *renderer;

    if (url->addurl == NULL)
        return AUTH_OK;

    handle = icecast_curl_new(NULL, &url->errormsg[0]);

    curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, handle_returned_header);

    if (strchr(url->addurl, '@') == NULL) {
        if (url->userpwd) {
            curl_easy_setopt(handle, CURLOPT_USERPWD, url->userpwd);
        } else {
            /* auth'd requests may not have a user/pass, but may use query args */
            if (client->username && client->password) {
                size_t len = strlen(client->username) + strlen(client->password) + 2;
                userpwd = malloc (len);
                snprintf(userpwd, len, "%s:%s",
                    client->username, client->password);
                curl_easy_setopt(handle, CURLOPT_USERPWD, userpwd);
            } else {
                curl_easy_setopt(handle, CURLOPT_USERPWD, "");
            }
        }
    } else {
        /* url has user/pass but libcurl may need to clear any existing settings */
        curl_easy_setopt(handle, CURLOPT_USERPWD, "");
    }

    if (!(renderer = url_add_params(auth_user, false))) {
        icecast_curl_free(handle);
        return AUTH_FAILED;
    }

    curl_easy_setopt(handle, CURLOPT_COPYPOSTFIELDS, string_renderer_to_string_zero_copy(renderer));
    igloo_ro_unref(&renderer);

    curl_easy_setopt(handle, CURLOPT_URL, url->addurl);
    curl_easy_setopt(handle, CURLOPT_WRITEHEADER, auth_user);
    url->errormsg[0] = '\0';

    url->result = AUTH_FAILED;

    ICECAST_LOG_DEBUG("Pre-request (%s)", url->addurl);
    res = curl_easy_perform(handle);
    icecast_curl_free(handle);

    free(userpwd);
    auth_user_url_clear(auth_user);

    if (res != CURLE_OK) {
        ICECAST_LOG_WARN("auth to server %s failed with \"% H\", (curl: %s)",
            url->addurl, url->errormsg, curl_easy_strerror(res));
        return AUTH_FAILED;
    }
    /* we received a response, lets see what it is */
    if (url->result == AUTH_FAILED) {
        ICECAST_LOG_INFO("client auth (%s) failed with \"% H\", (curl: %s)",
            url->addurl, url->errormsg, curl_easy_strerror(res));
    }

    return url->result;
}

static auth_result auth_url_adduser(auth_t      *auth,
                                    const char  *username,
                                    const char  *password)
{
    return AUTH_FAILED;
}

static auth_result auth_url_deleteuser(auth_t *auth, const char *username)
{
    return AUTH_FAILED;
}

static auth_result auth_url_listuser(auth_t *auth, xmlNodePtr srcnode)
{
    return AUTH_FAILED;
}

int auth_get_url_auth(auth_t *authenticator, config_options_t *options)
{
    auth_url    *url_info;

    authenticator->free         = auth_url_clear;
    authenticator->adduser      = auth_url_adduser;
    authenticator->deleteuser   = auth_url_deleteuser;
    authenticator->listuser     = auth_url_listuser;

    url_info                    = calloc(1, sizeof(auth_url));
    authenticator->state        = url_info;

    /* force auth thread to call function. this makes sure the auth_t is attached to client */
    authenticator->authenticate_client = url_add_client;

    url_info->addaction    = strdup("listener_add");
    url_info->removeaction = strdup("listener_remove");

    while(options) {
        if(strcmp(options->name, "username") == 0) {
            util_replace_string(&(url_info->username), options->value);
        } else if(strcmp(options->name, "password") == 0) {
            util_replace_string(&(url_info->password), options->value);
        } else if(strcmp(options->name, "headers") == 0) {
            util_replace_string(&(url_info->pass_headers), options->value);
        } else if(strcmp(options->name, "header_prefix") == 0) {
            util_replace_string(&(url_info->prefix_headers), options->value);
        } else if(strcmp(options->name, "client_add") == 0) {
            util_replace_string(&(url_info->addurl), options->value);
        } else if(strcmp(options->name, "client_remove") == 0) {
            authenticator->release_client = url_remove_client;
            util_replace_string(&(url_info->removeurl), options->value);
        } else if(strcmp(options->name, "action_add") == 0) {
            util_replace_string(&(url_info->addaction), options->value);
        } else if(strcmp(options->name, "action_remove") == 0) {
            util_replace_string(&(url_info->removeaction), options->value);
        } else if(strcmp(options->name, "auth_header") == 0) {
            util_replace_string(&(url_info->auth_header), options->value);
        } else if (strcmp(options->name, "timelimit_header") == 0) {
            util_replace_string(&(url_info->timelimit_header), options->value);
        } else if (strcmp(options->name, "header_auth") == 0) {
            util_replace_string(&(url_info->header_auth), options->value);
            util_strtolower(url_info->header_auth);
        } else if (strcmp(options->name, "header_timelimit") == 0) {
            util_replace_string(&(url_info->header_timelimit), options->value);
            util_strtolower(url_info->header_timelimit);
        } else if (strcmp(options->name, "header_message") == 0) {
            util_replace_string(&(url_info->header_message), options->value);
            util_strtolower(url_info->header_message);
        } else if (strcmp(options->name, "header_alter_action") == 0) {
            util_replace_string(&(url_info->header_alter_action), options->value);
            util_strtolower(url_info->header_alter_action);
        } else if (strcmp(options->name, "header_alter_argument") == 0) {
            util_replace_string(&(url_info->header_alter_argument), options->value);
            util_strtolower(url_info->header_alter_argument);
        } else {
            ICECAST_LOG_ERROR("Unknown option: %s", options->name);
        }
        options = options->next;
    }

    /* default headers */
    if (url_info->auth_header) {
        ICECAST_LOG_WARN("You use old style auth option \"auth_header\". Please switch to new style option \"header_auth\".");
    } else if (!url_info->header_auth && !url_info->auth_header) {
        ICECAST_LOG_WARN("You do not have enabled old or new style auth option for auth status header. I will enable both. Please set \"header_auth\".");
        url_info->auth_header = strdup(DEFAULT_HEADER_OLD_RESULT);
        url_info->header_auth = strdup(DEFAULT_HEADER_NEW_RESULT);
    }
    if (url_info->timelimit_header) {
        ICECAST_LOG_WARN("You use old style auth option \"timelimit_header\". Please switch to new style option \"header_timelimit\".");
    } else if (!url_info->header_timelimit && !url_info->timelimit_header) {
        ICECAST_LOG_WARN("You do not have enabled old or new style auth option for auth timelimit header. I will enable both. Please set \"header_timelimit\".");
        url_info->timelimit_header = strdup(DEFAULT_HEADER_OLD_TIMELIMIT);
        url_info->header_timelimit = strdup(DEFAULT_HEADER_NEW_TIMELIMIT);
    }

    if (url_info->auth_header)
        url_info->auth_header_len = strlen (url_info->auth_header);
    if (url_info->timelimit_header)
        url_info->timelimit_header_len = strlen (url_info->timelimit_header);

    if (url_info->username && url_info->password) {
        int len = strlen(url_info->username) + strlen(url_info->password) + 2;
        url_info->userpwd = malloc(len);
        snprintf(url_info->userpwd, len, "%s:%s",
            url_info->username, url_info->password);
    }

    ICECAST_LOG_INFO("URL based authentication setup");
    return 0;
}
