admin.c 42.3 KB
Newer Older
1 2 3 4 5
/* Icecast
 *
 * This program is distributed under the GNU General Public License, version 2.
 * A copy of this license is included with this source.
 *
6
 * Copyright 2000-2004, Jack Moffitt <jack@xiph.org,
7 8 9 10
 *                      Michael Smith <msmith@xiph.org>,
 *                      oddsock <oddsock@xiph.org>,
 *                      Karl Heyes <karl@xiph.org>
 *                      and others (see AUTHORS for details).
11
 * Copyright 2012-2018, Philipp "ph3-der-loewe" Schafft <lion@lion.leolix.org>,
12 13
 */

14 15 16 17
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

18 19
#include <string.h>
#include <stdlib.h>
20 21
#include <stdarg.h>
#include <time.h>
22 23 24
#include <libxml/xmlmemory.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
25

26
#include "cfgfile.h"
27 28 29 30 31 32
#include "connection.h"
#include "refbuf.h"
#include "client.h"
#include "source.h"
#include "global.h"
#include "stats.h"
33
#include "compat.h"
34
#include "xslt.h"
35
#include "fserve.h"
36
#include "admin.h"
37
#include "errors.h"
38 39 40 41

#include "format.h"

#include "logging.h"
42
#include "auth.h"
Ed "oddsock" Zaleski's avatar
Ed "oddsock" Zaleski committed
43 44 45
#ifdef _WIN32
#define snprintf _snprintf
#endif
46 47 48

#define CATMODULE "admin"

49 50 51 52 53
/* Helper macros */
#define COMMAND_REQUIRE(client,name,var)                                \
    do {                                                                \
        (var) = httpp_get_query_param((client)->parser, (name));        \
        if((var) == NULL) {                                             \
54
            client_send_error_by_id(client, ICECAST_ERROR_ADMIN_MISSING_PARAMETER); \
55 56 57 58 59 60 61
            return;                                                     \
        }                                                               \
    } while(0);

#define COMMAND_OPTIONAL(client,name,var) \
(var) = httpp_get_query_param((client)->parser, (name))

62 63 64
/* special commands */
#define COMMAND_ERROR                      ADMIN_COMMAND_ERROR
#define COMMAND_ANY                        ADMIN_COMMAND_ANY
65

Marvin Scholz's avatar
Marvin Scholz committed
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
#define FALLBACK_RAW_REQUEST                "fallbacks"
#define FALLBACK_TRANSFORMED_REQUEST        "fallbacks.xsl"
#define SHOUTCAST_METADATA_REQUEST          "admin.cgi"
#define METADATA_RAW_REQUEST                "metadata"
#define METADATA_TRANSFORMED_REQUEST        "metadata.xsl"
#define LISTCLIENTS_RAW_REQUEST             "listclients"
#define LISTCLIENTS_TRANSFORMED_REQUEST     "listclients.xsl"
#define STATS_RAW_REQUEST                   "stats"
#define STATS_TRANSFORMED_REQUEST           "stats.xsl"
#define QUEUE_RELOAD_RAW_REQUEST            "reloadconfig"
#define QUEUE_RELOAD_TRANSFORMED_REQUEST    "reloadconfig.xsl"
#define LISTMOUNTS_RAW_REQUEST              "listmounts"
#define LISTMOUNTS_TRANSFORMED_REQUEST      "listmounts.xsl"
#define STREAMLIST_RAW_REQUEST              "streamlist"
#define STREAMLIST_TRANSFORMED_REQUEST      "streamlist.xsl"
#define STREAMLIST_PLAINTEXT_REQUEST        "streamlist.txt"
#define MOVECLIENTS_RAW_REQUEST             "moveclients"
#define MOVECLIENTS_TRANSFORMED_REQUEST     "moveclients.xsl"
#define KILLCLIENT_RAW_REQUEST              "killclient"
#define KILLCLIENT_TRANSFORMED_REQUEST      "killclient.xsl"
#define KILLSOURCE_RAW_REQUEST              "killsource"
#define KILLSOURCE_TRANSFORMED_REQUEST      "killsource.xsl"
#define ADMIN_XSL_RESPONSE                  "response.xsl"
#define MANAGEAUTH_RAW_REQUEST              "manageauth"
#define MANAGEAUTH_TRANSFORMED_REQUEST      "manageauth.xsl"
#define UPDATEMETADATA_RAW_REQUEST          "updatemetadata"
#define UPDATEMETADATA_TRANSFORMED_REQUEST  "updatemetadata.xsl"
#define DEFAULT_RAW_REQUEST                 ""
#define DEFAULT_TRANSFORMED_REQUEST         ""
#define BUILDM3U_RAW_REQUEST                "buildm3u"
96

97
typedef void (*request_function_ptr)(client_t *, source_t *, admin_format_t);
Marvin Scholz's avatar
Marvin Scholz committed
98 99 100 101 102 103 104 105

typedef struct admin_command_handler {
    const char                     *route;
    const int                       type;
    const int                       format;
    const request_function_ptr      function;
} admin_command_handler_t;

106 107 108 109 110 111
typedef struct {
    const char *prefix;
    size_t length;
    const admin_command_handler_t *handlers;
} admin_command_table_t;

