admin.c 43.8 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"
43
#include "acl.h"
Ed "oddsock" Zaleski's avatar
Ed "oddsock" Zaleski committed
44 45 46
#ifdef _WIN32
#define snprintf _snprintf
#endif
47 48 49

#define CATMODULE "admin"

50 51
#define ADMIN_MAX_COMMAND_TABLES        8

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

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

65 66 67
/* special commands */
#define COMMAND_ERROR                      ADMIN_COMMAND_ERROR
#define COMMAND_ANY                        ADMIN_COMMAND_ANY
68

Marvin Scholz's avatar
Marvin Scholz committed
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 96 97 98
#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"
99

100 101 102 103 104 105
typedef struct {
    const char *prefix;
    size_t length;
    const admin_command_handler_t *handlers;
} admin_command_table_t;

106 107 108 109 110 111 112 113 114 115 116 117 118
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
119 120

static const admin_command_handler_t handlers[] = {
121 122 123 124 125 126 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
    { "*",                                  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
152 153
};

154
static admin_command_table_t command_tables[ADMIN_MAX_COMMAND_TABLES] = {
155 156 157
    {.prefix = NULL, .length = (sizeof(handlers)/sizeof(*handlers)), .handlers = handlers},
};

158 159 160 161 162 163 164 165 166 167 168
static inline int __is_command_table_valid(const admin_command_table_t * table)
{
    if (table == NULL)
        return 0;

    if (table->length == 0 || table->handlers == NULL)
        return 0;

    return 1;
}

169 170 171 172 173 174 175
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;

176 177 178
    if (!__is_command_table_valid(&(command_tables[t])))
        return NULL;

179 180 181 182 183 184 185 186 187 188 189 190 191
    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++)
192
            if (command_tables[i].prefix == NULL && __is_command_table_valid(&(command_tables[i])))
193 194 195 196 197 198 199 200
                return &(command_tables[i]);

        return NULL;
    }

    len = end - command;

    for (i = 0; i < (sizeof(command_tables)/sizeof(*command_tables)); i++) {
201 202 203
        if (!__is_command_table_valid(&(command_tables[i])))
            continue;

204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
        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;

222 223 224
    if (!__is_command_table_valid(table))
        return ADMIN_COMMAND_ERROR;

225 226 227 228 229 230 231
    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
232

233
admin_command_id_t admin_get_command(const char *command)
Marvin Scholz's avatar
Marvin Scholz committed
234
{
Philipp Schafft's avatar
Philipp Schafft committed
235
    size_t i;
236 237
    const admin_command_table_t *table = admin_get_table_by_prefix(command);
    const char *suffix;
Philipp Schafft's avatar
Philipp Schafft committed
238

239 240 241 242 243 244 245 246 247 248 249 250 251
    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
252 253 254 255

    return COMMAND_ERROR;
}

Marvin Scholz's avatar
Marvin Scholz committed
256 257
/* Get the command handler for command or NULL
 */
258
const admin_command_handler_t* admin_get_handler(admin_command_id_t command)
Marvin Scholz's avatar
Marvin Scholz committed
259
{
260 261
    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
262

263 264 265 266 267 268 269
    if (table == NULL)
        return NULL;

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

    return &(table->handlers[index]);
Marvin Scholz's avatar
Marvin Scholz committed
270 271 272 273 274
}

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

Marvin Scholz's avatar
Marvin Scholz committed
279 280
    if (handler != NULL)
        return handler->type;
Philipp Schafft's avatar
Philipp Schafft committed
281 282

    return ADMINTYPE_ERROR;
283 284
}

285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
int admin_command_table_register(const char *prefix, size_t handlers_length, const admin_command_handler_t *handlers)
{
    size_t i;

    if (prefix == NULL || handlers_length == 0 || handlers == NULL)
        return -1;

    for (i = 0; i < (sizeof(command_tables)/sizeof(*command_tables)); i++) {
        if (__is_command_table_valid(&(command_tables[i])))
            continue;

        command_tables[i].prefix    = prefix;
        command_tables[i].length    = handlers_length;
        command_tables[i].handlers  = handlers;

        return 0;
    }

    return -1;
}

int admin_command_table_unregister(const char *prefix)
{
    size_t i;

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

    return -1;
}

