admin.c 42.7 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
static admin_command_table_t command_tables[] = {
    {.prefix = NULL, .length = (sizeof(handlers)/sizeof(*handlers)), .handlers = handlers},
};

164 165 166 167 168 169 170 171 172 173 174
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;
}

175 176 177 178 179 180 181
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;

182 183 184
    if (!__is_command_table_valid(&(command_tables[t])))
        return NULL;

185 186 187 188 189 190 191 192 193 194 195 196 197
    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++)
198
            if (command_tables[i].prefix == NULL && __is_command_table_valid(&(command_tables[i])))
199 200 201 202 203 204 205 206
                return &(command_tables[i]);

        return NULL;
    }

    len = end - command;

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

210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
        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;

228 229 230
    if (!__is_command_table_valid(table))
        return ADMIN_COMMAND_ERROR;

231 232 233 234 235 236 237
    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
238

239
admin_command_id_t admin_get_command(const char *command)
Marvin Scholz's avatar
Marvin Scholz committed
240
{
Philipp Schafft's avatar
Philipp Schafft committed
241
    size_t i;
242 243
    const admin_command_table_t *table = admin_get_table_by_prefix(command);
    const char *suffix;
Philipp Schafft's avatar
Philipp Schafft committed
244

245 246 247 248 249 250 251 252 253 254 255 256 257
    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
258 259 260 261

    return COMMAND_ERROR;
}

Marvin Scholz's avatar
Marvin Scholz committed
262 263
/* Get the command handler for command or NULL
 */
264
const admin_command_handler_t* admin_get_handler(admin_command_id_t command)
Marvin Scholz's avatar
Marvin Scholz committed
265
{
266 267
    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
268

269 270 271 272 273 274 275
    if (table == NULL)
        return NULL;

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

    return &(table->handlers[index]);
Marvin Scholz's avatar
Marvin Scholz committed
276 277 278 279 280
}

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

Marvin Scholz's avatar
Marvin Scholz committed
285 286
    if (handler != NULL)
        return handler->type;
Philipp Schafft's avatar
Philipp Schafft committed
287 288

    return ADMINTYPE_ERROR;
289 290
}

