/*
 SPDX-License-Identifier: GPL-3.0-or-later
 myMPD (c) 2018-2026 Juergen Mang <mail@jcgames.de>
 https://github.com/jcorporation/mympd
*/

/*! \file
 * \brief Webserver implementation
 */

#include "compile_time.h"
#include "src/webserver/webserver.h"

#include "src/lib/api.h"
#include "src/lib/http_client/http_client.h"
#include "src/lib/json/json_rpc.h"
#include "src/lib/log.h"
#include "src/lib/mem.h"
#include "src/lib/msg_queue.h"
#include "src/lib/sds/sds_extras.h"
#include "src/lib/signal.h"
#include "src/lib/thread.h"
#include "src/webserver/albumart.h"
#include "src/webserver/folderart.h"
#include "src/webserver/placeholder.h"
#include "src/webserver/playlistart.h"
#include "src/webserver/proxy.h"
#include "src/webserver/request_handler.h"
#include "src/webserver/response.h"
#include "src/webserver/tagart.h"
#include "src/webserver/websocket.h"

#ifdef MYMPD_ENABLE_LUA
    #include "src/webserver/scripts.h"
#endif

#include <inttypes.h>

/**
 * Private definitions
 */
static void read_queue(struct mg_mgr *mgr);
static bool parse_internal_message(struct t_work_response *response, struct t_mg_user_data *mg_user_data);
static void ev_handler(struct mg_connection *nc, int ev, void *ev_data);
static void ev_handler_redirect(struct mg_connection *nc_http, int ev, void *ev_data);
static void mongoose_log(char ch, void *param);

/**
 * Public functions
 */

/**
 * Initializes the webserver
 * @param config pointer to myMPD config
 * @param mg_user_data already allocated t_mg_user_data to populate
 * @return struct mg_mgr* or NULL on error
 */
struct mg_mgr *webserver_init_mgr(struct t_config *config, struct t_mg_user_data *mg_user_data) {
    //init mongoose mgr
    mg_log_set(MG_LL_ERROR);
    mg_log_set_fn(mongoose_log, NULL);
    struct mg_mgr *mgr = malloc_assert(sizeof(struct mg_mgr));
    mg_mgr_init(mgr);
    mgr->userdata = mg_user_data;
    mgr->product_name = "myMPD "MYMPD_VERSION;
    mgr->directory_listing_css = DIRECTORY_LISTING_CSS;
    //set dns server
    mgr->dns4.url = get_dnsserver();
    MYMPD_LOG_DEBUG(NULL, "Setting dns server to %s", mgr->dns4.url);

    //bind to http_port
    if (config->http == true) {
        struct mg_connection *nc_http;
        sds http_url = sdscatfmt(sdsempty(), "http://%S:%i", config->http_host, config->http_port);
        if (config->ssl == true) {
            nc_http = mg_http_listen(mgr, http_url, ev_handler_redirect, NULL);
        }
        else {
            nc_http = mg_http_listen(mgr, http_url, ev_handler, NULL);
        }
        FREE_SDS(http_url);
        if (nc_http == NULL) {
            MYMPD_LOG_EMERG(NULL, "Can't bind to http://%s:%d", config->http_host, config->http_port);
            webserver_free(mgr);
            return NULL;
        }
        MYMPD_LOG_NOTICE(NULL, "Listening on http://%s:%d", config->http_host, config->http_port);
    }
    //bind to ssl_port
    if (config->ssl == true) {
        sds https_url = sdscatfmt(sdsempty(), "https://%S:%i", config->http_host, config->ssl_port);
        struct mg_connection *nc_https = mg_http_listen(mgr, https_url, ev_handler, NULL);
        FREE_SDS(https_url);
        if (nc_https == NULL) {
            MYMPD_LOG_ERROR(NULL, "Can't bind to https://%s:%d", config->http_host, config->ssl_port);
            webserver_free(mgr);
            return NULL;
        }
        MYMPD_LOG_NOTICE(NULL, "Listening on https://%s:%d", config->http_host, config->ssl_port);
        MYMPD_LOG_DEBUG(NULL, "Using certificate: %s", config->ssl_cert);
        MYMPD_LOG_DEBUG(NULL, "Using private key: %s", config->ssl_key);
    }
    else if (config->http == false) {
        MYMPD_LOG_ERROR(NULL, "Not listening on any port.");
        webserver_free(mgr);
        return NULL;
    }
    MYMPD_LOG_NOTICE(NULL, "Serving files from \"%s\"", MYMPD_DOC_ROOT);
    // Initialize the wakeup scheme
    if (mg_wakeup_init(mgr) == false) {
        MYMPD_LOG_EMERG(NULL, "Failure initializing webserver wakeup scheme");
        webserver_free(mgr);
        return NULL;
    }
    return mgr;
}

/**
 * Frees the mongoose mgr
 * @param mgr mongoose mgr to free
 */
