admin.c 43.4 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
#define ADMIN_MAX_COMMAND_TABLES        8

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

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

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

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

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

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

static const admin_command_handler_t handlers[] = {
120 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
    { "*",                                  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
151 152
};

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

157 158 159 160 161 162 163 164 165 166 167
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;
}

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

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

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

        return NULL;
    }

    len = end - command;

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

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

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

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

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

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

    return COMMAND_ERROR;
}

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

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

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

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

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

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

    return ADMINTYPE_ERROR;
282 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
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;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Marvin Scholz's avatar
Marvin Scholz committed
532
    if (handler->type == ADMINTYPE_MOUNT && !source) {
533
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_MISSING_PARAMETER);
Marvin Scholz's avatar
Marvin Scholz committed
534
        return;
535 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;
    }

    handler->function(client, source, format);
Marvin Scholz's avatar
Marvin Scholz committed
544 545
    if (source) {
        avl_tree_unlock(global.source_tree);
546
    }
Marvin Scholz's avatar
Marvin Scholz committed
547
    return;
548 549
}

550
static void html_success(client_t *client, char *message)
551
{
552 553
    ssize_t ret;

Marvin Scholz's avatar
Marvin Scholz committed
554 555
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
556
                                 "text/html", "utf-8",
557
                                 "", NULL, client);
558 559 560

    if (ret == -1 || ret >= PER_CLIENT_REFBUF_SIZE) {
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
561
        client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
562 563 564
        return;
    }

565
    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
566 567
        "<html><head><title>Admin request successful</title></head>"
        "<body><p>%s</p></body></html>", message);
568

569
    client->respcode = 200;
Marvin Scholz's avatar
Marvin Scholz committed
570 571
    client->refbuf->len = strlen(client->refbuf->data);
    fserve_add_client(client, NULL);
572 573
}

574

Marvin Scholz's avatar
Marvin Scholz committed
575 576
static void command_move_clients(client_t   *client,
                                 source_t   *source,
577
                                 admin_format_t response)
578
{
579
    const char *dest_source;
580
    source_t *dest;
581 582 583 584 585
    xmlDocPtr doc;
    xmlNodePtr node;
    char buf[255];
    int parameters_passed = 0;

586
    ICECAST_LOG_DEBUG("Doing optional check");
587
    if((COMMAND_OPTIONAL(client, "destination", dest_source))) {
588 589
        parameters_passed = 1;
    }
590
    ICECAST_LOG_DEBUG("Done optional check (%d)", parameters_passed);
591 592
    if (!parameters_passed) {
        doc = admin_build_sourcelist(source->mount);
593
        admin_send_response(doc, client, response,
594 595 596 597
             MOVECLIENTS_TRANSFORMED_REQUEST);
        xmlFreeDoc(doc);
        return;
    }
598

Marvin Scholz's avatar
Marvin Scholz committed
599
    dest = source_find_mount(dest_source);
600

Marvin Scholz's avatar
Marvin Scholz committed
601
    if (dest == NULL) {
602
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_NO_SUCH_DESTINATION);
603 604 605
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
606
    if (strcmp(dest->mount, source->mount) == 0) {
607
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SUPPLIED_MOUNTPOINTS_ARE_IDENTICAL);
608 609 610
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
611
    if (dest->running == 0 && dest->on_demand == 0) {
612
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_DEST_NOT_RUNNING);
613 614 615
        return;
    }

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

Marvin Scholz's avatar
Marvin Scholz committed
618
    doc = xmlNewDoc(XMLSTR("1.0"));
619
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
620 621
    xmlDocSetRootElement(doc, node);

Marvin Scholz's avatar
Marvin Scholz committed
622
    source_move_clients(source, dest);
623

Marvin Scholz's avatar
Marvin Scholz committed
624
    snprintf(buf, sizeof(buf), "Clients moved from %s to %s",
625
        source->mount, dest_source);
626 627
    xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
    xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
628