291 292 293
/* 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
294
xmlDocPtr admin_build_sourcelist(const char *mount)
295 296 297 298 299 300 301 302
{
    avl_node *node;
    source_t *source;
    xmlNodePtr xmlnode, srcnode;
    xmlDocPtr doc;
    char buf[22];
    time_t now = time(NULL);

303 304
    doc = xmlNewDoc (XMLSTR("1.0"));
    xmlnode = xmlNewDocNode (doc, NULL, XMLSTR("icestats"), NULL);
305
    xmlDocSetRootElement(doc, xmlnode);
306

307
    if (mount) {
308
        xmlNewTextChild (xmlnode, NULL, XMLSTR("current_source"), XMLSTR(mount));
309 310 311 312 313
    }

    node = avl_get_first(global.source_tree);
    while(node) {
        source = (source_t *)node->key;
314 315 316 317 318 319
        if (mount && strcmp (mount, source->mount) == 0)
        {
            node = avl_get_next (node);
            continue;
        }

320
        if (source->running || source->on_demand)
321
        {
322 323
            ice_config_t *config;
            mount_proxy *mountinfo;
Philipp Schafft's avatar
Philipp Schafft committed
324
            acl_t *acl = NULL;
325

326 327
            srcnode = xmlNewChild(xmlnode, NULL, XMLSTR("source"), NULL);
            xmlSetProp(srcnode, XMLSTR("mount"), XMLSTR(source->mount));
328

329
            xmlNewTextChild(srcnode, NULL, XMLSTR("fallback"),
330
                    (source->fallback_mount != NULL)?
331
                    XMLSTR(source->fallback_mount):XMLSTR(""));
332
            snprintf(buf, sizeof(buf), "%lu", source->listeners);
333
            xmlNewTextChild(srcnode, NULL, XMLSTR("listeners"), XMLSTR(buf));
334

Karl Heyes's avatar
Karl Heyes committed
335
            config = config_get_config();
Marvin Scholz's avatar
Marvin Scholz committed
336
            mountinfo = config_find_mount(config, source->mount, MOUNT_TYPE_NORMAL);
Philipp Schafft's avatar
Philipp Schafft committed
337
            if (mountinfo)
338
                acl = auth_stack_get_anonymous_acl(mountinfo->authstack, httpp_req_get);
Philipp Schafft's avatar
Philipp Schafft committed
339
            if (!acl)
340
                acl = auth_stack_get_anonymous_acl(config->authstack, httpp_req_get);
Philipp Schafft's avatar
Philipp Schafft committed
341
            if (acl && acl_test_web(acl) == ACL_POLICY_DENY) {
342
                xmlNewTextChild(srcnode, NULL, XMLSTR("authenticator"), XMLSTR("(dummy)"));
343
            }
Philipp Schafft's avatar
Philipp Schafft committed
344
            acl_release(acl);
345 346
            config_release_config();

Marvin Scholz's avatar
Marvin Scholz committed
347 348 349 350
            if (source->running) {
                if (source->client) {
                    snprintf(buf, sizeof(buf), "%lu",
                        (unsigned long)(now - source->con->con_time));
351
                    xmlNewTextChild(srcnode, NULL, XMLSTR("Connected"), XMLSTR(buf));
Karl Heyes's avatar
Karl Heyes committed
352
                }
353
                xmlNewTextChild(srcnode, NULL, XMLSTR("content-type"),
Marvin Scholz's avatar
Marvin Scholz committed
354
                    XMLSTR(source->format->contenttype));
355
            }
356
        }
357 358 359 360 361
        node = avl_get_next(node);
    }
    return(doc);
}

362 363 364 365
void admin_send_response(xmlDocPtr       doc,
                         client_t       *client,
                         admin_format_t  response,
                         const char     *xslt_template)
366
{
367
    if (response == ADMIN_FORMAT_RAW) {
368 369
        xmlChar *buff = NULL;
        int len = 0;
370 371 372
        size_t buf_len;
        ssize_t ret;

373
        xmlDocDumpMemory(doc, &buff, &len);
374 375 376 377

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

379 380
        client_set_queue(client, NULL);
        client->refbuf = refbuf_new(buf_len);
381

382 383 384
        ret = util_http_build_header(client->refbuf->data, buf_len, 0,
                                     0, 200, NULL,
                                     "text/xml", "utf-8",
385
                                     NULL, NULL, client);
Philipp Schafft's avatar
Philipp Schafft committed
386
        if (ret < 0) {
387
            ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
388
            client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
389 390
            xmlFree(buff);
            return;
Philipp Schafft's avatar
Philipp Schafft committed
391
        } else if (buf_len < (size_t)(len + ret + 64)) {
392 393 394 395 396 397 398 399 400 401
            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",
402
                                             NULL, NULL, client);
403 404
                if (ret == -1) {
                    ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
405
                    client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
406 407 408 409 410
                    xmlFree(buff);
                    return;
                }
            } else {
                ICECAST_LOG_ERROR("Client buffer reallocation failed. Dropping client.");
411
                client_send_error_by_id(client, ICECAST_ERROR_GEN_BUFFER_REALLOC);
412 413
                xmlFree(buff);
                return;
414
            }
415
        }
416

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

        client->refbuf->len = ret;
421 422 423
        xmlFree(buff);
        client->respcode = 200;
        fserve_add_client (client, NULL);
424
    }
425
    if (response == ADMIN_FORMAT_TRANSFORMED) {
426
        char *fullpath_xslt_template;
427
        size_t fullpath_xslt_template_len;
428 429
        ice_config_t *config = config_get_config();

430
        fullpath_xslt_template_len = strlen(config->adminroot_dir) + strlen(xslt_template) + strlen(PATH_SEPARATOR) + 1;
431 432
        fullpath_xslt_template = malloc(fullpath_xslt_template_len);
        snprintf(fullpath_xslt_template, fullpath_xslt_template_len, "%s%s%s",
433
            config->adminroot_dir, PATH_SEPARATOR, xslt_template);
434
        config_release_config();
435

436
        ICECAST_LOG_DEBUG("Sending XSLT (%s)", fullpath_xslt_template);
437 438 439 440
        xslt_transform(doc, fullpath_xslt_template, client);
        free(fullpath_xslt_template);
    }
}
441

442
void admin_handle_request(client_t *client, const char *uri)
443
{
Marvin Scholz's avatar
Marvin Scholz committed
444 445 446
    const char *mount;
    const admin_command_handler_t* handler;
    source_t *source = NULL;
447
    admin_format_t format;
448

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

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

Marvin Scholz's avatar
Marvin Scholz committed
453
    /* Check if admin command is valid */