112 113 114 115 116 117 118 119 120 121 122 123 124
static void command_fallback            (client_t *client, source_t *source, admin_format_t response);
static void command_metadata            (client_t *client, source_t *source, admin_format_t response);
static void command_shoutcast_metadata  (client_t *client, source_t *source, admin_format_t response);
static void command_show_listeners      (client_t *client, source_t *source, admin_format_t response);
static void command_stats               (client_t *client, source_t *source, admin_format_t response);
static void command_queue_reload        (client_t *client, source_t *source, admin_format_t response);
static void command_list_mounts         (client_t *client, source_t *source, admin_format_t response);
static void command_move_clients        (client_t *client, source_t *source, admin_format_t response);
static void command_kill_client         (client_t *client, source_t *source, admin_format_t response);
static void command_kill_source         (client_t *client, source_t *source, admin_format_t response);
static void command_manageauth          (client_t *client, source_t *source, admin_format_t response);
static void command_updatemetadata      (client_t *client, source_t *source, admin_format_t response);
static void command_buildm3u            (client_t *client, source_t *source, admin_format_t response);
Marvin Scholz's avatar
Marvin Scholz committed
125 126

static const admin_command_handler_t handlers[] = {
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
    { "*",                                  ADMINTYPE_GENERAL,      ADMIN_FORMAT_TRANSFORMED,    NULL }, /* for ACL framework */
    { FALLBACK_RAW_REQUEST,                 ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_fallback },
    { FALLBACK_TRANSFORMED_REQUEST,         ADMINTYPE_MOUNT,        ADMIN_FORMAT_TRANSFORMED,    command_fallback },
    { METADATA_RAW_REQUEST,                 ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_metadata },
    { METADATA_TRANSFORMED_REQUEST,         ADMINTYPE_MOUNT,        ADMIN_FORMAT_TRANSFORMED,    command_metadata },
    { SHOUTCAST_METADATA_REQUEST,           ADMINTYPE_MOUNT,        ADMIN_FORMAT_TRANSFORMED,    command_shoutcast_metadata },
    { LISTCLIENTS_RAW_REQUEST,              ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_show_listeners },
    { LISTCLIENTS_TRANSFORMED_REQUEST,      ADMINTYPE_MOUNT,        ADMIN_FORMAT_TRANSFORMED,    command_show_listeners },
    { STATS_RAW_REQUEST,                    ADMINTYPE_HYBRID,       ADMIN_FORMAT_RAW,            command_stats },
    { STATS_TRANSFORMED_REQUEST,            ADMINTYPE_HYBRID,       ADMIN_FORMAT_TRANSFORMED,    command_stats },
    { "stats.xml",                          ADMINTYPE_HYBRID,       ADMIN_FORMAT_RAW,            command_stats },
    { QUEUE_RELOAD_RAW_REQUEST,             ADMINTYPE_GENERAL,      ADMIN_FORMAT_RAW,            command_queue_reload },
    { QUEUE_RELOAD_TRANSFORMED_REQUEST,     ADMINTYPE_GENERAL,      ADMIN_FORMAT_TRANSFORMED,    command_queue_reload },
    { LISTMOUNTS_RAW_REQUEST,               ADMINTYPE_GENERAL,      ADMIN_FORMAT_RAW,            command_list_mounts },
    { LISTMOUNTS_TRANSFORMED_REQUEST,       ADMINTYPE_GENERAL,      ADMIN_FORMAT_TRANSFORMED,    command_list_mounts },
    { STREAMLIST_RAW_REQUEST,               ADMINTYPE_GENERAL,      ADMIN_FORMAT_RAW,            command_list_mounts },
    { STREAMLIST_PLAINTEXT_REQUEST,         ADMINTYPE_GENERAL,      ADMIN_FORMAT_PLAINTEXT,      command_list_mounts },
    { STREAMLIST_TRANSFORMED_REQUEST,       ADMINTYPE_GENERAL,      ADMIN_FORMAT_TRANSFORMED,    command_list_mounts },
    { MOVECLIENTS_RAW_REQUEST,              ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_move_clients },
    { MOVECLIENTS_TRANSFORMED_REQUEST,      ADMINTYPE_HYBRID,       ADMIN_FORMAT_TRANSFORMED,    command_move_clients },
    { KILLCLIENT_RAW_REQUEST,               ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_kill_client },
    { KILLCLIENT_TRANSFORMED_REQUEST,       ADMINTYPE_MOUNT,        ADMIN_FORMAT_TRANSFORMED,    command_kill_client },
    { KILLSOURCE_RAW_REQUEST,               ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_kill_source },
    { KILLSOURCE_TRANSFORMED_REQUEST,       ADMINTYPE_MOUNT,        ADMIN_FORMAT_TRANSFORMED,    command_kill_source },
    { MANAGEAUTH_RAW_REQUEST,               ADMINTYPE_GENERAL,      ADMIN_FORMAT_RAW,            command_manageauth },
    { MANAGEAUTH_TRANSFORMED_REQUEST,       ADMINTYPE_GENERAL,      ADMIN_FORMAT_TRANSFORMED,    command_manageauth },
    { UPDATEMETADATA_RAW_REQUEST,           ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_updatemetadata },
    { UPDATEMETADATA_TRANSFORMED_REQUEST,   ADMINTYPE_MOUNT,        ADMIN_FORMAT_TRANSFORMED,    command_updatemetadata },
    { BUILDM3U_RAW_REQUEST,                 ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_buildm3u },
    { DEFAULT_TRANSFORMED_REQUEST,          ADMINTYPE_HYBRID,       ADMIN_FORMAT_TRANSFORMED,    command_stats },
    { DEFAULT_RAW_REQUEST,                  ADMINTYPE_HYBRID,       ADMIN_FORMAT_TRANSFORMED,    command_stats }
Philipp Schafft's avatar
Philipp Schafft committed
158 159
};

160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
static admin_command_table_t command_tables[] = {
    {.prefix = NULL, .length = (sizeof(handlers)/sizeof(*handlers)), .handlers = handlers},
};

static inline const admin_command_table_t * admin_get_table(admin_command_id_t command)
{
    size_t t = (command & 0x00FF0000) >> 16;

    if (t >= (sizeof(command_tables)/sizeof(*command_tables)))
        return NULL;

    return &(command_tables[t]);
}