Marvin Scholz's avatar
Marvin Scholz committed
629
    admin_send_response(doc, client, response, ADMIN_XSL_RESPONSE);
630
    xmlFreeDoc(doc);
631 632
}

Marvin Scholz's avatar
Marvin Scholz committed
633 634 635 636 637
static inline xmlNodePtr __add_listener(client_t        *client,
                                        xmlNodePtr      parent,
                                        time_t          now,
                                        operation_mode  mode)
{
638 639 640 641
    const char *tmp;
    xmlNodePtr node;
    char buf[22];

Philipp Schafft's avatar
Philipp Schafft committed
642
    /* TODO: kh has support for a child node "lag". We should add that.
643 644
     * 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
645
     */
646 647 648 649 650 651 652

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

656
    xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "IP" : "ip"), XMLSTR(client->con->ip));
657 658 659

    tmp = httpp_getvar(client->parser, "user-agent");
    if (tmp)
660
        xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "UserAgent" : "useragent"), XMLSTR(tmp));
661 662 663

    tmp = httpp_getvar(client->parser, "referer");
    if (tmp)
664
        xmlNewTextChild(node, NULL, XMLSTR("referer"), XMLSTR(tmp));
665 666

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

    if (client->username)
670
        xmlNewTextChild(node, NULL, XMLSTR("username"), XMLSTR(client->username));
671

Philipp Schafft's avatar
Philipp Schafft committed
672
    if (client->role)
673
        xmlNewTextChild(node, NULL, XMLSTR("role"), XMLSTR(client->role));
Philipp Schafft's avatar
Philipp Schafft committed
674

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

677 678 679 680 681 682 683 684 685
    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;
    }

686 687 688
    return node;
}

Marvin Scholz's avatar
Marvin Scholz committed
689 690 691 692
void admin_add_listeners_to_mount(source_t          *source,
                                  xmlNodePtr        parent,
                                  operation_mode    mode)
{
693 694 695 696 697 698
    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) {
699
        __add_listener((client_t *)client_node->key, parent, now, mode);
700 701 702 703 704
        client_node = avl_get_next(client_node);
    }
    avl_tree_unlock(source->client_tree);
}

Marvin Scholz's avatar
Marvin Scholz committed
705 706
static void command_show_listeners(client_t *client,
                                   source_t *source,
707
                                   admin_format_t response)
708
{
709
    xmlDocPtr doc;
710
    xmlNodePtr node, srcnode;
711
    char buf[22];
712

713
    doc = xmlNewDoc(XMLSTR("1.0"));
714 715 716
    node = xmlNewDocNode(doc, NULL, XMLSTR("icestats"), NULL);
    srcnode = xmlNewChild(node, NULL, XMLSTR("source"), NULL);
    xmlSetProp(srcnode, XMLSTR("mount"), XMLSTR(source->mount));
717
    xmlDocSetRootElement(doc, node);
718

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

724
    admin_add_listeners_to_mount(source, srcnode, client->mode);
725

726
    admin_send_response(doc, client, response,
727 728
        LISTCLIENTS_TRANSFORMED_REQUEST);
    xmlFreeDoc(doc);
729 730
}

731
static void command_buildm3u(client_t *client, source_t *source, admin_format_t format)
732
{
Marvin Scholz's avatar
Marvin Scholz committed
733
    const char *mount = source->mount;
734 735
    const char *username = NULL;
    const char *password = NULL;
736
    ice_config_t *config;
737
    ssize_t ret;
738 739 740 741

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

Marvin Scholz's avatar
Marvin Scholz committed
742 743
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
744
                                 "audio/x-mpegurl", NULL,
745
                                 NULL, NULL, client);
746

Marvin Scholz's avatar
Marvin Scholz committed
747 748
    if (ret == -1 || ret >= (PER_CLIENT_REFBUF_SIZE - 512)) {
        /* we want at least 512 Byte left for data */
749
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
750
        client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
751 752 753 754
        return;
    }


Karl Heyes's avatar
Karl Heyes committed
755
    config = config_get_config();