void webserver_free(struct mg_mgr *mgr) {
    if (mgr->dns4.url != NULL) {
        sds dns4_url = (sds)mgr->dns4.url;
        FREE_SDS(dns4_url);
    }
    mg_mgr_free(mgr);
    FREE_PTR(mgr);
}

/**
 * Main function for the webserver thread
 * @param arg_mgr void pointer to mongoose mgr
 * @return NULL
 */
void *webserver_loop(void *arg_mgr) {
    thread_logname = sdsnew("webserver");
    set_threadname(thread_logname);
    thread_logline = sdsempty();

    struct mg_mgr *mgr = (struct mg_mgr *) arg_mgr;
    MYMPD_LOG_DEBUG(NULL, "Webserver thread is ready");
    // Initially read the queue
    read_queue(mgr);
    // Webserver polling
    while (s_signal_received == 0) {
        mg_mgr_poll(mgr, -1);
    }
    MYMPD_LOG_DEBUG(NULL, "Webserver thread stopped");
    FREE_SDS(thread_logname);
    FREE_SDS(thread_logline);
    return NULL;
}

/**
 * Private functions
 */

/**
 * Reads and processes all messages from the webserver queue.
 * This function does not block and returns immediately if the queue is empty.
 * @param mgr pointer to mongoose mgr
 */
static void read_queue(struct mg_mgr *mgr) {
    struct t_mg_user_data *mg_user_data = (struct t_mg_user_data *) mgr->userdata;
    struct t_work_response *response;
    while ((response = mympd_queue_shift(webserver_queue, -1, 0)) != NULL) {
        switch(response->type) {
            case RESPONSE_TYPE_SCRIPT_DIALOG:
            case RESPONSE_TYPE_NOTIFY_CLIENT:
                //websocket notify for specific clients
                websocket_send_notify_client(mgr, response);
                break;
            case RESPONSE_TYPE_NOTIFY_PARTITION:
                //websocket notify for all clients
                websocket_send_notify(mgr, response);
                break;
            case RESPONSE_TYPE_PUSH_CONFIG:
                //internal message
                if (response->cmd_id == INTERNAL_API_WEBSERVER_READY) {
                    mg_user_data->mympd_api_started = true;
                    free_response(response);
                }
                else if (response->cmd_id == INTERNAL_API_WEBSERVER_SETTINGS){
                    parse_internal_message(response, mg_user_data);
                }
                else {
                    MYMPD_LOG_ERROR(response->partition, "Invalid API method: %s", get_cmd_id_method_name(response->cmd_id));
                }
                break;
            case RESPONSE_TYPE_DEFAULT:
                //api response
                MYMPD_LOG_DEBUG(response->partition, "Got API response for id \"%lu\"", response->conn_id);
                webserver_send_api_response(mgr, response);
                break;
            case RESPONSE_TYPE_RAW:
                MYMPD_LOG_DEBUG(response->partition, "Got raw response for id \"%lu\" with %lu bytes", response->conn_id, (unsigned long)sdslen(response->data));
                webserver_send_raw_response(mgr, response);
                break;
            case RESPONSE_TYPE_REDIRECT:
                MYMPD_LOG_DEBUG(response->partition, "Got redirect for id \"%lu\" to %s", response->conn_id, response->data);
                webserver_send_redirect(mgr, response);
                break;
            case RESPONSE_TYPE_SCRIPT:
            case RESPONSE_TYPE_DISCARD:
                //ignore
                break;
        }
    }
}

/**
 * Sets the mg_user_data values from set_mg_user_data_request.
 * Message is sent from the mympd_api thread.
 * @param response Response with the data to set
 * @param mg_user_data t_mg_user_data to configure
 * @return true on success, else false
 */
