/* 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-2020, Philipp "ph3-der-loewe" Schafft <lion@lion.leolix.org>,
 */

/* client.c
 **
 ** client interface implementation
 **
 */

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

#include <stdlib.h>
#include <string.h>

#include <libxml/tree.h>

#include "common/thread/thread.h"
#include "common/avl/avl.h"
#include "common/httpp/httpp.h"

#include "global.h"
#include "refobject.h"
#include "cfgfile.h"
#include "connection.h"
#include "tls.h"
#include "refbuf.h"
#include "format.h"
#include "stats.h"
#include "fserve.h"
#include "errors.h"
#include "reportxml.h"
#include "refobject.h"
#include "xslt.h"
#include "source.h"

#include "client.h"
#include "auth.h"
#include "logging.h"

#include "util.h"
#include "acl.h"
#include "listensocket.h"
#include "fastevent.h"

/* for ADMIN_COMMAND_ERROR, and ADMIN_ICESTATS_LEGACY_EXTENSION_APPLICATION */
#include "admin.h"

#ifdef _WIN32
#define snprintf _snprintf
#endif

#undef CATMODULE
#define CATMODULE "client"

static inline void client_send_500(client_t *client, const char *message);

/* create a client_t with the provided connection and parser details. Return
 * 0 on success, -1 if server limit has been reached.  In either case a
 * client_t is returned just in case a message needs to be returned. Should
 * be called with global lock held.
 */
int client_create(client_t **c_ptr, connection_t *con, http_parser_t *parser)
{
    ice_config_t    *config;
    client_t        *client = (client_t *) calloc(1, sizeof(client_t));
    const listener_t *listener_real, *listener_effective;
    int              ret    = -1;

    if (client == NULL)
        abort();

    config = config_get_config();

    global.clients++;
    if (config->client_limit < global.clients) {
        ICECAST_LOG_WARN("server client limit reached (%d/%d)", config->client_limit, global.clients);
    } else {
        ret = 0;
    }

    config_release_config ();

    stats_event_args (NULL, "clients", "%d", global.clients);
    client->con = con;
    client->parser = parser;
    client->protocol = ICECAST_PROTOCOL_HTTP;
    client->request_body_length = 0;
    client->request_body_read = 0;
    client->admin_command = ADMIN_COMMAND_ERROR;
    client->refbuf = refbuf_new (PER_CLIENT_REFBUF_SIZE);
    client->refbuf->len = 0; /* force reader code to ignore buffer contents */
    client->pos = 0;
    client->write_to_client = format_generic_write_to_client;
    *c_ptr = client;

    listener_real = listensocket_get_listener(con->listensocket_real);
    listener_effective = listensocket_get_listener(con->listensocket_effective);
    ICECAST_LOG_DEBUG("Client %p created on connection %p (connection ID: %llu, socket real: %p \"%H\", socket effective: %p \"%H\")",
            client, con, (long long unsigned int)con->id,
            con->listensocket_real, con->listensocket_real ? listener_real->id : NULL,
            con->listensocket_effective, con->listensocket_effective ? listener_effective->id : NULL
            );
    listensocket_release_listener(con->listensocket_effective);
    listensocket_release_listener(con->listensocket_real);

    fastevent_emit(FASTEVENT_TYPE_CLIENT_CREATE, FASTEVENT_FLAG_MODIFICATION_ALLOWED, FASTEVENT_DATATYPE_CLIENT, client);

    return ret;
}

void client_complete(client_t *client)
{
    const char *header;
    long long unsigned int scannumber;

    do {
        header = httpp_getvar(client->parser, "content-length");
        if (header) {
            if (sscanf(header, "%llu", &scannumber) == 1) {
                client->request_body_length = scannumber;
                break;
            }
        }

        if (client->parser->req_type == httpp_req_source) {
            client->request_body_length = -1; /* streaming */
            break;
        }

        header = httpp_getvar(client->parser, "transfer-encoding");
        if (header) {
            if (strcasecmp(header, "identity") != 0) {
                client->request_body_length = -1; /* streaming */
                break;
            }
        }

        if (client->parser->req_type == httpp_req_put) {
            /* As we don't know yet, we asume this PUT is in streaming mode */
            client->request_body_length = -1; /* streaming */
            break;
        }

        if (client->parser->req_type == httpp_req_none) {
            /* We are a client. If the server did not tell us, we asume streaming. */
            client->request_body_length = -1; /* streaming */
            break;
        }
    } while (0);

    ICECAST_LOG_DEBUG("Client %p has request_body_length=%zi", client, client->request_body_length);
}