756
    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
757
        "Content-Disposition: attachment; filename=listen.m3u\r\n\r\n"
758 759 760
        "http://%s:%s@%s:%d%s\r\n",
        username,
        password,
Karl Heyes's avatar
Karl Heyes committed
761 762
        config->hostname,
        config->port,
763
        mount
764
    );
Karl Heyes's avatar
Karl Heyes committed
765
    config_release_config();
766

767
    client->respcode = 200;
Karl Heyes's avatar
Karl Heyes committed
768 769
    client->refbuf->len = strlen (client->refbuf->data);
    fserve_add_client (client, NULL);
770
}
771

Marvin Scholz's avatar
Marvin Scholz committed
772 773
xmlNodePtr admin_add_role_to_authentication(auth_t *auth, xmlNodePtr parent)
{
774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792
    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;
}
793

794
static void command_manageauth(client_t *client, source_t *source, admin_format_t response)
Marvin Scholz's avatar
Marvin Scholz committed
795
{
796
    xmlDocPtr doc;
797
    xmlNodePtr node, rolenode, usersnode, msgnode;
798 799
    const char *action = NULL;
    const char *username = NULL;
800
    const char *idstring = NULL;
801 802
    char *message = NULL;
    int ret = AUTH_OK;
803
    int error_id = ICECAST_ERROR_ADMIN_missing_parameter;
804 805
    long unsigned int id;
    ice_config_t *config = config_get_config();
Philipp Schafft's avatar
Philipp Schafft committed
806
    auth_t *auth;
807

Marvin Scholz's avatar
Marvin Scholz committed
808
    do {
809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829
        /* 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);
830
            error_id = ICECAST_ERROR_ADMIN_ROLEMGN_ROLE_NOT_FOUND;
831
            break;
832
        }
Philipp Schafft's avatar
Philipp Schafft committed
833

834
        COMMAND_OPTIONAL(client, "action", action);
835
        COMMAND_OPTIONAL(client, "username", username);
836 837

        if (action == NULL)
838
            action = "list";
839

Marvin Scholz's avatar
Marvin Scholz committed
840
        if (!strcmp(action, "add")) {
841
            const char *password = NULL;
842
            COMMAND_OPTIONAL(client, "password", password);
843

844 845
            if (username == NULL || password == NULL) {
                ICECAST_LOG_WARN("manage auth request add for %lu but no user/pass", id);
846 847
                break;
            }
848 849

            if (!auth->adduser) {
850
                error_id = ICECAST_ERROR_ADMIN_ROLEMGN_ADD_NOSYS;
851 852 853
                break;
            }

Philipp Schafft's avatar
Philipp Schafft committed
854
            ret = auth->adduser(auth, username, password);
855 856
            if (ret == AUTH_FAILED) {
                message = strdup("User add failed - check the icecast error log");
857
            } else if (ret == AUTH_USERADDED) {
858
                message = strdup("User added");
859
            } else if (ret == AUTH_USEREXISTS) {
860 861 862
                message = strdup("User already exists - not added");
            }
        }
Marvin Scholz's avatar
Marvin Scholz committed
863
        if (!strcmp(action, "delete")) {
864 865 866 867 868 869
            if (username == NULL) {
                ICECAST_LOG_WARN("manage auth request delete for %lu but no username", id);
                break;
            }

            if (!auth->deleteuser) {
870
                error_id = ICECAST_ERROR_ADMIN_ROLEMGN_DELETE_NOSYS;
871 872
                break;
            }
873

Philipp Schafft's avatar
Philipp Schafft committed
874
            ret = auth->deleteuser(auth, username);
875 876
            if (ret == AUTH_FAILED) {
                message = strdup("User delete failed - check the icecast error log");
877
            } else if (ret == AUTH_USERDELETED) {
878 879 880 881
                message = strdup("User deleted");
            }
        }

882
        doc = xmlNewDoc(XMLSTR("1.0"));
883
        node = xmlNewDocNode(doc, NULL, XMLSTR("icestats"), NULL);
884

885
        rolenode = admin_add_role_to_authentication(auth, node);
886

887
        if (message) {
888
            msgnode = xmlNewChild(node, NULL, XMLSTR("iceresponse"), NULL);
889
            xmlNewTextChild(msgnode, NULL, XMLSTR("message"), XMLSTR(message));
890
        }
891

892
        xmlDocSetRootElement(doc, node);
893

894 895 896 897
        if (auth && auth->listuser) {
            usersnode = xmlNewChild(rolenode, NULL, XMLSTR("users"), NULL);
            auth->listuser(auth, usersnode);
        }
898

899 900
        config_release_config();
        auth_release(auth);
901

902
        admin_send_response(doc, client, response,
Marvin Scholz's avatar
Marvin Scholz committed
903 904
            MANAGEAUTH_TRANSFORMED_REQUEST);
        free(message);
905 906 907 908
        xmlFreeDoc(doc);
        return;
    } while (0);

909 910
    config_release_config();
    auth_release(auth);
911
    client_send_error_by_id(client, error_id);
912 913
}

Marvin Scholz's avatar
Marvin Scholz committed
914 915
static void command_kill_source(client_t *client,
                                source_t *source,
916
                                admin_format_t response)
917
{
918 919 920
    xmlDocPtr doc;
    xmlNodePtr node;

921 922
    doc = xmlNewDoc (XMLSTR("1.0"));
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
923 924
    xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR("Source Removed"));
    xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
925 926
    xmlDocSetRootElement(doc, node);

927 928
    source->running = 0;

929
    admin_send_response(doc, client, response,
930 931
        ADMIN_XSL_RESPONSE);
    xmlFreeDoc(doc);
932 933
}

Marvin Scholz's avatar
Marvin Scholz committed
934 935
static void command_kill_client(client_t *client,
                                source_t *source,
936
                                admin_format_t response)
937
{
938
    const char *idtext;
939 940
    int id;
    client_t *listener;
941 942 943
    xmlDocPtr doc;
    xmlNodePtr node;
    char buf[50] = "";
944 945 946 947 948 949 950

    COMMAND_REQUIRE(client, "id", idtext);

    id = atoi(idtext);

    listener = source_find_client(source, id);

Marvin Scholz's avatar
Marvin Scholz committed
951
    doc = xmlNewDoc(XMLSTR("1.0"));
952
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
953
    xmlDocSetRootElement(doc, node);
954
    ICECAST_LOG_DEBUG("Response is %d", response);
955

956
    if(listener != NULL) {
957
        ICECAST_LOG_INFO("Admin request: client %d removed", id);
958 959 960 961 962

        /* This tags it for removal on the next iteration of the main source
         * loop
         */
        listener->con->error = 1;
963 964
        memset(buf, '\000', sizeof(buf));
        snprintf(buf, sizeof(buf)-1, "Client %d removed", id);
965 966
        xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
        xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
967 968
    }
    else {
969 970
        memset(buf, '\000', sizeof(buf));
        snprintf(buf, sizeof(buf)-1, "Client %d not found", id);
971 972
        xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
        xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("0"));
973
    }
974
    admin_send_response(doc, client, response,
975 976
        ADMIN_XSL_RESPONSE);
    xmlFreeDoc(doc);
977 978
}

Marvin Scholz's avatar
Marvin Scholz committed
979 980
static void command_fallback(client_t *client,
                             source_t *source,
981
                             admin_format_t response)
982
{
983
    const char *fallback;
984 985
    char *old;

986
    ICECAST_LOG_DEBUG("Got fallback request");
987 988 989 990 991 992 993

    COMMAND_REQUIRE(client, "fallback", fallback);

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

994
    html_success(client, "Fallback configured");
995 996
}

Marvin Scholz's avatar
Marvin Scholz committed
997 998
static void command_metadata(client_t *client,
                             source_t *source,
999
                             admin_format_t response)
1000
{
1001