static inline const admin_command_table_t * admin_get_table_by_prefix(const char *command)
{
    const char *end;
    size_t i;
    size_t len;

    end = strchr(command, '/');

    if (end == NULL) {
        for (i = 0; i < (sizeof(command_tables)/sizeof(*command_tables)); i++)
            if (command_tables[i].prefix == NULL)
                return &(command_tables[i]);

        return NULL;
    }

    len = end - command;

    for (i = 0; i < (sizeof(command_tables)/sizeof(*command_tables)); i++) {
        if (command_tables[i].prefix != NULL && strlen(command_tables[i].prefix) == len && strncmp(command_tables[i].prefix, command, len) == 0) {
            return &(command_tables[i]);
        }
    }

    return NULL;
}

static inline admin_command_id_t admin_get_command_by_table_and_index(const admin_command_table_t *table, size_t index)
{
    size_t t = table - command_tables;

    if (t >= (sizeof(command_tables)/sizeof(*command_tables)))
        return ADMIN_COMMAND_ERROR;

    if (index > 0x0FFFF)
        return ADMIN_COMMAND_ERROR;

    return (t << 16) | index;
}

static inline size_t admin_get_index_by_command(admin_command_id_t command)
{
    return command & 0x0FFFF;
}
Marvin Scholz's avatar
Marvin Scholz committed
218

219
admin_command_id_t admin_get_command(const char *command)
Marvin Scholz's avatar
Marvin Scholz committed
220
{
Philipp Schafft's avatar
Philipp Schafft committed
221
    size_t i;
222 223
    const admin_command_table_t *table = admin_get_table_by_prefix(command);
    const char *suffix;
Philipp Schafft's avatar
Philipp Schafft committed
224

225 226 227 228 229 230 231 232 233 234 235 236 237
    if (table == NULL)
        return COMMAND_ERROR;

    suffix = strchr(command, '/');
    if (suffix != NULL) {
        suffix++;
    } else {
        suffix = command;
    }

    for (i = 0; i < table->length; i++)
        if (strcmp(table->handlers[i].route, suffix) == 0)
            return admin_get_command_by_table_and_index(table, i);
Philipp Schafft's avatar
Philipp Schafft committed
238 239 240 241

    return COMMAND_ERROR;
}

Marvin Scholz's avatar
Marvin Scholz committed
242 243
/* Get the command handler for command or NULL
 */
244
const admin_command_handler_t* admin_get_handler(admin_command_id_t command)
Marvin Scholz's avatar
Marvin Scholz committed
245
{
246 247
    const admin_command_table_t *table = admin_get_table(command);
    size_t index = admin_get_index_by_command(command);
Philipp Schafft's avatar
Philipp Schafft committed
248

249 250 251 252 253 254 255
    if (table == NULL)
        return NULL;

    if (index >= table->length)
        return NULL;

    return &(table->handlers[index]);
Marvin Scholz's avatar
Marvin Scholz committed
256 257 258 259 260
}

/* Get the command type for command
 * If the command is invalid, ADMINTYPE_ERROR is returned.
 */
261
int admin_get_command_type(admin_command_id_t command)
Marvin Scholz's avatar
Marvin Scholz committed
262 263
{
    const admin_command_handler_t* handler = admin_get_handler(command);
Philipp Schafft's avatar
Philipp Schafft committed
264

Marvin Scholz's avatar
Marvin Scholz committed
265 266
    if (handler != NULL)
        return handler->type;
Philipp Schafft's avatar
Philipp Schafft committed
267 268

    return ADMINTYPE_ERROR;
269 270
}

271 272 273
/* build an XML doc containing information about currently running sources.
 * If a mountpoint is passed then that source will not be added to the XML
 * doc even if the source is running */
Marvin Scholz's avatar
Marvin Scholz committed
274
xmlDocPtr admin_build_sourcelist(const char *mount)
275 276 277 278 279 280 281 282
{
    avl_node *node;
    source_t *source;
    xmlNodePtr xmlnode, srcnode;
    xmlDocPtr doc;
    char buf[22];
    time_t now = time(NULL);

283 284
    doc = xmlNewDoc (XMLSTR("1.0"));
    xmlnode = xmlNewDocNode (doc, NULL, XMLSTR("icestats"), NULL);
285
    xmlDocSetRootElement(doc, xmlnode);
286

287
    if (mount) {
288
        xmlNewTextChild (xmlnode, NULL, XMLSTR("current_source"), XMLSTR(mount));
289 290 291 292 293
    }

    node = avl_get_first(global.source_tree);
    while(node) {
        source = (source_t *)node->key;
294 295 296 297 298 299
        if (mount && strcmp (mount, source->mount) == 0)
        {
            node = avl_get_next (node);
            continue;
        }

300
        if (source->running || source->on_demand)
301
        {
302 303
            ice_config_t *config;
            mount_proxy *mountinfo;
Philipp Schafft's avatar
Philipp Schafft committed
304
            acl_t *acl = NULL;
305

306 307
            srcnode = xmlNewChild(xmlnode, NULL, XMLSTR("source"), NULL);
            xmlSetProp(srcnode, XMLSTR("mount"), XMLSTR(source->mount));
308

309
            xmlNewTextChild(srcnode, NULL, XMLSTR("fallback"),
310
                    (source->fallback_mount != NULL)?
311
                    XMLSTR(source->fallback_mount):XMLSTR(""));
312
            snprintf(buf, sizeof(buf), "%lu", source->listeners);
313
            xmlNewTextChild(srcnode, NULL, XMLSTR("listeners"), XMLSTR(buf));
314

Karl Heyes's avatar
Karl Heyes committed
315
            config = config_get_config();
Marvin Scholz's avatar
Marvin Scholz committed
316
            mountinfo = config_find_mount(config, source->mount, MOUNT_TYPE_NORMAL);
Philipp Schafft's avatar
Philipp Schafft committed
317
            if (mountinfo)
318
                acl = auth_stack_get_anonymous_acl(mountinfo->authstack, httpp_req_get);
Philipp Schafft's avatar
Philipp Schafft committed
319
            if (!acl)
320
                acl = auth_stack_get_anonymous_acl(config->authstack, httpp_req_get);
Philipp Schafft's avatar
Philipp Schafft committed
321
            if (acl && acl_test_web(acl) == ACL_POLICY_DENY) {
322
                xmlNewTextChild(srcnode, NULL, XMLSTR("authenticator"), XMLSTR("(dummy)"));
323
            }
Philipp Schafft's avatar
Philipp Schafft committed
324
            acl_release(acl);
325 326
            config_release_config();

Marvin Scholz's avatar
Marvin Scholz committed
327 328 329 330
            if (source->running) {
                if (source->client) {
                    snprintf(buf, sizeof(buf), "%lu",
                        (unsigned long)(now - source->con->con_time));
331
                    xmlNewTextChild(srcnode, NULL, XMLSTR("Connected"), XMLSTR(buf));
Karl Heyes's avatar
Karl Heyes committed
332
                }
333
                xmlNewTextChild(srcnode, NULL, XMLSTR("content-type"),
Marvin Scholz's avatar
Marvin Scholz committed
334
                    XMLSTR(source->format->contenttype));
335
            }
336
        }
337 338 339 340 341
        node = avl_get_next(node);
    }
    return(doc);
}