static inline void client_reuseconnection(client_t *client) {
    connection_t *con;
    reuse_t reuse;

    if (!client)
        return;

    con = client->con;
    reuse = client->reuse;

    if (reuse == ICECAST_REUSE_UPGRADETLS) {
        http_parser_t *parser = client->parser;

        httpp_deletevar(parser, "upgrade");
        client->reuse = ICECAST_REUSE_CLOSE;

        /* release the buffer now, as the buffer could be on the source queue
         * and may of disappeared after auth completes */
        client_set_queue(client, NULL);
        client->refbuf = refbuf_new (PER_CLIENT_REFBUF_SIZE);
        client->refbuf->len = 0; /* force reader code to ignore buffer contents */
        client->pos = 0;

        connection_uses_tls(con);
        connection_queue_client(client);

        return;
    }

    con = connection_create(con->sock, con->listensocket_real, con->listensocket_effective, strdup(con->ip));
    client->con->sock = -1; /* TODO: do not use magic */

    /* handle to keep the TLS connection */
    if (client->con->tls) {
        /* AHhhggrr.. That pain....
         * stealing TLS state...
         */
        con->tls  = client->con->tls;
        con->read = client->con->read;
        con->send = client->con->send;
        client->con->tls  = NULL;
        client->con->read = NULL;
        client->con->send = NULL;
    }

    if (client->con->readbufferlen) {
        /* Aend... moorre paaiin.
         * stealing putback buffer.
         */
        con->readbuffer = client->con->readbuffer;
        con->readbufferlen = client->con->readbufferlen;
        client->con->readbuffer = NULL;
        client->con->readbufferlen = 0;
    }

    client->reuse = ICECAST_REUSE_CLOSE;

    client_destroy(client);
    connection_queue(con);
}

void client_destroy(client_t *client)
{
    ICECAST_LOG_DEBUG("Called to destory client %p", client);
    if (client == NULL)
        return;

    fastevent_emit(FASTEVENT_TYPE_CLIENT_DESTROY, FASTEVENT_FLAG_MODIFICATION_ALLOWED, FASTEVENT_DATATYPE_CLIENT, client);

    if (client->reuse != ICECAST_REUSE_CLOSE) {
        /* only reuse the client if we reached the body's EOF. */
        if (client_body_eof(client) == 1) {
            client_reuseconnection(client);
            return;
        }
    }

    /* release the buffer now, as the buffer could be on the source queue
     * and may of disappeared after auth completes */
    client_set_queue(client, NULL);

    if (auth_release_client(client))
        return;

    /* write log entry if ip is set (some things don't set it, like outgoing
     * slave requests
     */
    if (client->respcode && client->parser)
        logging_access(client);
    if (client->con)
        connection_close(client->con);
    if (client->parser)
        httpp_destroy(client->parser);
    if (client->encoding)
        httpp_encoding_release(client->encoding);

    global_lock();
    global.clients--;
    stats_event_args(NULL, "clients", "%d", global.clients);
    global_unlock();

    /* we need to free client specific format data (if any) */
    if (client->free_client_data)
        client->free_client_data(client);

    refobject_unref(client->handler_module);
    free(client->handler_function);
    free(client->uri);
    free(client->username);
    free(client->password);
    free(client->role);
    acl_release(client->acl);

    free(client);
}

/* helper function for reading data from a client */
static ssize_t __client_read_bytes_real(client_t *client, void *buf, size_t len)
{
    /* we have data to read from a refbuf first */
    if (client->refbuf->len < len)
        len = client->refbuf->len;
    memcpy (buf, client->refbuf->data, len);
    if (len < client->refbuf->len) {
        char *ptr = client->refbuf->data;
        memmove (ptr, ptr+len, client->refbuf->len - len);
    }
    client->refbuf->len -= len;
    return len;
}