320 321 322
/* 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
323
xmlDocPtr admin_build_sourcelist(const char *mount)
324 325 326 327 328 329 330 331
{
    avl_node *node;
    source_t *source;
    xmlNodePtr xmlnode, srcnode;
    xmlDocPtr doc;
    char buf[22];
    time_t now = time(NULL);

332 333
    doc = xmlNewDoc (XMLSTR("1.0"));
    xmlnode = xmlNewDocNode (doc, NULL, XMLSTR("icestats"), NULL);
334
    xmlDocSetRootElement(doc, xmlnode);
335

336
    if (mount) {
337
        xmlNewTextChild (xmlnode, NULL, XMLSTR("current_source"), XMLSTR(mount));
338 339 340 341 342
    }

    node = avl_get_first(global.source_tree);
    while(node) {
        source = (source_t *)node->key;
343 344 345 346 347 348
        if (mount && strcmp (mount, source->mount) == 0)
        {
            node = avl_get_next (node);
            continue;
        }

349
        if (source->running || source->on_demand)
350
        {
351 352
            ice_config_t *config;
            mount_proxy *mountinfo;
Philipp Schafft's avatar
Philipp Schafft committed
353
            acl_t *acl = NULL;
354

355 356
            srcnode = xmlNewChild(xmlnode, NULL, XMLSTR("source"), NULL);
            xmlSetProp(srcnode, XMLSTR("mount"), XMLSTR(source->mount));
357

358
            xmlNewTextChild(srcnode, NULL, XMLSTR("fallback"),
359
                    (source->fallback_mount != NULL)?
360
                    XMLSTR(source->fallback_mount):XMLSTR(""));
361
            snprintf(buf, sizeof(buf), "%lu", source->listeners);
362
            xmlNewTextChild(srcnode, NULL, XMLSTR("listeners"), XMLSTR(buf));
363

Karl Heyes's avatar
Karl Heyes committed
364
            config = config_get_config();
Marvin Scholz's avatar
Marvin Scholz committed
365
            mountinfo = config_find_mount(config, source->mount, MOUNT_TYPE_NORMAL);
Philipp Schafft's avatar
Philipp Schafft committed
366
            if (mountinfo)
367
                acl = auth_stack_get_anonymous_acl(mountinfo->authstack, httpp_req_get);
Philipp Schafft's avatar
Philipp Schafft committed
368
            if (!acl)
369
                acl = auth_stack_get_anonymous_acl(config->authstack, httpp_req_get);
Philipp Schafft's avatar
Philipp Schafft committed
370
            if (acl && acl_test_web(acl) == ACL_POLICY_DENY) {
371
                xmlNewTextChild(srcnode, NULL, XMLSTR("authenticator"), XMLSTR("(dummy)"));
372
            }
Philipp Schafft's avatar
Philipp Schafft committed
373
            acl_release(acl);
374 375
            config_release_config();

Marvin Scholz's avatar
Marvin Scholz committed
376 377 378 379
            if (source->running) {
                if (source->client) {
                    snprintf(buf, sizeof(buf), "%lu",
                        (unsigned long)(now - source->con->con_time));
380
                    xmlNewTextChild(srcnode, NULL, XMLSTR("Connected"), XMLSTR(buf));
Karl Heyes's avatar
Karl Heyes committed
381
                }
382
                xmlNewTextChild(srcnode, NULL, XMLSTR("content-type"),
Marvin Scholz's avatar
Marvin Scholz committed
383
                    XMLSTR(source->format->contenttype));
384
            }
385
        }
386 387 388 389 390
        node = avl_get_next(node);
    }
    return(doc);
}

391 392 393 394
void admin_send_response(xmlDocPtr       doc,
                         client_t       *client,
                         admin_format_t  response,
                         const char     *xslt_template)
395
{
396
    if (response == ADMIN_FORMAT_RAW) {
397 398
        xmlChar *buff = NULL;
        int len = 0;
399 400 401
        size_t buf_len;
        ssize_t ret;

402
        xmlDocDumpMemory(doc, &buff, &len);
403 404 405 406

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

408 409
        client_set_queue(client, NULL);
        client->refbuf = refbuf_new(buf_len);
410

411 412 413
        ret = util_http_build_header(client->refbuf->data, buf_len, 0,
                                     0, 200, NULL,
                                     "text/xml", "utf-8",
414
                                     NULL, NULL, client);
Philipp Schafft's avatar
Philipp Schafft committed
415
        if (ret < 0) {
416
            ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
417
            client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
418 419
            xmlFree(buff);
            return;
Philipp Schafft's avatar
Philipp Schafft committed
420
        } else if (buf_len < (size_t)(len + ret + 64)) {
421 422 423 424 425 426 427 428 429 430
            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",
431
                                             NULL, NULL, client);
432 433
                if (ret == -1) {
                    ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
434
                    client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
435 436 437 438 439
                    xmlFree(buff);
                    return;
                }
            } else {
                ICECAST_LOG_ERROR("Client buffer reallocation failed. Dropping client.");
440
                client_send_error_by_id(client, ICECAST_ERROR_GEN_BUFFER_REALLOC);
441 442
                xmlFree(buff);
                return;
443
            }
444
        }
445

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

        client->refbuf->len = ret;
450 451 452
        xmlFree(buff);
        client->respcode = 200;
        fserve_add_client (client, NULL);
453
    }
454
    if (response == ADMIN_FORMAT_TRANSFORMED) {
455
        char *fullpath_xslt_template;
456
        size_t fullpath_xslt_template_len;
457 458
        ice_config_t *config = config_get_config();

459
        fullpath_xslt_template_len = strlen(config->adminroot_dir) + strlen(xslt_template) + strlen(PATH_SEPARATOR) + 1;
460 461
        fullpath_xslt_template = malloc(fullpath_xslt_template_len);
        snprintf(fullpath_xslt_template, fullpath_xslt_template_len, "%s%s%s",
462
            config->adminroot_dir, PATH_SEPARATOR, xslt_template);
463
        config_release_config();
464

465
        ICECAST_LOG_DEBUG("Sending XSLT (%s)", fullpath_xslt_template);
466 467 468 469
        xslt_transform(doc, fullpath_xslt_template, client);
        free(fullpath_xslt_template);
    }
}
470

471
void admin_handle_request(client_t *client, const char *uri)
472
{
Marvin Scholz's avatar
Marvin Scholz committed
473 474 475
    const char *mount;
    const admin_command_handler_t* handler;
    source_t *source = NULL;
476
    admin_format_t format;
477

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

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

Marvin Scholz's avatar
Marvin Scholz committed
482
    /* Check if admin command is valid */