342 343 344 345
void admin_send_response(xmlDocPtr       doc,
                         client_t       *client,
                         admin_format_t  response,
                         const char     *xslt_template)
346
{
347
    if (response == ADMIN_FORMAT_RAW) {
348 349
        xmlChar *buff = NULL;
        int len = 0;
350 351 352
        size_t buf_len;
        ssize_t ret;

353
        xmlDocDumpMemory(doc, &buff, &len);
354 355 356 357

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

359 360
        client_set_queue(client, NULL);
        client->refbuf = refbuf_new(buf_len);
361

362 363 364
        ret = util_http_build_header(client->refbuf->data, buf_len, 0,
                                     0, 200, NULL,
                                     "text/xml", "utf-8",
365
                                     NULL, NULL, client);
Philipp Schafft's avatar
Philipp Schafft committed
366
        if (ret < 0) {
367
            ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
368
            client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
369 370
            xmlFree(buff);
            return;
Philipp Schafft's avatar
Philipp Schafft committed
371
        } else if (buf_len < (size_t)(len + ret + 64)) {
372 373 374 375 376 377 378 379 380 381
            void *new_data;
            buf_len = ret + len + 64;
            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, 200, NULL,
                                             "text/xml", "utf-8",
382
                                             NULL, NULL, client);
383 384
                if (ret == -1) {
                    ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
385
                    client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
386 387 388 389 390
                    xmlFree(buff);
                    return;
                }
            } else {
                ICECAST_LOG_ERROR("Client buffer reallocation failed. Dropping client.");
391
                client_send_error_by_id(client, ICECAST_ERROR_GEN_BUFFER_REALLOC);
392 393
                xmlFree(buff);
                return;
394
            }
395
        }
396

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

        client->refbuf->len = ret;
401 402 403
        xmlFree(buff);
        client->respcode = 200;
        fserve_add_client (client, NULL);
404
    }
405
    if (response == ADMIN_FORMAT_TRANSFORMED) {
406
        char *fullpath_xslt_template;
407
        size_t fullpath_xslt_template_len;
408 409
        ice_config_t *config = config_get_config();

410
        fullpath_xslt_template_len = strlen(config->adminroot_dir) + strlen(xslt_template) + strlen(PATH_SEPARATOR) + 1;
411 412
        fullpath_xslt_template = malloc(fullpath_xslt_template_len);
        snprintf(fullpath_xslt_template, fullpath_xslt_template_len, "%s%s%s",
413
            config->adminroot_dir, PATH_SEPARATOR, xslt_template);
414
        config_release_config();
415

416
        ICECAST_LOG_DEBUG("Sending XSLT (%s)", fullpath_xslt_template);
417 418 419 420
        xslt_transform(doc, fullpath_xslt_template, client);
        free(fullpath_xslt_template);
    }
}
421