static bool parse_internal_message(struct t_work_response *response, struct t_mg_user_data *mg_user_data) {
    bool rc = false;
    if (response->extra != NULL) {
        struct set_mg_user_data_request *new_mg_user_data = (struct set_mg_user_data_request *)response->extra;
        struct t_config *config = mg_user_data->config;

        sdsclear(mg_user_data->browse_directory);
        mg_user_data->browse_directory = sdscatfmt(mg_user_data->browse_directory, "%S/%s", config->workdir, DIR_WORK_EMPTY);
        mg_user_data->browse_directory = sdscatfmt(mg_user_data->browse_directory, ",/browse/pics=%S/%s", config->workdir, DIR_WORK_PICS);
        mg_user_data->browse_directory = sdscatfmt(mg_user_data->browse_directory, ",/browse/smartplaylists=%S/%s", config->workdir, DIR_WORK_SMARTPLS);
        if (sdslen(new_mg_user_data->playlist_directory) > 0) {
            mg_user_data->browse_directory = sdscatfmt(mg_user_data->browse_directory, ",/browse/playlists=%S", new_mg_user_data->playlist_directory);
            mg_user_data->publish_playlists = true;
        }
        else {
            mg_user_data->publish_playlists = false;
        }
        FREE_SDS(new_mg_user_data->playlist_directory);

        if (sdslen(new_mg_user_data->music_directory) > 0)
        {
            mg_user_data->browse_directory = sdscatfmt(mg_user_data->browse_directory, ",/browse/music=%S", new_mg_user_data->music_directory);
            mg_user_data->publish_music = true;
        }
        else {
            mg_user_data->publish_music = false;
        }
        mg_user_data->music_directory = sds_replace(mg_user_data->music_directory, new_mg_user_data->music_directory);
        FREE_SDS(new_mg_user_data->music_directory);
        MYMPD_LOG_DEBUG(NULL, "Document root: \"%s\"", mg_user_data->browse_directory);

        //webradios
        mg_user_data->webradiodb = new_mg_user_data->webradiodb;
        mg_user_data->webradio_favorites = new_mg_user_data->webradio_favorites;

        //coverimage names
        sdsfreesplitres(mg_user_data->image_names_sm, mg_user_data->image_names_sm_len);
        mg_user_data->image_names_sm = sds_split_comma_trim(new_mg_user_data->image_names_sm, &mg_user_data->image_names_sm_len);
        FREE_SDS(new_mg_user_data->image_names_sm);

        sdsfreesplitres(mg_user_data->image_names_md, mg_user_data->image_names_md_len);
        mg_user_data->image_names_md = sds_split_comma_trim(new_mg_user_data->image_names_md, &mg_user_data->image_names_md_len);
        FREE_SDS(new_mg_user_data->image_names_md);

        sdsfreesplitres(mg_user_data->image_names_lg, mg_user_data->image_names_lg_len);
        mg_user_data->image_names_lg = sds_split_comma_trim(new_mg_user_data->image_names_lg, &mg_user_data->image_names_lg_len);
        FREE_SDS(new_mg_user_data->image_names_lg);

        // Lyrics
        mg_user_data->lyrics.uslt_ext = sds_replace(mg_user_data->lyrics.uslt_ext, new_mg_user_data->lyrics.uslt_ext);
        FREE_SDS(new_mg_user_data->lyrics.uslt_ext);
        mg_user_data->lyrics.sylt_ext = sds_replace(mg_user_data->lyrics.sylt_ext, new_mg_user_data->lyrics.sylt_ext);
        FREE_SDS(new_mg_user_data->lyrics.sylt_ext);
        mg_user_data->lyrics.vorbis_uslt = sds_replace(mg_user_data->lyrics.vorbis_uslt, new_mg_user_data->lyrics.vorbis_uslt);
        FREE_SDS(new_mg_user_data->lyrics.vorbis_uslt);
        mg_user_data->lyrics.vorbis_sylt = sds_replace(mg_user_data->lyrics.vorbis_sylt, new_mg_user_data->lyrics.vorbis_sylt);
        FREE_SDS(new_mg_user_data->lyrics.vorbis_sylt);

        //set per partition stream uris
        list_clear(&mg_user_data->stream_uris);
        struct t_list_node *current = new_mg_user_data->partitions.head;
        sds uri = sdsempty();
        while (current != NULL) {
            if (current->value_i > 0) {
                uri = sdscatfmt(uri, "http://%s:%I",
                    (new_mg_user_data->mpd_host[0] == '/' ? "127.0.0.1" : new_mg_user_data->mpd_host),
                    current->value_i);
                MYMPD_LOG_DEBUG(current->key, "Setting stream uri to \"%s\"", uri);
                list_push(&mg_user_data->stream_uris, current->key, 0, uri, NULL);
            }
            sdsclear(uri);
            current = current->next;
        }
        FREE_SDS(uri);

        //custom placeholder images
        get_placeholder_image(config->workdir, "coverimage-booklet", &mg_user_data->placeholder_booklet);
        get_placeholder_image(config->workdir, "coverimage-folder", &mg_user_data->placeholder_folder);
        get_placeholder_image(config->workdir, "coverimage-mympd", &mg_user_data->placeholder_mympd);
        get_placeholder_image(config->workdir, "coverimage-notavailable", &mg_user_data->placeholder_na);
        get_placeholder_image(config->workdir, "coverimage-playlist", &mg_user_data->placeholder_playlist);
        get_placeholder_image(config->workdir, "coverimage-smartpls", &mg_user_data->placeholder_smartpls);
        get_placeholder_image(config->workdir, "coverimage-stream", &mg_user_data->placeholder_stream);
        get_placeholder_image(config->workdir, "coverimage-transparent", &mg_user_data->placeholder_transparent);

        //cleanup
        FREE_SDS(new_mg_user_data->mpd_host);
        list_clear(&new_mg_user_data->partitions);
        FREE_PTR(response->extra);
        response->extra = NULL;
        rc = true;
    }
    else {
        MYMPD_LOG_WARN(NULL, "Invalid internal message: %s", response->data);
    }
    free_response(response);
    return rc;
}