int client_read_bytes(client_t *client, void *buf, unsigned len)
{
    ssize_t (*reader)(void*, void*, size_t) = (ssize_t(*)(void*,void*,size_t))__client_read_bytes_real;
    void *userdata = client;
    int bytes;

    if (!(client->refbuf && client->refbuf->len)) {
        reader = (ssize_t(*)(void*,void*,size_t))connection_read_bytes;
        userdata = client->con;
    }

    if (client->encoding) {
        bytes = httpp_encoding_read(client->encoding, buf, len, reader, userdata);
    } else {
        bytes = reader(userdata, buf, len);
    }

    if (bytes == -1 && client->con->error)
        ICECAST_LOG_DEBUG("reading from connection has failed");

    fastevent_emit(FASTEVENT_TYPE_CLIENT_READ, FASTEVENT_FLAG_MODIFICATION_ALLOWED, FASTEVENT_DATATYPE_OBRD, client, buf, (size_t)len, (ssize_t)bytes);

    return bytes;
}

static inline void _client_send_report(client_t *client, const char *uuid, const char *message, int http_status, const char *location)
{
    reportxml_t *report;
    admin_format_t admin_format;
    const char *xslt = NULL;

    admin_format = client_get_admin_format_by_content_negotiation(client);

    switch (admin_format) {
        case ADMIN_FORMAT_RAW:
            xslt = NULL;
        break;
        case ADMIN_FORMAT_HTML:
            xslt = CLIENT_DEFAULT_ERROR_XSL_HTML;
        break;
        case ADMIN_FORMAT_PLAINTEXT:
            xslt = CLIENT_DEFAULT_ERROR_XSL_PLAINTEXT;
        break;
        default:
            client_send_500(client, "Invalid Admin Type");
        break;
    }

    report = client_get_reportxml(uuid, NULL, message);

    client_send_reportxml(client, report, DOCUMENT_DOMAIN_ADMIN, xslt, admin_format, http_status, location);

    refobject_unref(report);
}

void client_send_error_by_error(client_t *client, const icecast_error_t *error)
{

    if (!error) {
         client_send_500(client, "Unknown error ID");
         return;
    }

    if (error->http_status == 500) {
         client_send_500(client, error->message);
        return;
    }

    _client_send_report(client, error->uuid, error->message, error->http_status, NULL);
}
void client_send_error_by_uuid(client_t *client, const char *uuid)
{
    client_send_error_by_error(client, error_get_by_uuid(uuid));
}
void client_send_error_by_id(client_t *client, icecast_error_id_t id)
{
    client_send_error_by_error(client, error_get_by_id(id));
}

void client_send_101(client_t *client, reuse_t reuse)
{
    ssize_t ret;

    if (!client)
        return;

    if (reuse != ICECAST_REUSE_UPGRADETLS) {
        client_send_500(client, "Bad reuse parameter");
        return;
    }

    client->reuse = reuse;

    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE, 0,
                                 0, 101, NULL,
                                 "text/plain", "utf-8",
                                 NULL, NULL, client);

    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
             "Content-Length: 0\r\nUpgrade: TLS/1.0, HTTP/1.1\r\n\r\n");

    client->respcode = 101;
    client->refbuf->len = strlen(client->refbuf->data);

    fastevent_emit(FASTEVENT_TYPE_CLIENT_SEND_RESPONSE, FASTEVENT_FLAG_MODIFICATION_ALLOWED, FASTEVENT_DATATYPE_CLIENT, client);

    fserve_add_client(client, NULL);
}