483
    if (handler == NULL || handler->function == NULL) {
Marvin Scholz's avatar
Marvin Scholz committed
484 485
        ICECAST_LOG_ERROR("Error parsing command string or unrecognised command: %H",
                uri);
486
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_UNRECOGNISED_COMMAND);
487 488 489
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
490
    /* Check ACL */
Philipp Schafft's avatar
Philipp Schafft committed
491
    if (acl_test_admin(client->acl, client->admin_command) != ACL_POLICY_ALLOW) {
Marvin Scholz's avatar
Marvin Scholz committed
492 493

        /* ACL disallows, check exceptions */
494
        if ((handler->function == command_metadata && handler->format == ADMIN_FORMAT_RAW) &&
Philipp Schafft's avatar
Philipp Schafft committed
495 496
            (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
497 498
            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
499
        } else {
500
            client_send_error_by_id(client, ICECAST_ERROR_GEN_CLIENT_NEEDS_TO_AUTHENTICATE);
501 502
            return;
        }
503 504
    }

505 506
    mount = httpp_get_query_param(client->parser, "mount");

Marvin Scholz's avatar
Marvin Scholz committed
507
    /* Find mountpoint source */
508
    if(mount != NULL) {
509

Philipp Schafft's avatar
Philipp Schafft committed
510
        /* This is a mount request, handle it as such */
511
        avl_tree_rlock(global.source_tree);
Michael Smith's avatar
Michael Smith committed
512
        source = source_find_mount_raw(mount);
513

Marvin Scholz's avatar
Marvin Scholz committed
514
        /* No Source found */
Marvin Scholz's avatar
Marvin Scholz committed
515
        if (source == NULL) {
516
            avl_tree_unlock(global.source_tree);
Marvin Scholz's avatar
Marvin Scholz committed
517 518
            ICECAST_LOG_WARN("Admin command \"%H\" on non-existent source \"%H\"",
                    uri, mount);
519
            client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SOURCE_DOES_NOT_EXIST);
Marvin Scholz's avatar
Marvin Scholz committed
520 521 522
            return;
        } /* No Source running */
        else if (source->running == 0 && source->on_demand == 0) {
523
            avl_tree_unlock(global.source_tree);
Marvin Scholz's avatar
Marvin Scholz committed
524 525
            ICECAST_LOG_INFO("Received admin command \"%H\" on unavailable mount \"%H\"",
                    uri, mount);
526
            client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SOURCE_IS_NOT_AVAILABLE);
Marvin Scholz's avatar
Marvin Scholz committed
527
            return;
528
        }