422
void admin_handle_request(client_t *client, const char *uri)
423
{
Marvin Scholz's avatar
Marvin Scholz committed
424 425 426
    const char *mount;
    const admin_command_handler_t* handler;
    source_t *source = NULL;
427
    admin_format_t format;
428

Marvin Scholz's avatar
Marvin Scholz committed
429
    ICECAST_LOG_DEBUG("Got admin request '%s'", uri);
430

Marvin Scholz's avatar
Marvin Scholz committed
431
    handler = admin_get_handler(client->admin_command);
432

Marvin Scholz's avatar
Marvin Scholz committed
433
    /* Check if admin command is valid */
434
    if (handler == NULL || handler->function == NULL) {
Marvin Scholz's avatar
Marvin Scholz committed
435 436
        ICECAST_LOG_ERROR("Error parsing command string or unrecognised command: %H",
                uri);
437
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_UNRECOGNISED_COMMAND);
438 439 440
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
441
    /* Check ACL */
Philipp Schafft's avatar
Philipp Schafft committed
442
    if (acl_test_admin(client->acl, client->admin_command) != ACL_POLICY_ALLOW) {
Marvin Scholz's avatar
Marvin Scholz committed
443 444

        /* ACL disallows, check exceptions */
445
        if ((handler->function == command_metadata && handler->format == ADMIN_FORMAT_RAW) &&
Philipp Schafft's avatar
Philipp Schafft committed
446 447
            (acl_test_method(client->acl, httpp_req_source) == ACL_POLICY_ALLOW ||
             acl_test_method(client->acl, httpp_req_put)    == ACL_POLICY_ALLOW)) {
Marvin Scholz's avatar
Marvin Scholz committed
448 449
            ICECAST_LOG_DEBUG("Granted right to call COMMAND_RAW_METADATA_UPDATE to "
                "client because it is allowed to do SOURCE or PUT.");
Philipp Schafft's avatar
Philipp Schafft committed
450
        } else {
451
            client_send_error_by_id(client, ICECAST_ERROR_GEN_CLIENT_NEEDS_TO_AUTHENTICATE);
452 453
            return;
        }
454 455
    }

456 457
    mount = httpp_get_query_param(client->parser, "mount");

Marvin Scholz's avatar
Marvin Scholz committed
458
    /* Find mountpoint source */
459
    if(mount != NULL) {
460

Philipp Schafft's avatar
Philipp Schafft committed
461
        /* This is a mount request, handle it as such */
462
        avl_tree_rlock(global.source_tree);
Michael Smith's avatar
Michael Smith committed
463
        source = source_find_mount_raw(mount);
464

Marvin Scholz's avatar
Marvin Scholz committed
465
        /* No Source found */
Marvin Scholz's avatar
Marvin Scholz committed
466
        if (source == NULL) {
467
            avl_tree_unlock(global.source_tree);
Marvin Scholz's avatar
Marvin Scholz committed
468 469
            ICECAST_LOG_WARN("Admin command \"%H\" on non-existent source \"%H\"",
                    uri, mount);
470
            client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SOURCE_DOES_NOT_EXIST);
Marvin Scholz's avatar
Marvin Scholz committed
471 472 473
            return;
        } /* No Source running */
        else if (source->running == 0 && source->on_demand == 0) {
474
            avl_tree_unlock(global.source_tree);
Marvin Scholz's avatar
Marvin Scholz committed
475 476
            ICECAST_LOG_INFO("Received admin command \"%H\" on unavailable mount \"%H\"",
                    uri, mount);
477
            client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SOURCE_IS_NOT_AVAILABLE);
Marvin Scholz's avatar
Marvin Scholz committed
478
            return;
479
        }
Marvin Scholz's avatar
Marvin Scholz committed
480 481
        ICECAST_LOG_INFO("Received admin command %H on mount '%s'",
                    uri, mount);
482 483
    }

Marvin Scholz's avatar
Marvin Scholz committed
484
    if (handler->type == ADMINTYPE_MOUNT && !source) {
485
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_MISSING_PARAMETER);
Marvin Scholz's avatar
Marvin Scholz committed
486
        return;
487 488
    }

489 490 491 492 493 494 495
    if (handler->format == ADMIN_FORMAT_AUTO) {
        format = client_get_admin_format_by_content_negotiation(client);
    } else {
        format = handler->format;
    }

    handler->function(client, source, format);
Marvin Scholz's avatar
Marvin Scholz committed
496 497
    if (source) {
        avl_tree_unlock(global.source_tree);
498
    }
Marvin Scholz's avatar
Marvin Scholz committed
499
    return;
500 501
}

502
static void html_success(client_t *client, char *message)
503
{
504 505
    ssize_t ret;

Marvin Scholz's avatar
Marvin Scholz committed
506 507
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
508
                                 "text/html", "utf-8",
509
                                 "", NULL, client);
510 511 512

    if (ret == -1 || ret >= PER_CLIENT_REFBUF_SIZE) {
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
513
        client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
514 515 516
        return;
    }

517
    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
518 519
        "<html><head><title>Admin request successful</title></head>"
        "<body><p>%s</p></body></html>", message);
520

521
    client->respcode = 200;
Marvin Scholz's avatar
Marvin Scholz committed
522 523
    client->refbuf->len = strlen(client->refbuf->data);
    fserve_add_client(client, NULL);
524 525
}

526

Marvin Scholz's avatar
Marvin Scholz committed
527 528
static void command_move_clients(client_t   *client,
                                 source_t   *source,
529
                                 admin_format_t response)
530
{
531
    const char *dest_source;
532
    source_t *dest;
533 534 535 536 537
    xmlDocPtr doc;
    xmlNodePtr node;
    char buf[255];
    int parameters_passed = 0;

538
    ICECAST_LOG_DEBUG("Doing optional check");
539
    if((COMMAND_OPTIONAL(client, "destination", dest_source))) {
540 541
        parameters_passed = 1;
    }
542
    ICECAST_LOG_DEBUG("Done optional check (%d)", parameters_passed);
543 544
    if (!parameters_passed) {
        doc = admin_build_sourcelist(source->mount);
545
        admin_send_response(doc, client, response,
546 547 548 549
             MOVECLIENTS_TRANSFORMED_REQUEST);
        xmlFreeDoc(doc);
        return;
    }
550

Marvin Scholz's avatar
Marvin Scholz committed
551
    dest = source_find_mount(dest_source);
552

Marvin Scholz's avatar
Marvin Scholz committed
553
    if (dest == NULL) {
554
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_NO_SUCH_DESTINATION);
555 556 557
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
558
    if (strcmp(dest->mount, source->mount) == 0) {
559
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SUPPLIED_MOUNTPOINTS_ARE_IDENTICAL);
560 561 562
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
563
    if (dest->running == 0 && dest->on_demand == 0) {
564
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_DEST_NOT_RUNNING);
565 566 567
        return;
    }

568
    ICECAST_LOG_INFO("source is \"%s\", destination is \"%s\"", source->mount, dest->mount);
569

Marvin Scholz's avatar
Marvin Scholz committed
570
    doc = xmlNewDoc(XMLSTR("1.0"));