void client_send_204(client_t *client)
{
    source_t *source;
    ssize_t ret;

    if (!client)
        return;

    client->reuse = ICECAST_REUSE_KEEPALIVE;

    /* We get a source_t* here as this is likely a reply to OPTIONS and we want
     * to have as much infos as possible in that case.
     */
    avl_tree_rlock(global.source_tree);
    source = source_find_mount_raw(client->uri);
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE, 0,
                                 0, 204, NULL,
                                 NULL, NULL,
                                 NULL, source, client);
    avl_tree_unlock(global.source_tree);

    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
             "Content-Length: 0\r\n\r\n");

    client->respcode = 204;
    client->refbuf->len = strlen(client->refbuf->data);

    fastevent_emit(FASTEVENT_TYPE_CLIENT_SEND_RESPONSE, FASTEVENT_FLAG_MODIFICATION_ALLOWED, FASTEVENT_DATATYPE_CLIENT, client);

    fserve_add_client(client, NULL);
}

void client_send_426(client_t *client, reuse_t reuse)
{
    ssize_t ret;

    if (!client)
        return;

    if (reuse != ICECAST_REUSE_UPGRADETLS) {
        client_send_500(client, "Bad reuse parameter");
        return;
    }

    client->reuse = reuse;

    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE, 0,
                                 0, 426, NULL,
                                 "text/plain", "utf-8",
                                 NULL, NULL, client);

    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
             "Content-Length: 0\r\nUpgrade: TLS/1.0, HTTP/1.1\r\n\r\n");

    client->respcode = 426;
    client->refbuf->len = strlen(client->refbuf->data);

    client->reuse = ICECAST_REUSE_KEEPALIVE;

    fastevent_emit(FASTEVENT_TYPE_CLIENT_SEND_RESPONSE, FASTEVENT_FLAG_MODIFICATION_ALLOWED, FASTEVENT_DATATYPE_CLIENT, client);

    fserve_add_client(client, NULL);
}

/* this function is designed to work even if client is in bad state */
static inline void client_send_500(client_t *client, const char *message)
{
    const char header[] = "HTTP/1.0 500 Internal Server Error\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n"
                          "500 - Internal Server Error\n---------------------------\n";
    const ssize_t header_len = sizeof(header) - 1;
    ssize_t ret;

    client->respcode = 500;
    client->refbuf->len = 0;
    fastevent_emit(FASTEVENT_TYPE_CLIENT_SEND_RESPONSE, FASTEVENT_FLAG_MODIFICATION_ALLOWED, FASTEVENT_DATATYPE_CLIENT, client);

    ret = client_send_bytes(client, header, header_len);

    /* only send message if we have one AND if header could have transmitted completly */
    if (message && ret == header_len)
        client_send_bytes(client, message, strlen(message));

    client_destroy(client);
}

void client_send_redirect(client_t *client, const char *uuid, int status, const char *location)
{
    _client_send_report(client, uuid, "Redirecting", status, location);
}