Marvin Scholz's avatar
Marvin Scholz committed
529 530
        ICECAST_LOG_INFO("Received admin command %H on mount '%s'",
                    uri, mount);
531 532
    }

Marvin Scholz's avatar
Marvin Scholz committed
533
    if (handler->type == ADMINTYPE_MOUNT && !source) {
534
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_MISSING_PARAMETER);
Marvin Scholz's avatar
Marvin Scholz committed
535
        return;
536 537
    }

538 539 540 541 542 543
    if (handler->format == ADMIN_FORMAT_AUTO) {
        format = client_get_admin_format_by_content_negotiation(client);
    } else {
        format = handler->format;
    }

544 545 546 547 548 549 550 551 552 553 554 555 556
    switch (client->parser->req_type) {
        case httpp_req_get:
            handler->function(client, source, format);
        break;
        case httpp_req_options:
            client_send_204(client);
        break;
        default:
            ICECAST_LOG_ERROR("Wrong request type from client");
            client_send_error_by_id(client, ICECAST_ERROR_CON_UNKNOWN_REQUEST);
        break;
    }

Marvin Scholz's avatar
Marvin Scholz committed
557 558
    if (source) {
        avl_tree_unlock(global.source_tree);
559
    }
Marvin Scholz's avatar
Marvin Scholz committed
560
    return;
561 562
}

563
static void html_success(client_t *client, char *message)
564
{
565 566
    ssize_t ret;

Marvin Scholz's avatar
Marvin Scholz committed
567 568
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
569
                                 "text/html", "utf-8",
570
                                 "", NULL, client);
571 572 573

    if (ret == -1 || ret >= PER_CLIENT_REFBUF_SIZE) {
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
574
        client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
575 576 577
        return;
    }

578
    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
579 580
        "<html><head><title>Admin request successful</title></head>"
        "<body><p>%s</p></body></html>", message);
581

582
    client->respcode = 200;
Marvin Scholz's avatar
Marvin Scholz committed
583 584
    client->refbuf->len = strlen(client->refbuf->data);
    fserve_add_client(client, NULL);
585 586
}

587

Marvin Scholz's avatar
Marvin Scholz committed
588 589
static void command_move_clients(client_t   *client,
                                 source_t   *source,
590
                                 admin_format_t response)
591
{
592
    const char *dest_source;
593
    source_t *dest;
594 595 596 597 598
    xmlDocPtr doc;
    xmlNodePtr node;
    char buf[255];
    int parameters_passed = 0;

599
    ICECAST_LOG_DEBUG("Doing optional check");
600
    if((COMMAND_OPTIONAL(client, "destination", dest_source))) {
601 602
        parameters_passed = 1;
    }
603
    ICECAST_LOG_DEBUG("Done optional check (%d)", parameters_passed);
604 605
    if (!parameters_passed) {
        doc = admin_build_sourcelist(source->mount);
606
        admin_send_response(doc, client, response,
607 608 609 610
             MOVECLIENTS_TRANSFORMED_REQUEST);
        xmlFreeDoc(doc);
        return;
    }
611

Marvin Scholz's avatar
Marvin Scholz committed
612
    dest = source_find_mount(dest_source);
613

Marvin Scholz's avatar
Marvin Scholz committed
614
    if (dest == NULL) {
615
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_NO_SUCH_DESTINATION);
616 617 618
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
619
    if (strcmp(dest->mount, source->mount) == 0) {
620
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SUPPLIED_MOUNTPOINTS_ARE_IDENTICAL);
621 622 623
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
624
    if (dest->running == 0 && dest->on_demand == 0) {
625
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_DEST_NOT_RUNNING);
626 627 628
        return;
    }

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