/**
 * Handles the data from the MG_EV_WAKEUP event
 * @param nc Mongoose connection
 * @param data Received data
 */
static void handle_wakeup(struct mg_connection *nc, struct mg_str *data) {
    switch(data->buf[0]) {
        case 'Q':
            read_queue(nc->mgr);
            break;
        case 'X':
            MYMPD_LOG_DEBUG(NULL, "Wakeup mongoose polling");
            break;
        default:
            MYMPD_LOG_ERROR(NULL, "Unhandled wakeup data received");
    }
}

/**
 * Validates the request
 * @param nc Mongoose conncection
 * @param hm HTTP Request
 * @return true on valid request, else false
 */
static bool check_request(struct mg_connection *nc, struct mg_http_message *hm) {
    // First log it
    if (hm->query.len > 0) {
        MYMPD_LOG_INFO(NULL, "HTTP request (%lu): %.*s %.*s?%.*s %.*s",
            nc->id,
            (int)hm->method.len, hm->method.buf,
            (int)hm->uri.len, hm->uri.buf,
            (int)hm->query.len, hm->query.buf,
            (int)hm->proto.len, hm->proto.buf
        );
    }
    else {
        MYMPD_LOG_INFO(NULL, "HTTP request (%lu): %.*s %.*s %.*s",
            nc->id,
            (int)hm->method.len, hm->method.buf,
            (int)hm->uri.len, hm->uri.buf,
            (int)hm->proto.len, hm->proto.buf
        );
    }
    //limit protocol
    if (mg_strcmp(hm->proto, mg_str("HTTP/1.1")) == 0) {
        nc->data[2] = 'K';
    }
    else if (mg_strcmp(hm->proto, mg_str("HTTP/1.0")) == 0) {
        nc->data[2] = 'C';
    }
    else {
        MYMPD_LOG_ERROR(NULL, "Invalid http protocol \"%.*s\" (%lu)", (int)hm->proto.len, hm->proto.buf, nc->id);
        webserver_send_error(nc, 405, "Invalid http protocol");
        nc->is_draining = 1;
        return false;
    }
    //limit allowed http methods
    if (mg_strcmp(hm->method, mg_str("GET")) == 0) {
        nc->data[1] = 'G';
    }
    else if (mg_strcmp(hm->method, mg_str("HEAD")) == 0) {
        nc->data[1] = 'H';
    }
    else if (mg_strcmp(hm->method, mg_str("POST")) == 0) {
        nc->data[1] = 'P';
    }
    else if (mg_strcmp(hm->method, mg_str("OPTIONS")) == 0) {
        nc->data[1] = 'O';
        webserver_send_cors_reply(nc);
        return false;
    }
    else {
        MYMPD_LOG_ERROR(NULL, "Invalid http method \"%.*s\" (%lu)", (int)hm->method.len, hm->method.buf, nc->id);
        webserver_send_error(nc, 405, "Invalid http method");
        //nc->is_draining = 1;
        return false;
    }
    //check uri length
    if (hm->uri.len > URI_LENGTH_MAX) {
        MYMPD_LOG_ERROR(NULL, "Uri is too long, length is %lu, maximum length is %d (%lu)", (unsigned long)hm->uri.len, URI_LENGTH_MAX, nc->id);
        webserver_send_error(nc, 414, "Uri is too long");
        //nc->is_draining = 1;
        return false;
    }
    //check post requests length
    if (nc->data[1] == 'P' && (hm->body.len == 0 || hm->body.len > BODY_SIZE_MAX)) {
        MYMPD_LOG_ERROR(NULL, "POST request with body of size %lu is out of bounds (%lu)", (unsigned long)hm->body.len, nc->id);
        webserver_send_error(nc, 413, "Post request is too large");
        //nc->is_draining = 1;
        return false;
    }
    //respect connection close header
    struct mg_str *connection_hdr = mg_http_get_header(hm, "Connection");
    if (connection_hdr != NULL) {
        if (mg_strcasecmp(*connection_hdr, mg_str("close")) == 0) {
            nc->data[2] = 'C';
        }
        else {
            nc->data[2] = 'K';
        }
    }
    return true;
}

/**
 * Central webserver event handler
 * nc->label usage
 * 0 - connection type: F = frontend connection, B = backend connection
 * 1 - http method: G = GET, H = HEAD, P = POST
 * 2 - connection header: C = close, K = keepalive
 *
 * @param nc mongoose connection
 * @param ev connection event
 * @param ev_data event data (http / websocket message)
 */