/* this function sends a reportxml file to the client in the prefered format. */
void client_send_reportxml(client_t *client, reportxml_t *report, document_domain_t domain, const char *xsl, admin_format_t admin_format_hint, int status, const char *location)
{
    admin_format_t admin_format;
    xmlDocPtr doc;

    if (!client)
        return;

    if (!report) {
        ICECAST_LOG_ERROR("No report xml given. Sending 500 to client %p", client);
        client_send_500(client, "No report.");
        return;
    }

    if (!status)
        status = 200;

    if (admin_format_hint == ADMIN_FORMAT_AUTO) {
        admin_format = client_get_admin_format_by_content_negotiation(client);
    } else {
        admin_format = admin_format_hint;
    }

    if (!xsl) {
        switch (admin_format) {
            case ADMIN_FORMAT_RAW:
                /* noop, we don't need to set xsl */
            break;
            case ADMIN_FORMAT_HTML:
                xsl = CLIENT_DEFAULT_REPORT_XSL_HTML;
            break;
            case ADMIN_FORMAT_PLAINTEXT:
                xsl = CLIENT_DEFAULT_REPORT_XSL_PLAINTEXT;
            break;
            default:
                ICECAST_LOG_ERROR("Unsupported admin format and no XSLT file given. Sending 500 to client %p", client);
                client_send_500(client, "Unsupported admin format.");
                return;
            break;
        }
    } else if (admin_format_hint == ADMIN_FORMAT_AUTO) {
        ICECAST_LOG_ERROR("No explicit admin format but XSLT file given. BUG. Sending 500 to client %p", client);
        client_send_500(client, "Admin type AUTO but XSLT.");
        return;
    }

    doc = reportxml_render_xmldoc(report);
    if (!doc) {
        ICECAST_LOG_ERROR("Can not render XML Document from report. Sending 500 to client %p", client);
        client_send_500(client, "Can not render XML Document from report.");
        return;
    }

    if (admin_format == ADMIN_FORMAT_RAW) {
        xmlChar *buff = NULL;
        size_t location_length = 0;
        int len = 0;
        size_t buf_len;
        ssize_t ret;

        xmlDocDumpMemory(doc, &buff, &len);

        if (location) {
            location_length = strlen(location);
        }

        buf_len = len + location_length + 1024;
        if (buf_len < 4096)
            buf_len = 4096;

        client_set_queue(client, NULL);
        client->refbuf = refbuf_new(buf_len);
        client->reuse = ICECAST_REUSE_KEEPALIVE;

        ret = util_http_build_header(client->refbuf->data, buf_len, 0,
                0, status, NULL,
                "text/xml", "utf-8",
                NULL, NULL, client);
        if (ret < 0) {
            ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
            client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
            xmlFree(buff);
            return;
        } else if (buf_len < (size_t)(len + location_length + ret + 128)) {
            void *new_data;
            buf_len = ret + len + 128;
            new_data = realloc(client->refbuf->data, buf_len);
            if (new_data) {
                ICECAST_LOG_DEBUG("Client buffer reallocation succeeded.");
                client->refbuf->data = new_data;
                client->refbuf->len = buf_len;
                ret = util_http_build_header(client->refbuf->data, buf_len, 0,
                        0, status, NULL,
                        "text/xml", "utf-8",
                        NULL, NULL, client);
                if (ret == -1) {
                    ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
                    client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
                    xmlFree(buff);
                    return;
                }
            } else {
                ICECAST_LOG_ERROR("Client buffer reallocation failed. Dropping client.");
                client_send_error_by_id(client, ICECAST_ERROR_GEN_BUFFER_REALLOC);
                xmlFree(buff);
                return;
            }
        }

        /* FIXME: in this section we hope no function will ever return -1 */
        if (location) {
            ret += snprintf(client->refbuf->data + ret, buf_len - ret, "Location: %s\r\n", location);
        }
        ret += snprintf(client->refbuf->data + ret, buf_len - ret, "Content-Length: %d\r\n\r\n%s", xmlStrlen(buff), buff);

        client->refbuf->len = ret;
        xmlFree(buff);
        client->respcode = status;
        fastevent_emit(FASTEVENT_TYPE_CLIENT_SEND_RESPONSE, FASTEVENT_FLAG_MODIFICATION_ALLOWED, FASTEVENT_DATATYPE_CLIENT, client);
        fserve_add_client (client, NULL);
    } else {
        char *fullpath_xslt_template;
        const char *document_domain_path;
        ssize_t fullpath_xslt_template_len;
        ice_config_t *config;

        config = config_get_config();
        switch (domain) {
            case DOCUMENT_DOMAIN_WEB:
                document_domain_path = config->webroot_dir;
            break;
            case DOCUMENT_DOMAIN_ADMIN:
                document_domain_path = config->adminroot_dir;
            break;
            default:
                config_release_config();
                ICECAST_LOG_ERROR("Invalid document domain. Sending 500 to client %p", client);
                client_send_500(client, "Invalid document domain.");
                return;
            break;
        }
        fullpath_xslt_template_len = strlen(document_domain_path) + strlen(xsl) + strlen(PATH_SEPARATOR) + 1;
        fullpath_xslt_template = malloc(fullpath_xslt_template_len);
        snprintf(fullpath_xslt_template, fullpath_xslt_template_len, "%s%s%s", document_domain_path, PATH_SEPARATOR, xsl);
        config_release_config();

        ICECAST_LOG_DEBUG("Sending XSLT (%s)", fullpath_xslt_template);
        fastevent_emit(FASTEVENT_TYPE_CLIENT_SEND_RESPONSE, FASTEVENT_FLAG_MODIFICATION_ALLOWED, FASTEVENT_DATATYPE_CLIENT, client);
        xslt_transform(doc, fullpath_xslt_template, client, status, location, NULL);
        free(fullpath_xslt_template);
    }

    xmlFreeDoc(doc);
}