571
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
572 573
    xmlDocSetRootElement(doc, node);

Marvin Scholz's avatar
Marvin Scholz committed
574
    source_move_clients(source, dest);
575

Marvin Scholz's avatar
Marvin Scholz committed
576
    snprintf(buf, sizeof(buf), "Clients moved from %s to %s",
577
        source->mount, dest_source);
578 579
    xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
    xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
580

Marvin Scholz's avatar
Marvin Scholz committed
581
    admin_send_response(doc, client, response, ADMIN_XSL_RESPONSE);
582
    xmlFreeDoc(doc);
583 584
}

Marvin Scholz's avatar
Marvin Scholz committed
585 586 587 588 589
static inline xmlNodePtr __add_listener(client_t        *client,
                                        xmlNodePtr      parent,
                                        time_t          now,
                                        operation_mode  mode)
{
590 591 592 593
    const char *tmp;
    xmlNodePtr node;
    char buf[22];

Philipp Schafft's avatar
Philipp Schafft committed
594
    /* TODO: kh has support for a child node "lag". We should add that.
595 596
     * BEFORE RELEASE NEXT DOCUMENT #2097: Changed case of child nodes to lower case.
     * The case of <ID>, <IP>, <UserAgent> and <Connected> got changed to lower case.
Philipp Schafft's avatar
Philipp Schafft committed
597
     */
598 599 600 601 602 603 604

    node = xmlNewChild(parent, NULL, XMLSTR("listener"), NULL);
    if (!node)
        return NULL;

    memset(buf, '\000', sizeof(buf));
    snprintf(buf, sizeof(buf)-1, "%lu", client->con->id);
Philipp Schafft's avatar
Philipp Schafft committed
605
    xmlSetProp(node, XMLSTR("id"), XMLSTR(buf));
606
    xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "ID" : "id"), XMLSTR(buf));
607

608
    xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "IP" : "ip"), XMLSTR(client->con->ip));
609 610 611

    tmp = httpp_getvar(client->parser, "user-agent");
    if (tmp)
612
        xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "UserAgent" : "useragent"), XMLSTR(tmp));
613 614 615

    tmp = httpp_getvar(client->parser, "referer");
    if (tmp)
616
        xmlNewTextChild(node, NULL, XMLSTR("referer"), XMLSTR(tmp));
617 618

    snprintf(buf, sizeof(buf), "%lu", (unsigned long)(now - client->con->con_time));
619
    xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "Connected" : "connected"), XMLSTR(buf));
620 621

    if (client->username)
622
        xmlNewTextChild(node, NULL, XMLSTR("username"), XMLSTR(client->username));
623

Philipp Schafft's avatar
Philipp Schafft committed
624
    if (client->role)
625
        xmlNewTextChild(node, NULL, XMLSTR("role"), XMLSTR(client->role));
Philipp Schafft's avatar
Philipp Schafft committed
626

627
    xmlNewTextChild(node, NULL, XMLSTR("tls"), XMLSTR(client->con->tls ? "true" : "false"));
628

629 630 631 632 633 634 635 636 637
    switch (client->protocol) {
        case ICECAST_PROTOCOL_HTTP:
            xmlNewTextChild(node, NULL, XMLSTR("protocol"), XMLSTR("http"));
        break;
        case ICECAST_PROTOCOL_SHOUTCAST:
            xmlNewTextChild(node, NULL, XMLSTR("protocol"), XMLSTR("icy"));
        break;
    }

638 639 640
    return node;
}

Marvin Scholz's avatar
Marvin Scholz committed
641 642 643 644
void admin_add_listeners_to_mount(source_t          *source,
                                  xmlNodePtr        parent,
                                  operation_mode    mode)
{
645 646 647 648 649 650
    time_t now = time(NULL);
    avl_node *client_node;

    avl_tree_rlock(source->client_tree);
    client_node = avl_get_first(source->client_tree);
    while(client_node) {
651
        __add_listener((client_t *)client_node->key, parent, now, mode);
652 653 654 655 656
        client_node = avl_get_next(client_node);
    }
    avl_tree_unlock(source->client_tree);
}

Marvin Scholz's avatar
Marvin Scholz committed
657 658
static void command_show_listeners(client_t *client,
                                   source_t *source,
659
                                   admin_format_t response)
660
{
661
    xmlDocPtr doc;
662
    xmlNodePtr node, srcnode;
663
    char buf[22];
664

665
    doc = xmlNewDoc(XMLSTR("1.0"));
666 667 668
    node = xmlNewDocNode(doc, NULL, XMLSTR("icestats"), NULL);
    srcnode = xmlNewChild(node, NULL, XMLSTR("source"), NULL);
    xmlSetProp(srcnode, XMLSTR("mount"), XMLSTR(source->mount));
669
    xmlDocSetRootElement(doc, node);
670

671
    memset(buf, '\000', sizeof(buf));
672
    snprintf (buf, sizeof(buf), "%lu", source->listeners);
673
    /* BEFORE RELEASE NEXT DOCUMENT #2097: Changed "Listeners" to lower case. */
674
    xmlNewTextChild(srcnode, NULL, XMLSTR(client->mode == OMODE_LEGACY ? "Listeners" : "listeners"), XMLSTR(buf));
675

676
    admin_add_listeners_to_mount(source, srcnode, client->mode);
677

678
    admin_send_response(doc, client, response,
679 680
        LISTCLIENTS_TRANSFORMED_REQUEST);
    xmlFreeDoc(doc);
681 682
}