static void ev_handler(struct mg_connection *nc, int ev, void *ev_data) {
    //connection specific data structure
    struct t_frontend_nc_data *frontend_nc_data = (struct t_frontend_nc_data *) nc->fn_data;
    //mongoose user data
    struct t_mg_user_data *mg_user_data = (struct t_mg_user_data *) nc->mgr->userdata;
    struct t_config *config = mg_user_data->config;
    switch(ev) {
        case MG_EV_OPEN: {
            if (nc->is_listening) {
                MYMPD_LOG_DEBUG(NULL, "Main listening connection opened");
                nc->fn_data = NULL;
                webserver_queue->mg_conn_id = nc->id;
                webserver_queue->mg_mgr = nc->mgr;
            }
            else {
                mg_user_data->connection_count++;
                //initialize fn_data
                frontend_nc_data = malloc_assert(sizeof(struct t_frontend_nc_data));
                frontend_nc_data->partition = NULL;           // populated on websocket handshake
                frontend_nc_data->id = 0;                     // populated through websocket message
                frontend_nc_data->last_ws_ping = time(NULL);  // websocket ping timestamp
                frontend_nc_data->backend_nc = NULL;          // used for reverse proxy function
                nc->fn_data = frontend_nc_data;
                //set labels
                nc->data[0] = 'F'; // connection type
                nc->data[1] = '-'; // http method
                nc->data[2] = '-'; // connection header
            }
            break;
        }
        case MG_EV_WAKEUP: {
            struct mg_str *data = (struct mg_str *) ev_data;
            handle_wakeup(nc, data);
            break;
        }
        case MG_EV_ACCEPT:
            if (loglevel == LOG_DEBUG) {
                sds ip = print_ip(sdsempty(), &nc->rem);
                MYMPD_LOG_DEBUG(NULL, "New connection id \"%lu\" from %s, connections: %d", nc->id, ip, mg_user_data->connection_count);
                FREE_SDS(ip);
            }
            //ssl support
            if (config->ssl == true) {
                mg_tls_init(nc, &mg_user_data->tls_opts);
            }
            //enforce connection limit
            if (webserver_enforce_conn_limit(nc, mg_user_data->connection_count) == false) {
                break;
            }
            //enforce acl
            if (webserver_enforce_acl(nc, config->acl) == false) {
                break;
            }
            break;
        case MG_EV_WS_OPEN: {
            nc->is_resp = 1;
            break;
        }
        case MG_EV_WS_MSG: {
            struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
            struct mg_str matches[1];
            size_t sent = 0;
            if (wm->data.len > 9) {
                MYMPD_LOG_ERROR(frontend_nc_data->partition, "Websocket (%lu) message too long: %lu", nc->id, (unsigned long)wm->data.len);
                sent = mg_ws_send(nc, "too long", 8, WEBSOCKET_OP_TEXT);
            }
            else if (mg_strcmp(wm->data, mg_str("ping")) == 0) {
                sent = mg_ws_send(nc, "pong", 4, WEBSOCKET_OP_TEXT);
            }
            else if (mg_match(wm->data, mg_str("id:*"), matches)) {
                if (mg_str_to_num(matches[0], 10, &frontend_nc_data->id, sizeof(frontend_nc_data->id)) == true) {
                    MYMPD_LOG_INFO(frontend_nc_data->partition, "Setting websocket (%lu) id to \"%u\"", nc->id, frontend_nc_data->id);
                    sent = mg_ws_send(nc, "ok", 2, WEBSOCKET_OP_TEXT);
                }
                else {
                    MYMPD_LOG_ERROR(frontend_nc_data->partition, "Websocket (%lu): Invalid message, closing connection", nc->id);
                }
            }
            else {
                MYMPD_LOG_DEBUG(frontend_nc_data->partition, "Websocket (%lu) message: %.*s", nc->id, (int)wm->data.len, wm->data.buf);
                MYMPD_LOG_ERROR(frontend_nc_data->partition, "Websocket (%lu): Invalid message", nc->id);
                sent = mg_ws_send(nc, "invalid", 7, WEBSOCKET_OP_TEXT);
            }
            if (sent == 0) {
                MYMPD_LOG_ERROR(frontend_nc_data->partition, "Websocket (%lu): Could not reply, closing connection", nc->id);
                nc->is_closing = 1;
            }
            else {
                frontend_nc_data->last_ws_ping = time(NULL);
            }
            break;
        }
        case MG_EV_HTTP_MSG: {
            nc->is_resp = 1;
            struct mg_http_message *hm = (struct mg_http_message *) ev_data;
            if (check_request(nc, hm) == false) {
                break;
            }
            //handle uris
            if (mg_match(hm->uri, mg_str("/api/*"), NULL)) {
                //api request
                if (mg_user_data->mympd_api_started == false) {
                    //mympd_api thread not yet ready
                    MYMPD_LOG_WARN(frontend_nc_data->partition, "mympd_api thread not yet ready");
                    webserver_send_jsonrpc_response(nc, GENERAL_API_NOT_READY, 0, JSONRPC_FACILITY_GENERAL, JSONRPC_SEVERITY_ERROR, "myMPD not yet ready");
                }
                //check partition
                if (get_partition_from_uri(nc, hm, frontend_nc_data) == false) {
                    break;
                }
                //body
                sds body = sdsnewlen(hm->body.buf, hm->body.len);
                /*
                 * We use the custom header X-myMPD-Session for authorization
                 * to allow other authorization methods in reverse proxy setups
                 */
                struct mg_str *auth_header = mg_http_get_header(hm, "X-myMPD-Session");
                bool rc = request_handler_api(nc, body, auth_header, mg_user_data);
                FREE_SDS(body);
                if (rc == false) {
                    MYMPD_LOG_ERROR(frontend_nc_data->partition, "Invalid API request");
                    webserver_send_jsonrpc_response(nc, GENERAL_API_UNKNOWN, 0, JSONRPC_FACILITY_GENERAL, JSONRPC_SEVERITY_ERROR, "Invalid API request");
                }
            }
            else if (mg_match(hm->uri, mg_str("/albumart-large/*"), NULL)) {
                request_handler_albumart_by_album_id(hm, nc->id, ALBUMART_LG);
            }
            else if (mg_match(hm->uri, mg_str("/albumart-thumb/*"), NULL)) {
                request_handler_albumart_by_album_id(hm, nc->id, ALBUMART_SM);
            }
            else if (mg_match(hm->uri, mg_str("/albumart/*"), NULL)) {
                request_handler_albumart_by_album_id(hm, nc->id, ALBUMART_MD);
            }
            else if (mg_match(hm->uri, mg_str("/albumart-large"), NULL)) {
                request_handler_albumart_by_uri(nc, hm, ALBUMART_LG);
            }
            else if (mg_match(hm->uri, mg_str("/albumart-thumb"), NULL)) {
                request_handler_albumart_by_uri(nc, hm, ALBUMART_SM);
            }
            else if (mg_match(hm->uri, mg_str("/albumart"), NULL)) {
                request_handler_albumart_by_uri(nc, hm, ALBUMART_MD);
            }
            else if (mg_match(hm->uri, mg_str("/folderart"), NULL)) {
                request_handler_folderart(nc, hm);
            }
            else if (mg_match(hm->uri, mg_str("/tagart"), NULL)) {
                request_handler_tagart(nc, hm);
            }
            else if (mg_match(hm->uri, mg_str("/playlistart"), NULL)) {
                request_handler_playlistart(nc, hm);
            }
            else if (mg_match(hm->uri, mg_str("/browse/#"), NULL)) {
                request_handler_browse(nc, hm, mg_user_data);
            }
            else if (mg_match(hm->uri, mg_str("/webradio"), NULL)) {
                request_handler_extm3u(nc, hm, mg_user_data);
            }
            else if (mg_match(hm->uri, mg_str("/ws/*"), NULL)) {
                //check partition
                if (get_partition_from_uri(nc, hm, frontend_nc_data) == false) {
                    break;
                }
                mg_ws_upgrade(nc, hm, NULL);
                MYMPD_LOG_INFO(frontend_nc_data->partition, "New Websocket connection established (%lu)", nc->id);
                sds response = jsonrpc_event(sdsempty(), JSONRPC_EVENT_WELCOME);
                mg_ws_send(nc, response, sdslen(response), WEBSOCKET_OP_TEXT);
                FREE_SDS(response);
            }
            else if (mg_match(hm->uri, mg_str("/stream/*"), NULL)) {
                //check partition
                if (get_partition_from_uri(nc, hm, frontend_nc_data) == false) {
                    break;
                }
                struct t_list_node *node = list_get_node(&mg_user_data->stream_uris, frontend_nc_data->partition);
                if (node == NULL) {
                    webserver_send_error(nc, 404, "Stream uri not configured");
                    //nc->is_draining = 1;
                    break;
                }
                create_backend_connection(nc, frontend_nc_data->backend_nc, node->value_p, forward_backend_to_frontend_stream, true);
            }
            else if (mg_match(hm->uri, mg_str("/proxy"), NULL)) {
                //Makes a get request to the defined uri and returns the response
                request_handler_proxy(nc, hm, frontend_nc_data->backend_nc);
            }
            else if (mg_match(hm->uri, mg_str("/proxy-covercache"), NULL)) {
                //Makes a get request to the defined uri and caches and returns the response
                request_handler_proxy_covercache(nc, hm, frontend_nc_data->backend_nc);
            }
            else if (mg_match(hm->uri, mg_str("/serverinfo"), NULL)) {
                request_handler_serverinfo(nc);
            }
        #ifdef MYMPD_ENABLE_LUA
            else if (mg_match(hm->uri, mg_str("/script-api/*"), NULL)) {
                if (config->scripts_external == false) {
                    MYMPD_LOG_ERROR(frontend_nc_data->partition, "External scripts are disabled");
                    webserver_send_jsonrpc_response(nc, GENERAL_API_UNKNOWN, 0, JSONRPC_FACILITY_SCRIPT, JSONRPC_SEVERITY_ERROR, "External scripts are disabled");
                }
                //enforce script acl
                if (webserver_enforce_acl(nc, config->scriptacl) == false) {
                    break;
                }
                //check partition
                if (get_partition_from_uri(nc, hm, frontend_nc_data) == false) {
                    break;
                }
                sds body = sdsnewlen(hm->body.buf, hm->body.len);
                bool rc = request_handler_script_api(nc, body);
                FREE_SDS(body);
                if (rc == false) {
                    MYMPD_LOG_ERROR(frontend_nc_data->partition, "Invalid script API request");
                    webserver_send_jsonrpc_response(nc, GENERAL_API_UNKNOWN, 0, JSONRPC_FACILITY_SCRIPT, JSONRPC_SEVERITY_ERROR, "Invalid script API request");
                }
            }
            else if (mg_match(hm->uri, mg_str("/script/*/*"), NULL)) {
                script_execute_http(nc, hm, config);
            }
            else if (mg_match(hm->uri, mg_str("/bgimage/*"), NULL)) {
                script_execute_bgimage(nc, hm);
            }
        #endif
            else if (mg_match(hm->uri, mg_str("/assets/coverimage-booklet"), NULL)) {
                webserver_serve_placeholder_image(nc, hm, mg_user_data->placeholder_booklet);
            }
            else if (mg_match(hm->uri, mg_str("/assets/coverimage-folder"), NULL)) {
                webserver_serve_placeholder_image(nc, hm, mg_user_data->placeholder_folder);
            }
            else if (mg_match(hm->uri, mg_str("/assets/coverimage-mympd"), NULL)) {
                webserver_serve_placeholder_image(nc, hm, mg_user_data->placeholder_mympd);
            }
            else if (mg_match(hm->uri, mg_str("/assets/coverimage-notavailable"), NULL)) {
                webserver_serve_placeholder_image(nc, hm, mg_user_data->placeholder_na);
            }
            else if (mg_match(hm->uri, mg_str("/assets/coverimage-smartpls"), NULL)) {
                webserver_serve_placeholder_image(nc, hm, mg_user_data->placeholder_smartpls);
            }
            else if (mg_match(hm->uri, mg_str("/assets/coverimage-stream"), NULL)) {
                webserver_serve_placeholder_image(nc, hm, mg_user_data->placeholder_stream);
            }
            else if (mg_match(hm->uri, mg_str("/assets/coverimage-playlist"), NULL)) {
                webserver_serve_placeholder_image(nc, hm, mg_user_data->placeholder_playlist);
            }
            else if (mg_match(hm->uri, mg_str("/assets/coverimage-transparent"), NULL)) {
                webserver_serve_placeholder_image(nc, hm, mg_user_data->placeholder_transparent);
            }
            else if (mg_match(hm->uri, mg_str("/custom.css"), NULL)) {
                webserver_send_data(nc, config->custom_css, sdslen(config->custom_css), EXTRA_HEADERS_CSS);
            }
            else if (mg_match(hm->uri, mg_str("/custom.js"), NULL)) {
                webserver_send_data(nc, config->custom_js, sdslen(config->custom_js), EXTRA_HEADERS_JS);
            }
            else if (mg_match(hm->uri, mg_str("/index.html"), NULL)) {
                webserver_send_header_redirect(nc, "/", "");
            }
            else if (mg_match(hm->uri, mg_str("/favicon.ico"), NULL)) {
                webserver_send_header_redirect(nc, "/assets/appicon-192.png", "");
            }
            else if (mg_match(hm->uri, mg_str("/ca.crt"), NULL)) {
                request_handler_ca(nc, hm, mg_user_data);
            }
            else {
                //all other uris
                #ifndef MYMPD_EMBEDDED_ASSETS
                    //serve all files from filesystem
                    static struct mg_http_serve_opts s_http_server_opts;
                    s_http_server_opts.root_dir = MYMPD_DOC_ROOT;
                    if (mg_match(hm->uri, mg_str("/test/#"), NULL)) {
                        //test suite uses innerHTML
                        s_http_server_opts.extra_headers = EXTRA_HEADERS_UNSAFE;
                    }
                    else {
                        s_http_server_opts.extra_headers = nc->data[2] == 'C'
                            ? EXTRA_HEADERS_SAFE_CLOSE
                            : EXTRA_HEADERS_SAFE_KEEP_ALIVE;
                    }
                    s_http_server_opts.mime_types = EXTRA_MIME_TYPES;
                    mg_http_serve_dir(nc, hm, &s_http_server_opts);
                #else
                    //serve embedded files
                    sds uri = sdsnewlen(hm->uri.buf, hm->uri.len);
                    webserver_serve_embedded_files(nc, uri);
                    FREE_SDS(uri);
                #endif
            }
            break;
        }
        case MG_EV_CLOSE: {
            if (nc->is_websocket == 1) {
                MYMPD_LOG_INFO(NULL, "Websocket connection \"%lu\" closed", nc->id);
            }
            else {
                MYMPD_LOG_INFO(NULL, "HTTP connection \"%lu\" closed", nc->id);
            }
            mg_user_data->connection_count--;
            if (frontend_nc_data == NULL) {
                if (nc->is_listening == 0) {
                    MYMPD_LOG_WARN(NULL, "frontend_nc_data not allocated");
                }
                break;
            }
            if (frontend_nc_data->backend_nc != NULL) {
                MYMPD_LOG_INFO(NULL, "Closing backend connection \"%lu\"", frontend_nc_data->backend_nc->id);
                //remove pointer to frontend connection
                struct t_backend_nc_data *backend_nc_data = (struct t_backend_nc_data *)frontend_nc_data->backend_nc->fn_data;
                backend_nc_data->frontend_nc = NULL;
                //close backend connection
                frontend_nc_data->backend_nc->is_closing = 1;
            }
            FREE_SDS(frontend_nc_data->partition);
            FREE_PTR(frontend_nc_data);
            nc->fn_data = NULL;
            break;
        }
    }
}