Marvin Scholz's avatar
Marvin Scholz committed
631
    doc = xmlNewDoc(XMLSTR("1.0"));
632
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
633 634
    xmlDocSetRootElement(doc, node);

Marvin Scholz's avatar
Marvin Scholz committed
635
    source_move_clients(source, dest);
636

Marvin Scholz's avatar
Marvin Scholz committed
637
    snprintf(buf, sizeof(buf), "Clients moved from %s to %s",
638
        source->mount, dest_source);
639 640
    xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
    xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
641

Marvin Scholz's avatar
Marvin Scholz committed
642
    admin_send_response(doc, client, response, ADMIN_XSL_RESPONSE);
643
    xmlFreeDoc(doc);
644 645
}

Marvin Scholz's avatar
Marvin Scholz committed
646 647 648 649 650
static inline xmlNodePtr __add_listener(client_t        *client,
                                        xmlNodePtr      parent,
                                        time_t          now,
                                        operation_mode  mode)
{
651 652 653 654
    const char *tmp;
    xmlNodePtr node;
    char buf[22];

Philipp Schafft's avatar
Philipp Schafft committed
655
    /* TODO: kh has support for a child node "lag". We should add that.
656 657
     * 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
658
     */
659 660 661 662 663 664 665

    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
666
    xmlSetProp(node, XMLSTR("id"), XMLSTR(buf));
667
    xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "ID" : "id"), XMLSTR(buf));
668

669
    xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "IP" : "ip"), XMLSTR(client->con->ip));
670 671 672

    tmp = httpp_getvar(client->parser, "user-agent");
    if (tmp)
673
        xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "UserAgent" : "useragent"), XMLSTR(tmp));
674 675 676

    tmp = httpp_getvar(client->parser, "referer");
    if (tmp)
677
        xmlNewTextChild(node, NULL, XMLSTR("referer"), XMLSTR(tmp));
678 679

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

    if (client->username)
683
        xmlNewTextChild(node, NULL, XMLSTR("username"), XMLSTR(client->username));
684

Philipp Schafft's avatar
Philipp Schafft committed
685
    if (client->role)
686
        xmlNewTextChild(node, NULL, XMLSTR("role"), XMLSTR(client->role));
Philipp Schafft's avatar
Philipp Schafft committed
687

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

690 691 692 693 694 695 696 697 698
    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;
    }

699 700 701
    return node;
}

Marvin Scholz's avatar
Marvin Scholz committed
702 703 704 705
void admin_add_listeners_to_mount(source_t          *source,
                                  xmlNodePtr        parent,
                                  operation_mode    mode)
{
706 707 708 709 710 711
    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) {
712
        __add_listener((client_t *)client_node->key, parent, now, mode);
713 714 715 716 717
        client_node = avl_get_next(client_node);
    }
    avl_tree_unlock(source->client_tree);
}

Marvin Scholz's avatar
Marvin Scholz committed
718 719
static void command_show_listeners(client_t *client,
                                   source_t *source,
720
                                   admin_format_t response)
721
{
722
    xmlDocPtr doc;
723
    xmlNodePtr node, srcnode;
724
    char buf[22];
725

726
    doc = xmlNewDoc(XMLSTR("1.0"));
727 728 729
    node = xmlNewDocNode(doc, NULL, XMLSTR("icestats"), NULL);
    srcnode = xmlNewChild(node, NULL, XMLSTR("source"), NULL);
    xmlSetProp(srcnode, XMLSTR("mount"), XMLSTR(source->mount));
730
    xmlDocSetRootElement(doc, node);
731

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

737
    admin_add_listeners_to_mount(source, srcnode, client->mode);
738

739
    admin_send_response(doc, client, response,
740 741
        LISTCLIENTS_TRANSFORMED_REQUEST);
    xmlFreeDoc(doc);
742 743
}