683
static void command_buildm3u(client_t *client, source_t *source, admin_format_t format)
684
{
Marvin Scholz's avatar
Marvin Scholz committed
685
    const char *mount = source->mount;
686 687
    const char *username = NULL;
    const char *password = NULL;
688
    ice_config_t *config;
689
    ssize_t ret;
690 691 692 693

    COMMAND_REQUIRE(client, "username", username);
    COMMAND_REQUIRE(client, "password", password);

Marvin Scholz's avatar
Marvin Scholz committed
694 695
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
696
                                 "audio/x-mpegurl", NULL,
697
                                 NULL, NULL, client);
698

Marvin Scholz's avatar
Marvin Scholz committed
699 700
    if (ret == -1 || ret >= (PER_CLIENT_REFBUF_SIZE - 512)) {
        /* we want at least 512 Byte left for data */
701
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
702
        client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
703 704 705 706
        return;
    }


Karl Heyes's avatar
Karl Heyes committed
707
    config = config_get_config();
708
    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
709
        "Content-Disposition: attachment; filename=listen.m3u\r\n\r\n"
710 711 712
        "http://%s:%s@%s:%d%s\r\n",
        username,
        password,
Karl Heyes's avatar
Karl Heyes committed
713 714
        config->hostname,
        config->port,
715
        mount
716
    );
Karl Heyes's avatar
Karl Heyes committed
717
    config_release_config();
718

719
    client->respcode = 200;
Karl Heyes's avatar
Karl Heyes committed
720 721
    client->refbuf->len = strlen (client->refbuf->data);
    fserve_add_client (client, NULL);
722
}
723

Marvin Scholz's avatar
Marvin Scholz committed
724 725
xmlNodePtr admin_add_role_to_authentication(auth_t *auth, xmlNodePtr parent)
{
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
    xmlNodePtr rolenode = xmlNewChild(parent, NULL, XMLSTR("role"), NULL);
    char idbuf[32];

    snprintf(idbuf, sizeof(idbuf), "%lu", auth->id);
    xmlSetProp(rolenode, XMLSTR("id"), XMLSTR(idbuf));

    if (auth->type)
        xmlSetProp(rolenode, XMLSTR("type"), XMLSTR(auth->type));
    if (auth->role)
        xmlSetProp(rolenode, XMLSTR("name"), XMLSTR(auth->role));
    if (auth->management_url)
        xmlSetProp(rolenode, XMLSTR("management-url"), XMLSTR(auth->management_url));

    xmlSetProp(rolenode, XMLSTR("can-adduser"), XMLSTR(auth->adduser ? "true" : "false"));
    xmlSetProp(rolenode, XMLSTR("can-deleteuser"), XMLSTR(auth->deleteuser ? "true" : "false"));
    xmlSetProp(rolenode, XMLSTR("can-listuser"), XMLSTR(auth->listuser ? "true" : "false"));

    return rolenode;
}
745

746
static void command_manageauth(client_t *client, source_t *source, admin_format_t response)
Marvin Scholz's avatar
Marvin Scholz committed
747
{
748
    xmlDocPtr doc;
749
    xmlNodePtr node, rolenode, usersnode, msgnode;
750 751
    const char *action = NULL;
    const char *username = NULL;
752
    const char *idstring = NULL;
753 754
    char *message = NULL;
    int ret = AUTH_OK;
755
    int error_id = ICECAST_ERROR_ADMIN_missing_parameter;
756 757
    long unsigned int id;
    ice_config_t *config = config_get_config();
Philipp Schafft's avatar
Philipp Schafft committed
758
    auth_t *auth;
759

Marvin Scholz's avatar
Marvin Scholz committed
760
    do {
761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781
        /* get id */
        COMMAND_REQUIRE(client, "id", idstring);
        id = atol(idstring);

        /* no find a auth_t for that id by looking up the config */
        /* globals first */
        auth = auth_stack_getbyid(config->authstack, id);
        /* now mounts */
        if (!auth) {
            mount_proxy *mount = config->mounts;
            while (mount) {
                auth = auth_stack_getbyid(mount->authstack, id);
                if (auth)
                    break;
                mount = mount->next;
            }
        }

        /* check if we found one */
        if (auth == NULL) {
            ICECAST_LOG_WARN("Client requested mangement for unknown role %lu", id);
782
            error_id = ICECAST_ERROR_ADMIN_ROLEMGN_ROLE_NOT_FOUND;
783
            break;
784
        }
Philipp Schafft's avatar
Philipp Schafft committed
785

786
        COMMAND_OPTIONAL(client, "action", action);
787
        COMMAND_OPTIONAL(client, "username", username);
788 789

        if (action == NULL)
790
            action = "list";
791

Marvin Scholz's avatar
Marvin Scholz committed
792
        if (!strcmp(action, "add")) {
793
            const char *password = NULL;
794
            COMMAND_OPTIONAL(client, "password", password);
795

796 797
            if (username == NULL || password == NULL) {
                ICECAST_LOG_WARN("manage auth request add for %lu but no user/pass", id);
798 799
                break;
            }
800 801

            if (!auth->adduser) {
802
                error_id = ICECAST_ERROR_ADMIN_ROLEMGN_ADD_NOSYS;
803 804 805
                break;
            }

Philipp Schafft's avatar
Philipp Schafft committed
806
            ret = auth->adduser(auth, username, password);
807 808
            if (ret == AUTH_FAILED) {
                message = strdup("User add failed - check the icecast error log");
809
            } else if (ret == AUTH_USERADDED) {
810
                message = strdup("User added");
811
            } else if (ret == AUTH_USEREXISTS) {
812 813 814
                message = strdup("User already exists - not added");
            }
        }
Marvin Scholz's avatar
Marvin Scholz committed
815
        if (!strcmp(action, "delete")) {
816 817 818 819 820 821
            if (username == NULL) {
                ICECAST_LOG_WARN("manage auth request delete for %lu but no username", id);
                break;
            }

            if (!auth->deleteuser) {
822
                error_id = ICECAST_ERROR_ADMIN_ROLEMGN_DELETE_NOSYS;
823 824
                break;
            }
825

Philipp Schafft's avatar
Philipp Schafft committed
826
            ret = auth->deleteuser(auth, username);
827 828
            if (ret == AUTH_FAILED) {
                message = strdup("User delete failed - check the icecast error log");
829
            } else if (ret == AUTH_USERDELETED) {
830 831 832 833
                message = strdup("User deleted");
            }
        }

834
        doc = xmlNewDoc(XMLSTR("1.0"));
835
        node = xmlNewDocNode(doc, NULL, XMLSTR("icestats"), NULL);
836

837
        rolenode = admin_add_role_to_authentication(auth, node);
838

839
        if (message) {
840
            msgnode = xmlNewChild(node, NULL, XMLSTR("iceresponse"), NULL);
841
            xmlNewTextChild(msgnode, NULL, XMLSTR("message"), XMLSTR(message));
842
        }
843

844
        xmlDocSetRootElement(doc, node);
845

846 847 848 849
        if (auth && auth->listuser) {
            usersnode = xmlNewChild(rolenode, NULL, XMLSTR("users"), NULL);
            auth->listuser(auth, usersnode);
        }
850

851 852
        config_release_config();
        auth_release(auth);
853

854
        admin_send_response(doc, client, response,
Marvin Scholz's avatar
Marvin Scholz committed
855 856
            MANAGEAUTH_TRANSFORMED_REQUEST);
        free(message);
857 858 859 860
        xmlFreeDoc(doc);
        return;
    } while (0);

861 862
    config_release_config();
    auth_release(auth);
863
    client_send_error_by_id(client, error_id);
864 865
}