454
    if (handler == NULL || handler->function == NULL) {
Marvin Scholz's avatar
Marvin Scholz committed
455 456
        ICECAST_LOG_ERROR("Error parsing command string or unrecognised command: %H",
                uri);
457
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_UNRECOGNISED_COMMAND);
458 459 460
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
461
    /* Check ACL */
Philipp Schafft's avatar
Philipp Schafft committed
462
    if (acl_test_admin(client->acl, client->admin_command) != ACL_POLICY_ALLOW) {
Marvin Scholz's avatar
Marvin Scholz committed
463 464

        /* ACL disallows, check exceptions */
465
        if ((handler->function == command_metadata && handler->format == ADMIN_FORMAT_RAW) &&
Philipp Schafft's avatar
Philipp Schafft committed
466 467
            (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
468 469
            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
470
        } else {
471
            client_send_error_by_id(client, ICECAST_ERROR_GEN_CLIENT_NEEDS_TO_AUTHENTICATE);
472 473
            return;
        }
474 475
    }

476 477
    mount = httpp_get_query_param(client->parser, "mount");

Marvin Scholz's avatar
Marvin Scholz committed
478
    /* Find mountpoint source */
479
    if(mount != NULL) {
480

Philipp Schafft's avatar
Philipp Schafft committed
481
        /* This is a mount request, handle it as such */
482
        avl_tree_rlock(global.source_tree);
Michael Smith's avatar
Michael Smith committed
483
        source = source_find_mount_raw(mount);
484

Marvin Scholz's avatar
Marvin Scholz committed
485
        /* No Source found */
Marvin Scholz's avatar
Marvin Scholz committed
486
        if (source == NULL) {
487
            avl_tree_unlock(global.source_tree);
Marvin Scholz's avatar
Marvin Scholz committed
488 489
            ICECAST_LOG_WARN("Admin command \"%H\" on non-existent source \"%H\"",
                    uri, mount);
490
            client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SOURCE_DOES_NOT_EXIST);
Marvin Scholz's avatar
Marvin Scholz committed
491 492 493
            return;
        } /* No Source running */
        else if (source->running == 0 && source->on_demand == 0) {
494
            avl_tree_unlock(global.source_tree);
Marvin Scholz's avatar
Marvin Scholz committed
495 496
            ICECAST_LOG_INFO("Received admin command \"%H\" on unavailable mount \"%H\"",
                    uri, mount);
497
            client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SOURCE_IS_NOT_AVAILABLE);
Marvin Scholz's avatar
Marvin Scholz committed
498
            return;
499
        }
Marvin Scholz's avatar
Marvin Scholz committed
500 501
        ICECAST_LOG_INFO("Received admin command %H on mount '%s'",
                    uri, mount);
502 503
    }

Marvin Scholz's avatar
Marvin Scholz committed
504
    if (handler->type == ADMINTYPE_MOUNT && !source) {
505
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_MISSING_PARAMETER);
Marvin Scholz's avatar
Marvin Scholz committed
506
        return;
507 508
    }

509 510 511 512 513 514 515
    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
516 517
    if (source) {
        avl_tree_unlock(global.source_tree);
518
    }
Marvin Scholz's avatar
Marvin Scholz committed
519
    return;
520 521
}