/**
 * Redirects the client to https if ssl is enabled.
 * Only requests to /browse/webradios are not redirected.
 * @param nc mongoose connection
 * @param ev connection event
 * @param ev_data event data (http / websocket message)
 */
static void ev_handler_redirect(struct mg_connection *nc, int ev, void *ev_data) {
    struct t_mg_user_data *mg_user_data = (struct t_mg_user_data *) nc->mgr->userdata;
    struct t_config *config = mg_user_data->config;

    switch(ev) {
        case MG_EV_OPEN:
            if (nc->is_listening) {
                MYMPD_LOG_DEBUG(NULL, "Redirect listening connection opened");
            }
            mg_user_data->connection_count++;
            //set labels
            nc->data[0] = 'F'; // connection type
            nc->data[1] = '-'; // http method
            nc->data[2] = '-'; // connection header
            break;
        case MG_EV_ACCEPT:
            //enforce connection limit
            if (webserver_enforce_conn_limit(nc, mg_user_data->connection_count) == false) {
                break;
            }
            //enforce acl
            if (webserver_enforce_acl(nc, config->acl) == false) {
                break;
            }
            break;
        case MG_EV_HTTP_MSG: {
            struct mg_http_message *hm = (struct mg_http_message *) ev_data;
            if (check_request(nc, hm) == false) {
                break;
            }
            // Serve some directories without ssl
            if (mg_match(hm->uri, mg_str("/webradio"), NULL)) {
                request_handler_extm3u(nc, hm, mg_user_data);
                break;
            }
            #ifdef MYMPD_ENABLE_LUA
                if (mg_match(hm->uri, mg_str("/script/*/*"), NULL)) {
                    script_execute_http(nc, hm, config);
                    break;
                }
            #endif
            //redirect to https
            struct mg_str *host_hdr = mg_http_get_header(hm, "Host");
            if (host_hdr == NULL) {
                MYMPD_LOG_ERROR(NULL, "No host header found, closing connection");
                webserver_send_error(nc, 405, "No host header found");
                //nc->is_closing = 1;
                break;
            }

            sds host_header = sdscatlen(sdsempty(), host_hdr->buf, host_hdr->len);
            int count = 0;
            sds *tokens = sdssplitlen(host_header, (ssize_t)sdslen(host_header), ":", 1, &count);
            sds s_redirect = sdscatfmt(sdsempty(), "https://%S", tokens[0]);
            if (config->ssl_port != 443) {
                s_redirect = sdscatfmt(s_redirect, ":%i", config->ssl_port);
            }
            MYMPD_LOG_INFO(NULL, "Redirecting to %s", s_redirect);
            webserver_send_header_found(nc, s_redirect, "");
            //nc->is_draining = 1;
            sdsfreesplitres(tokens, count);
            FREE_SDS(host_header);
            FREE_SDS(s_redirect);
            break;
        }
        case MG_EV_CLOSE:
            MYMPD_LOG_INFO(NULL, "Connection \"%lu\" closed", nc->id);
            mg_user_data->connection_count--;
            break;
    }
}

/**
 * Mongoose logging function
 * @param ch character to log
 * @param param
 */
static void mongoose_log(char ch, void *param) {
    static char buf[256];
    static size_t len;
    buf[len++] = ch;
    if (ch == '\n' ||
        len >= sizeof(buf))
    {
        if (ch == '\n') {
            //Remove newline
            len--;
        }
        //we log only mongoose errors
        MYMPD_LOG_ERROR(NULL, "%.*s", (int) len, buf); //Send logs
        len = 0;
    }
    (void)param;
}