Marvin Scholz's avatar
Marvin Scholz committed
866 867
static void command_kill_source(client_t *client,
                                source_t *source,
868
                                admin_format_t response)
869
{
870 871 872
    xmlDocPtr doc;
    xmlNodePtr node;

873 874
    doc = xmlNewDoc (XMLSTR("1.0"));
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
875 876
    xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR("Source Removed"));
    xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
877 878
    xmlDocSetRootElement(doc, node);

879 880
    source->running = 0;

881
    admin_send_response(doc, client, response,
882 883
        ADMIN_XSL_RESPONSE);
    xmlFreeDoc(doc);
884 885
}

Marvin Scholz's avatar
Marvin Scholz committed
886 887
static void command_kill_client(client_t *client,
                                source_t *source,
888
                                admin_format_t response)
889
{
890
    const char *idtext;
891 892
    int id;
    client_t *listener;
893 894 895
    xmlDocPtr doc;
    xmlNodePtr node;
    char buf[50] = "";
896 897 898 899 900 901 902

    COMMAND_REQUIRE(client, "id", idtext);

    id = atoi(idtext);

    listener = source_find_client(source, id);

Marvin Scholz's avatar
Marvin Scholz committed
903
    doc = xmlNewDoc(XMLSTR("1.0"));
904
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
905
    xmlDocSetRootElement(doc, node);
906
    ICECAST_LOG_DEBUG("Response is %d", response);
907

908
    if(listener != NULL) {
909
        ICECAST_LOG_INFO("Admin request: client %d removed", id);
910 911 912 913 914

        /* This tags it for removal on the next iteration of the main source
         * loop
         */
        listener->con->error = 1;
915 916
        memset(buf, '\000', sizeof(buf));
        snprintf(buf, sizeof(buf)-1, "Client %d removed", id);
917 918
        xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
        xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
919 920
    }
    else {
921 922
        memset(buf, '\000', sizeof(buf));
        snprintf(buf, sizeof(buf)-1, "Client %d not found", id);
923 924
        xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
        xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("0"));
925
    }
926
    admin_send_response(doc, client, response,
927 928
        ADMIN_XSL_RESPONSE);
    xmlFreeDoc(doc);
929 930
}

Marvin Scholz's avatar
Marvin Scholz committed
931 932
static void command_fallback(client_t *client,
                             source_t *source,
933
                             admin_format_t response)
934
{
935
    const char *fallback;
936 937
    char *old;

938
    ICECAST_LOG_DEBUG("Got fallback request");
939 940 941 942 943 944 945

    COMMAND_REQUIRE(client, "fallback", fallback);

    old = source->fallback_mount;
    source->fallback_mount = strdup(fallback);
    free(old);

946
    html_success(client, "Fallback configured");
947 948
}

Marvin Scholz's avatar
Marvin Scholz committed
949 950
static void command_metadata(client_t *client,
                             source_t *source,
951
                             admin_format_t response)
952
{
953
    const char *action;
954
    const char *song, *title, *artist, *charset;
955
    format_plugin_t *plugin;
956 957
    xmlDocPtr doc;
    xmlNodePtr node;
958
    int same_ip = 1;
959

960
    doc = xmlNewDoc(XMLSTR("1.0"));
Marvin Scholz's avatar
Marvin Scholz committed
961
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
962
    xmlDocSetRootElement(doc, node);
963

964
    ICECAST_LOG_DEBUG("Got metadata update request");
965

966
    if (source->parser && source->parser->req_type == httpp_req_put) {
Marvin Scholz's avatar
Marvin Scholz committed
967 968
        ICECAST_LOG_ERROR("Got legacy SOURCE-style metadata update command on "
            "source connected with PUT at mountpoint %s", source->mount);
Philipp Schafft's avatar
Philipp Schafft committed
969 970
    }

971
    COMMAND_REQUIRE(client, "mode", action);
972 973 974
    COMMAND_OPTIONAL(client, "song", song);
    COMMAND_OPTIONAL(client, "title", title);
    COMMAND_OPTIONAL(client, "artist", artist);