522
static void html_success(client_t *client, char *message)
523
{
524 525
    ssize_t ret;

Marvin Scholz's avatar
Marvin Scholz committed
526 527
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
528
                                 "text/html", "utf-8",
529
                                 "", NULL, client);
530 531 532

    if (ret == -1 || ret >= PER_CLIENT_REFBUF_SIZE) {
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
533
        client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
534 535 536
        return;
    }

537
    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
538 539
        "<html><head><title>Admin request successful</title></head>"
        "<body><p>%s</p></body></html>", message);
540

541
    client->respcode = 200;
Marvin Scholz's avatar
Marvin Scholz committed
542 543
    client->refbuf->len = strlen(client->refbuf->data);
    fserve_add_client(client, NULL);
544 545
}

546

Marvin Scholz's avatar
Marvin Scholz committed
547 548
static void command_move_clients(client_t   *client,
                                 source_t   *source,
549
                                 admin_format_t response)
550
{
551
    const char *dest_source;
552
    source_t *dest;
553 554 555 556 557
    xmlDocPtr doc;
    xmlNodePtr node;
    char buf[255];
    int parameters_passed = 0;

558
    ICECAST_LOG_DEBUG("Doing optional check");
559
    if((COMMAND_OPTIONAL(client, "destination", dest_source))) {
560 561
        parameters_passed = 1;
    }
562
    ICECAST_LOG_DEBUG("Done optional check (%d)", parameters_passed);
563 564
    if (!parameters_passed) {
        doc = admin_build_sourcelist(source->mount);
565
        admin_send_response(doc, client, response,
566 567 568 569
             MOVECLIENTS_TRANSFORMED_REQUEST);
        xmlFreeDoc(doc);
        return;
    }
570

Marvin Scholz's avatar
Marvin Scholz committed
571
    dest = source_find_mount(dest_source);
572

Marvin Scholz's avatar
Marvin Scholz committed
573
    if (dest == NULL) {
574
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_NO_SUCH_DESTINATION);
575 576 577
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
578
    if (strcmp(dest->mount, source->mount) == 0) {
579
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SUPPLIED_MOUNTPOINTS_ARE_IDENTICAL);
580 581 582
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
583
    if (dest->running == 0 && dest->on_demand == 0) {
584
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_DEST_NOT_RUNNING);
585 586 587
        return;
    }

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

Marvin Scholz's avatar
Marvin Scholz committed
590
    doc = xmlNewDoc(XMLSTR("1.0"));
591
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
592 593
    xmlDocSetRootElement(doc, node);

Marvin Scholz's avatar
Marvin Scholz committed
594
    source_move_clients(source, dest);
595

Marvin Scholz's avatar
Marvin Scholz committed
596
    snprintf(buf, sizeof(buf), "Clients moved from %s to %s",
597
        source->mount, dest_source);
598 599
    xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
    xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
600

Marvin Scholz's avatar
Marvin Scholz committed
601
    admin_send_response(doc, client, response, ADMIN_XSL_RESPONSE);
602
    xmlFreeDoc(doc);
603 604
}

Marvin Scholz's avatar
Marvin Scholz committed
605 606 607 608 609
static inline xmlNodePtr __add_listener(client_t        *client,
                                        xmlNodePtr      parent,
                                        time_t          now,
                                        operation_mode  mode)
{
610 611 612 613
    const char *tmp;
    xmlNodePtr node;
    char buf[22];

Philipp Schafft's avatar
Philipp Schafft committed
614
    /* TODO: kh has support for a child node "lag". We should add that.
615 616
     * 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
617
     */
618 619 620 621 622 623 624

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

628
    xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "IP" : "ip"), XMLSTR(client->con->ip));
629 630 631

    tmp = httpp_getvar(client->parser, "user-agent");
    if (tmp)
632
        xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "UserAgent" : "useragent"), XMLSTR(tmp));
633 634 635

    tmp = httpp_getvar(client->parser, "referer");
    if (tmp)
636
        xmlNewTextChild(node, NULL, XMLSTR("referer"), XMLSTR(tmp));