744
static void command_buildm3u(client_t *client, source_t *source, admin_format_t format)
745
{
Marvin Scholz's avatar
Marvin Scholz committed
746
    const char *mount = source->mount;
747 748
    const char *username = NULL;
    const char *password = NULL;
749
    ice_config_t *config;
750
    ssize_t ret;
751 752 753 754

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

Marvin Scholz's avatar
Marvin Scholz committed
755 756
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
757
                                 "audio/x-mpegurl", NULL,
758
                                 NULL, NULL, client);
759

Marvin Scholz's avatar
Marvin Scholz committed
760 761
    if (ret == -1 || ret >= (PER_CLIENT_REFBUF_SIZE - 512)) {
        /* we want at least 512 Byte left for data */
762
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
763
        client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
764 765 766 767
        return;
    }


Karl Heyes's avatar
Karl Heyes committed
768
    config = config_get_config();
769
    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
770
        "Content-Disposition: attachment; filename=listen.m3u\r\n\r\n"
771 772 773
        "http://%s:%s@%s:%d%s\r\n",
        username,
        password,
Karl Heyes's avatar
Karl Heyes committed
774 775
        config->hostname,
        config->port,
776
        mount
777
    );
Karl Heyes's avatar
Karl Heyes committed
778
    config_release_config();
779

780
    client->respcode = 200;
Karl Heyes's avatar
Karl Heyes committed
781 782
    client->refbuf->len = strlen (client->refbuf->data);
    fserve_add_client (client, NULL);
783
}
784

Marvin Scholz's avatar
Marvin Scholz committed
785 786
xmlNodePtr admin_add_role_to_authentication(auth_t *auth, xmlNodePtr parent)
{
787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805
    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;
}
806