static void client_get_reportxml__add_basic_stats(reportxml_t *report)
{
    reportxml_node_t *rootnode, *extension;
    xmlNodePtr xmlroot;
    xmlNodePtr modules;

    rootnode = reportxml_get_root_node(report);

    extension = reportxml_node_new(REPORTXML_NODE_TYPE_EXTENSION, NULL, NULL, NULL);
    reportxml_node_set_attribute(extension, "application", ADMIN_ICESTATS_LEGACY_EXTENSION_APPLICATION);

    reportxml_node_add_child(rootnode, extension);

    refobject_unref(rootnode);

    xmlroot = xmlNewNode(NULL, XMLSTR("icestats"));
    modules = module_container_get_modulelist_as_xml(global.modulecontainer);
    xmlAddChild(xmlroot, modules);


    reportxml_node_add_xml_child(extension, xmlroot);
    refobject_unref(extension);
    xmlFreeNode(xmlroot);
}

reportxml_t *client_get_reportxml(const char *state_definition, const char *state_akindof, const char *state_text)
{
    reportxml_t *report = NULL;

    if (state_definition) {
        ice_config_t *config;

        config = config_get_config();
        report = reportxml_database_build_report(config->reportxml_db, state_definition, -1);
        config_release_config();
    }

    if (!report) {
        reportxml_node_t *rootnode, *incidentnode, *statenode;

        report = refobject_new(reportxml_t);
        rootnode = reportxml_get_root_node(report);
        incidentnode = reportxml_node_new(REPORTXML_NODE_TYPE_INCIDENT, NULL, NULL, NULL);
        statenode = reportxml_node_new(REPORTXML_NODE_TYPE_STATE, NULL, state_definition, state_akindof);

        if (state_text) {
            reportxml_node_t *textnode;

            textnode = reportxml_node_new(REPORTXML_NODE_TYPE_TEXT, NULL, NULL, NULL);
            reportxml_node_set_content(textnode, state_text);
            reportxml_node_add_child(statenode, textnode);
            refobject_unref(textnode);
        }

        reportxml_node_add_child(incidentnode, statenode);
        reportxml_node_add_child(rootnode, incidentnode);
        refobject_unref(statenode);
        refobject_unref(incidentnode);
        refobject_unref(rootnode);
    }

    client_get_reportxml__add_basic_stats(report);

    return report;
}

admin_format_t client_get_admin_format_by_content_negotiation(client_t *client)
{
    const char *pref;

    if (!client || !client->parser)
        return CLIENT_DEFAULT_ADMIN_FORMAT;

    pref = util_http_select_best(httpp_getvar(client->parser, "accept"), "text/xml", "text/html", "text/plain", (const char*)NULL);

    if (strcmp(pref, "text/xml") == 0) {
        return ADMIN_FORMAT_RAW;
    } else if (strcmp(pref, "text/html") == 0) {
        return ADMIN_FORMAT_HTML;
    } else if (strcmp(pref, "text/plain") == 0) {
        return ADMIN_FORMAT_PLAINTEXT;
    } else {
        return CLIENT_DEFAULT_ADMIN_FORMAT;
    }
}

/* helper function for sending the data to a client */
int client_send_bytes(client_t *client, const void *buf, unsigned len)
{
    int ret = connection_send_bytes(client->con, buf, len);

    if (client->con->error)
        ICECAST_LOG_DEBUG("Client connection died");

    fastevent_emit(FASTEVENT_TYPE_CLIENT_WRITE, FASTEVENT_FLAG_NONE, FASTEVENT_DATATYPE_OBRD, client, buf, (size_t)len, (ssize_t)ret);

    return ret;
}

void client_set_queue(client_t *client, refbuf_t *refbuf)
{
    refbuf_t *to_release = client->refbuf;

    client->refbuf = refbuf;
    if (refbuf)
        refbuf_addref(client->refbuf);
    client->pos = 0;
    if (to_release)
        refbuf_release(to_release);
}