637 638

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

    if (client->username)
642
        xmlNewTextChild(node, NULL, XMLSTR("username"), XMLSTR(client->username));
643

Philipp Schafft's avatar
Philipp Schafft committed
644
    if (client->role)
645
        xmlNewTextChild(node, NULL, XMLSTR("role"), XMLSTR(client->role));
Philipp Schafft's avatar
Philipp Schafft committed
646

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

649 650 651 652 653 654 655 656 657
    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;
    }

658 659 660
    return node;
}

Marvin Scholz's avatar
Marvin Scholz committed
661 662 663 664
void admin_add_listeners_to_mount(source_t          *source,
                                  xmlNodePtr        parent,
                                  operation_mode    mode)
{
665 666 667 668 669 670
    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) {
671
        __add_listener((client_t *)client_node->key, parent, now, mode);
672 673 674 675 676
        client_node = avl_get_next(client_node);
    }
    avl_tree_unlock(source->client_tree);
}

Marvin Scholz's avatar
Marvin Scholz committed
677 678
static void command_show_listeners(client_t *client,
                                   source_t *source,
679
                                   admin_format_t response)
680
{
681
    xmlDocPtr doc;
682
    xmlNodePtr node, srcnode;
683
    char buf[22];
684

685
    doc = xmlNewDoc(XMLSTR("1.0"));
686 687 688
    node = xmlNewDocNode(doc, NULL, XMLSTR("icestats"), NULL);
    srcnode = xmlNewChild(node, NULL, XMLSTR("source"), NULL);
    xmlSetProp(srcnode, XMLSTR("mount"), XMLSTR(source->mount));
689
    xmlDocSetRootElement(doc, node);
690

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

696
    admin_add_listeners_to_mount(source, srcnode, client->mode);
697

698
    admin_send_response(doc, client, response,
699 700
        LISTCLIENTS_TRANSFORMED_REQUEST);
    xmlFreeDoc(doc);
701 702
}

703
static void command_buildm3u(client_t *client, source_t *source, admin_format_t format)
704
{
Marvin Scholz's avatar
Marvin Scholz committed
705
    const char *mount = source->mount;
706 707
    const char *username = NULL;
    const char *password = NULL;
708
    ice_config_t *config;
709
    ssize_t ret;
710 711 712 713

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

Marvin Scholz's avatar
Marvin Scholz committed
714 715
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
716
                                 "audio/x-mpegurl", NULL,
717
                                 NULL, NULL, client);
718

Marvin Scholz's avatar
Marvin Scholz committed
719 720
    if (ret == -1 || ret >= (PER_CLIENT_REFBUF_SIZE - 512)) {
        /* we want at least 512 Byte left for data */
721
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
722
        client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
723 724 725 726
        return;
    }


Karl Heyes's avatar
Karl Heyes committed
727
    config = config_get_config();
728
    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
729
        "Content-Disposition: attachment; filename=listen.m3u\r\n\r\n"
730 731 732
        "http://%s:%s@%s:%d%s\r\n",
        username,
        password,
Karl Heyes's avatar
Karl Heyes committed
733 734
        config->hostname,
        config->port,
735
        mount
736
    );
Karl Heyes's avatar
Karl Heyes committed
737
    config_release_config();
738

739
    client->respcode = 200;
Karl Heyes's avatar
Karl Heyes committed
740 741
    client->refbuf->len = strlen (client->refbuf->data);
    fserve_add_client (client, NULL);
742
}
743

Marvin Scholz's avatar
Marvin Scholz committed
744 745
xmlNodePtr admin_add_role_to_authentication(auth_t *auth, xmlNodePtr parent)
{
746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764
    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;
}
765