807
static void command_manageauth(client_t *client, source_t *source, admin_format_t response)
Marvin Scholz's avatar
Marvin Scholz committed
808
{
809
    xmlDocPtr doc;
810
    xmlNodePtr node, rolenode, usersnode, msgnode;
811 812
    const char *action = NULL;
    const char *username = NULL;
813
    const char *idstring = NULL;
814 815
    char *message = NULL;
    int ret = AUTH_OK;
816
    int error_id = ICECAST_ERROR_ADMIN_missing_parameter;
817 818
    long unsigned int id;
    ice_config_t *config = config_get_config();
Philipp Schafft's avatar
Philipp Schafft committed
819
    auth_t *auth;
820

Marvin Scholz's avatar
Marvin Scholz committed
821
    do {
822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842
        /* 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);
843
            error_id = ICECAST_ERROR_ADMIN_ROLEMGN_ROLE_NOT_FOUND;
844
            break;
845
        }
Philipp Schafft's avatar
Philipp Schafft committed
846

847
        COMMAND_OPTIONAL(client, "action", action);
848
        COMMAND_OPTIONAL(client, "username", username);
849 850

        if (action == NULL)
851
            action = "list";
852

Marvin Scholz's avatar
Marvin Scholz committed
853
        if (!strcmp(action, "add")) {
854
            const char *password = NULL;
855
            COMMAND_OPTIONAL(client, "password", password);
856

857 858
            if (username == NULL || password == NULL) {
                ICECAST_LOG_WARN("manage auth request add for %lu but no user/pass", id);
859 860
                break;
            }
861 862

            if (!auth->adduser) {
863
                error_id = ICECAST_ERROR_ADMIN_ROLEMGN_ADD_NOSYS;
864 865 866
                break;
            }

Philipp Schafft's avatar
Philipp Schafft committed
867
            ret = auth->adduser(auth, username, password);
868 869
            if (ret == AUTH_FAILED) {
                message = strdup("User add failed - check the icecast error log");
870
            } else if (ret == AUTH_USERADDED) {
871
                message = strdup("User added");
872
            } else if (ret == AUTH_USEREXISTS) {
873 874 875
                message = strdup("User already exists - not added");
            }
        }
Marvin Scholz's avatar
Marvin Scholz committed
876
        if (!strcmp(action, "delete")) {
877 878 879 880 881 882
            if (username == NULL) {
                ICECAST_LOG_WARN("manage auth request delete for %lu but no username", id);
                break;
            }

            if (!auth->deleteuser) {
883
                error_id = ICECAST_ERROR_ADMIN_ROLEMGN_DELETE_NOSYS;
884 885
                break;
            }
886

Philipp Schafft's avatar
Philipp Schafft committed
887
            ret = auth->deleteuser(auth, username);
888 889
            if (ret == AUTH_FAILED) {
                message = strdup("User delete failed - check the icecast error log");
890
            } else if (ret == AUTH_USERDELETED) {
891 892 893 894
                message = strdup("User deleted");
            }
        }

895
        doc = xmlNewDoc(XMLSTR("1.0"));
896
        node = xmlNewDocNode(doc, NULL, XMLSTR("icestats"), NULL);
897

898
        rolenode = admin_add_role_to_authentication(auth, node);
899

900
        if (message) {
901
            msgnode = xmlNewChild(node, NULL, XMLSTR("iceresponse"), NULL);
902
            xmlNewTextChild(msgnode, NULL, XMLSTR("message"), XMLSTR(message));
903
        }
904

905
        xmlDocSetRootElement(doc, node);
906

907 908 909 910
        if (auth && auth->listuser) {
            usersnode = xmlNewChild(rolenode, NULL, XMLSTR("users"), NULL);
            auth->listuser(auth, usersnode);
        }
911

912 913
        config_release_config();
        auth_release(auth);
914

915
        admin_send_response(doc, client, response,
Marvin Scholz's avatar
Marvin Scholz committed
916 917
            MANAGEAUTH_TRANSFORMED_REQUEST);
        free(message);
918 919 920 921
        xmlFreeDoc(doc);
        return;
    } while (0);

922 923
    config_release_config();
    auth_release(auth);
924
    client_send_error_by_id(client, error_id);
925 926
}

Marvin Scholz's avatar
Marvin Scholz committed
927 928
static void command_kill_source(client_t *client,
                                source_t *source,
929
                                admin_format_t response)
930
{
931 932 933
    xmlDocPtr doc;
    xmlNodePtr node;

934 935
    doc = xmlNewDoc (XMLSTR("1.0"));
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
936 937
    xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR("Source Removed"));
    xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
938 939
    xmlDocSetRootElement(doc, node);

940 941
    source->running = 0;

942
    admin_send_response(doc, client, response,
943 944
        ADMIN_XSL_RESPONSE);
    xmlFreeDoc(doc);
945 946
}

Marvin Scholz's avatar
Marvin Scholz committed
947 948
static void command_kill_client(client_t *client,
                                source_t *source,
949
                                admin_format_t response)
950
{
951
    const char *idtext;
952 953
    int id;
    client_t *listener;
954 955 956
    xmlDocPtr doc;
    xmlNodePtr node;
    char buf[50] = "";
957 958 959 960 961 962 963

    COMMAND_REQUIRE(client, "id", idtext);

    id = atoi(idtext);

    listener = source_find_client(source, id);

Marvin Scholz's avatar
Marvin Scholz committed
964
    doc = xmlNewDoc(XMLSTR("1.0"));
965
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
966
    xmlDocSetRootElement(doc, node);
967
    ICECAST_LOG_DEBUG("Response is %d", response);
968

969
    if(listener != NULL) {
970
        ICECAST_LOG_INFO("Admin request: client %d removed", id);
971 972 973 974 975

        /* This tags it for removal on the next iteration of the main source
         * loop
         */
        listener->con->error = 1;
976 977
        memset(buf, '\000', sizeof(buf));
        snprintf(buf, sizeof(buf)-1, "Client %d removed", id);
978 979
        xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
        xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
980 981
    }
    else {
982 983
        memset(buf, '\000', sizeof(buf));
        snprintf(buf, sizeof(buf)-1, "Client %d not found", id);
984 985
        xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
        xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("0"));
986
    }
987
    admin_send_response(doc, client, response,
988 989
        ADMIN_XSL_RESPONSE);
    xmlFreeDoc(doc);
990 991
}

Marvin Scholz's avatar
Marvin Scholz committed
992 993
static void command_fallback(client_t *client,
                             source_t *source,
994
                             admin_format_t response)
995
{
996
    const char *fallback;
997 998
    char *old;

999
    ICECAST_LOG_DEBUG("Got fallback request");
1000 1001 1002 1003 1004 1005 1006

    COMMAND_REQUIRE(client, "fallback", fallback);

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

1007
    html_success(client, "Fallback configured");
1008 1009
}