ssize_t client_body_read(client_t *client, void *buf, size_t len)
{
    ssize_t ret;

    ICECAST_LOG_DDEBUG("Reading from body (client=%p)", client);

    if (client->request_body_length != -1) {
        size_t left = (size_t)client->request_body_length - client->request_body_read;
        if (len > left) {
            ICECAST_LOG_DEBUG("Limiting read request to left over body size: left %zu byte, requested %zu byte", left, len);
            len = left;
        }
    }

    ret = client_read_bytes(client, buf, len);

    if (ret > 0) {
        client->request_body_read += ret;
    }

    fastevent_emit(FASTEVENT_TYPE_CLIENT_READ_BODY, FASTEVENT_FLAG_MODIFICATION_ALLOWED, FASTEVENT_DATATYPE_OBRD, client, buf, len, ret);

    return ret;
}

/* we might un-static this if needed at some time in distant future. -- ph3-der-loewe, 2018-04-17 */
static int client_eof(client_t *client)
{
    if (!client)
        return -1;

    if (!client->con)
        return 0;

    if (client->con->tls && tls_got_shutdown(client->con->tls) > 0)
        client->con->error = 1;

    if (client->con->error)
        return 1;

    return 0;
}

int client_body_eof(client_t *client)
{
    int ret = -1;

    if (!client)
        return -1;

    if (client->request_body_length != -1 && client->request_body_read == (size_t)client->request_body_length) {
        ICECAST_LOG_DDEBUG("Reached given body length (client=%p)", client);
        ret = 1;
    } else if (client->encoding) {
        ICECAST_LOG_DDEBUG("Looking for body EOF with encoding (client=%p)", client);
        ret = httpp_encoding_eof(client->encoding, (int(*)(void*))client_eof, client);
    } else {
        ICECAST_LOG_DDEBUG("Looking for body EOF without encoding (client=%p)", client);
        ret = client_eof(client);
    }

    ICECAST_LOG_DDEBUG("... result is: %i (client=%p)", ret, client);
    return ret;
}

client_slurp_result_t client_body_slurp(client_t *client, void *buf, size_t *len)
{
    if (!client || !buf || !len)
        return CLIENT_SLURP_ERROR;

    if (client->request_body_length != -1) {
        /* non-streaming mode */
        size_t left = (size_t)client->request_body_length - client->request_body_read;

        if (!left)
            return CLIENT_SLURP_SUCCESS;

        if (*len < (size_t)client->request_body_length)
            return CLIENT_SLURP_BUFFER_TO_SMALL;

        if (left > 2048)
            left = 2048;

        client_body_read(client, buf + client->request_body_read, left);

        if ((size_t)client->request_body_length == client->request_body_read) {
            *len = client->request_body_read;

            return CLIENT_SLURP_SUCCESS;
        } else {
            return CLIENT_SLURP_NEEDS_MORE_DATA;
        }
    } else {
        /* streaming mode */
        size_t left = *len - client->request_body_read;
        int ret;

        if (left) {
            if (left > 2048)
                left = 2048;

            client_body_read(client, buf + client->request_body_read, left);
        }

        ret = client_body_eof(client);
        switch (ret) {
            case 0:
                if (*len == client->request_body_read) {
                    return CLIENT_SLURP_BUFFER_TO_SMALL;
                }
                return CLIENT_SLURP_NEEDS_MORE_DATA;
            break;
            case 1:
                return CLIENT_SLURP_SUCCESS;
            break;
            default:
                return CLIENT_SLURP_ERROR;
            break;
        }
    }
}