766
static void command_manageauth(client_t *client, source_t *source, admin_format_t response)
Marvin Scholz's avatar
Marvin Scholz committed
767
{
768
    xmlDocPtr doc;
769
    xmlNodePtr node, rolenode, usersnode, msgnode;
770 771
    const char *action = NULL;
    const char *username = NULL;
772
    const char *idstring = NULL;
773 774
    char *message = NULL;
    int ret = AUTH_OK;
775
    int error_id = ICECAST_ERROR_ADMIN_missing_parameter;
776 777
    long unsigned int id;
    ice_config_t *config = config_get_config();
Philipp Schafft's avatar
Philipp Schafft committed
778
    auth_t *auth;
779

Marvin Scholz's avatar
Marvin Scholz committed
780
    do {
781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801
        /* 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);
802
            error_id = ICECAST_ERROR_ADMIN_ROLEMGN_ROLE_NOT_FOUND;
803
            break;
804
        }
Philipp Schafft's avatar
Philipp Schafft committed
805

806
        COMMAND_OPTIONAL(client, "action", action);
807
        COMMAND_OPTIONAL(client, "username", username);
808 809

        if (action == NULL)
810
            action = "list";
811

Marvin Scholz's avatar
Marvin Scholz committed
812
        if (!strcmp(action, "add")) {
813
            const char *password = NULL;
814
            COMMAND_OPTIONAL(client, "password", password);
815

816 817
            if (username == NULL || password == NULL) {
                ICECAST_LOG_WARN("manage auth request add for %lu but no user/pass", id);
818 819
                break;
            }
820 821

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

Philipp Schafft's avatar
Philipp Schafft committed
826
            ret = auth->adduser(auth, username, password);
827 828
            if (ret == AUTH_FAILED) {
                message = strdup("User add failed - check the icecast error log");
829
            } else if (ret == AUTH_USERADDED) {
830
                message = strdup("User added");
831
            } else if (ret == AUTH_USEREXISTS) {
832 833 834
                message = strdup("User already exists - not added");
            }
        }
Marvin Scholz's avatar
Marvin Scholz committed
835
        if (!strcmp(action, "delete")) {
836 837 838 839 840 841
            if (username == NULL) {
                ICECAST_LOG_WARN("manage auth request delete for %lu but no username", id);
                break;
            }

            if (!auth->deleteuser) {
842
                error_id = ICECAST_ERROR_ADMIN_ROLEMGN_DELETE_NOSYS;
843 844
                break;
            }
845

Philipp Schafft's avatar
Philipp Schafft committed
846
            ret = auth->deleteuser(auth, username);
847 848
            if (ret == AUTH_FAILED) {
                message = strdup("User delete failed - check the icecast error log");
849
            } else if (ret == AUTH_USERDELETED) {
850 851 852 853
                message = strdup("User deleted");
            }
        }

854
        doc = xmlNewDoc(XMLSTR("1.0"));
855
        node = xmlNewDocNode(doc, NULL, XMLSTR("icestats"), NULL);
856

857
        rolenode = admin_add_role_to_authentication(auth, node);
858

859
        if (message) {
860
            msgnode = xmlNewChild(node, NULL, XMLSTR("iceresponse"), NULL);
861
            xmlNewTextChild(msgnode, NULL, XMLSTR("message"), XMLSTR(message));
862
        }
863

864
        xmlDocSetRootElement(doc, node);
865

866 867 868 869
        if (auth && auth->listuser) {
            usersnode = xmlNewChild(rolenode, NULL, XMLSTR("users"), NULL);
            auth->listuser(auth, usersnode);
        }
870

871 872
        config_release_config();
        auth_release(auth);
873

874
        admin_send_response(doc, client, response,
Marvin Scholz's avatar
Marvin Scholz committed
875 876
            MANAGEAUTH_TRANSFORMED_REQUEST);
        free(message);
877 878 879 880
        xmlFreeDoc(doc);
        return;
    } while (0);

881 882
    config_release_config();
    auth_release(auth);
883
    client_send_error_by_id(client, error_id);
884 885
}

Marvin Scholz's avatar
Marvin Scholz committed
886 887
static void command_kill_source(client_t *client,
                                source_t *source,
888
                                admin_format_t response)
889
{
890 891 892
    xmlDocPtr doc;
    xmlNodePtr node;

893 894
    doc = xmlNewDoc (