client_slurp_result_t client_body_skip(client_t *client)
{
    char buf[2048];
    int ret;

    ICECAST_LOG_DEBUG("Slurping client %p", client);

    if (!client) {
        ICECAST_LOG_DEBUG("Slurping client %p ... failed", client);
        return CLIENT_SLURP_ERROR;
    }

    if (client->request_body_length != -1) {
        size_t left = (size_t)client->request_body_length - client->request_body_read;

        if (!left) {
            ICECAST_LOG_DEBUG("Slurping client %p ... was a success", client);
            return CLIENT_SLURP_SUCCESS;
        }

        if (left > sizeof(buf))
            left = sizeof(buf);

        client_body_read(client, buf, left);

        if ((size_t)client->request_body_length == client->request_body_read) {
            ICECAST_LOG_DEBUG("Slurping client %p ... was a success", client);
            return CLIENT_SLURP_SUCCESS;
        } else {
            ICECAST_LOG_DEBUG("Slurping client %p ... needs more data", client);
            return CLIENT_SLURP_NEEDS_MORE_DATA;
        }
    } else {
        client_body_read(client, buf, sizeof(buf));
    }

    ret = client_body_eof(client);
    switch (ret) {
        case 0:
            ICECAST_LOG_DEBUG("Slurping client %p ... needs more data", client);
            return CLIENT_SLURP_NEEDS_MORE_DATA;
        break;
        case 1:
            ICECAST_LOG_DEBUG("Slurping client %p ... was a success", client);
            return CLIENT_SLURP_SUCCESS;
        break;
        default:
            ICECAST_LOG_DEBUG("Slurping client %p ... failed", client);
            return CLIENT_SLURP_ERROR;
        break;
    }
}

ssize_t client_get_baseurl(client_t *client, listensocket_t *listensocket, char *buf, size_t len, const char *user, const char *pw, const char *prefix, const char *suffix0, const char *suffix1)
{
    const listener_t *listener = NULL;
    const ice_config_t *config = NULL;
    const char *host = NULL;
    const char *proto = "http";
    int port = 0;
    ssize_t ret;
    tlsmode_t tlsmode = ICECAST_TLSMODE_AUTO;
    protocol_t protocol = ICECAST_PROTOCOL_HTTP;

    if (!buf || !len)
        return -1;

    if (!prefix)
        prefix = "";

    if (!suffix0)
        suffix0 = "";

    if (!suffix1)
        suffix1 = "";

    if (client) {
        host = httpp_getvar(client->parser, "host");

        /* at least a couple of players (fb2k/winamp) are reported to send a
         * host header but without the port number. So if we are missing the
         * port then lets treat it as if no host line was sent */
        if (host && strchr(host, ':') == NULL)
            host = NULL;

        listensocket = client->con->listensocket_effective;
        tlsmode = client->con->tlsmode;
        protocol = client->protocol;
    }

    if (!host && listensocket) {
        listener = listensocket_get_listener(listensocket);
        if (listener) {
            host = listener->bind_address;
            port = listener->port;
            if (!client)
                tlsmode = listener->tls;
        }
    }

    if (!host) {
        config = config_get_config();
        host = config->hostname;
        if (!port)
            port = config->port;
    }

    switch (tlsmode) {
        case ICECAST_TLSMODE_DISABLED:
        case ICECAST_TLSMODE_AUTO:
            switch (protocol) {
                case ICECAST_PROTOCOL_HTTP: proto = "http"; break;
                case ICECAST_PROTOCOL_SHOUTCAST: proto = "icy"; break;
            }
            break;
        case ICECAST_TLSMODE_AUTO_NO_PLAIN:
        case ICECAST_TLSMODE_RFC2817:
        case ICECAST_TLSMODE_RFC2818:
            switch (protocol) {
                case ICECAST_PROTOCOL_HTTP: proto = "https"; break;
                case ICECAST_PROTOCOL_SHOUTCAST: proto = "icys"; break;
            }
            break;
    }

    if (host && port) {
        ret = snprintf(buf, len, "%s%s://%s%s%s%s%s:%i%s%s", prefix, proto, user ? user : "", pw ? ":" : "", pw ? pw : "", (user || pw) ? "@" : "", host, port, suffix0, suffix1);
    } else if (host) {
        ret = snprintf(buf, len, "%s%s://%s%s%s%s%s%s%s", prefix, proto, user ? user : "", pw ? ":" : "", pw ? pw : "", (user || pw) ? "@" : "", host, suffix0, suffix1);
    } else {
        ret = -1;
    }

    if (config)
        config_release_config();
    if (listener)
        listensocket_release_listener(listensocket);

    return ret;
}
