admin.c 44.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 27
#include "admin.h"
#include "compat.h"
28
#include "cfgfile.h"
29 30 31 32 33 34
#include "connection.h"
#include "refbuf.h"
#include "client.h"
#include "source.h"
#include "global.h"
#include "stats.h"
35
#include "xslt.h"
36
#include "fserve.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
/* Helper macros */
#define COMMAND_REQUIRE(client,name,var)                                \
    do {                                                                \
55
        (var) = httpp_get_param((client)->parser, (name));        \
56
        if((var) == NULL) {                                             \
57
            client_send_error_by_id(client, ICECAST_ERROR_ADMIN_MISSING_PARAMETER); \
58 59 60 61 62
            return;                                                     \
        }                                                               \
    } while(0);

#define COMMAND_OPTIONAL(client,name,var) \
63
(var) = httpp_get_param((client)->parser, (name))
64

Marvin Scholz's avatar
Marvin Scholz committed
65
#define FALLBACK_RAW_REQUEST                "fallbacks"
66
#define FALLBACK_HTML_REQUEST               "fallbacks.xsl"
Marvin Scholz's avatar
Marvin Scholz committed
67 68
#define SHOUTCAST_METADATA_REQUEST          "admin.cgi"
#define METADATA_RAW_REQUEST                "metadata"
69
#define METADATA_HTML_REQUEST               "metadata.xsl"
Marvin Scholz's avatar
Marvin Scholz committed
70
#define LISTCLIENTS_RAW_REQUEST             "listclients"
71
#define LISTCLIENTS_HTML_REQUEST            "listclients.xsl"
Marvin Scholz's avatar
Marvin Scholz committed
72
#define STATS_RAW_REQUEST                   "stats"
73
#define STATS_HTML_REQUEST                  "stats.xsl"
Marvin Scholz's avatar
Marvin Scholz committed
74
#define QUEUE_RELOAD_RAW_REQUEST            "reloadconfig"
75
#define QUEUE_RELOAD_HTML_REQUEST           "reloadconfig.xsl"
Marvin Scholz's avatar
Marvin Scholz committed
76
#define LISTMOUNTS_RAW_REQUEST              "listmounts"
77
#define LISTMOUNTS_HTML_REQUEST             "listmounts.xsl"
Marvin Scholz's avatar
Marvin Scholz committed
78
#define STREAMLIST_RAW_REQUEST              "streamlist"
79
#define STREAMLIST_HTML_REQUEST             "streamlist.xsl"
Marvin Scholz's avatar
Marvin Scholz committed
80 81
#define STREAMLIST_PLAINTEXT_REQUEST        "streamlist.txt"
#define MOVECLIENTS_RAW_REQUEST             "moveclients"
82
#define MOVECLIENTS_HTML_REQUEST            "moveclients.xsl"
Marvin Scholz's avatar
Marvin Scholz committed
83
#define KILLCLIENT_RAW_REQUEST              "killclient"
84
#define KILLCLIENT_HTML_REQUEST             "killclient.xsl"
Marvin Scholz's avatar
Marvin Scholz committed
85
#define KILLSOURCE_RAW_REQUEST              "killsource"
86
#define KILLSOURCE_HTML_REQUEST             "killsource.xsl"
Marvin Scholz's avatar
Marvin Scholz committed
87 88
#define ADMIN_XSL_RESPONSE                  "response.xsl"
#define MANAGEAUTH_RAW_REQUEST              "manageauth"
89
#define MANAGEAUTH_HTML_REQUEST             "manageauth.xsl"
Marvin Scholz's avatar
Marvin Scholz committed
90
#define UPDATEMETADATA_RAW_REQUEST          "updatemetadata"
91
#define UPDATEMETADATA_HTML_REQUEST         "updatemetadata.xsl"
Marvin Scholz's avatar
Marvin Scholz committed
92
#define DEFAULT_RAW_REQUEST                 ""
93
#define DEFAULT_HTML_REQUEST                ""
Marvin Scholz's avatar
Marvin Scholz committed
94
#define BUILDM3U_RAW_REQUEST                "buildm3u"
95

96 97 98 99 100 101
typedef struct {
    const char *prefix;
    size_t length;
    const admin_command_handler_t *handlers;
} admin_command_table_t;

102 103 104 105 106 107 108 109 110 111 112 113 114
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
115 116

static const admin_command_handler_t handlers[] = {
117 118 119 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
    { "*",                                  ADMINTYPE_GENERAL,      ADMIN_FORMAT_HTML,          NULL, NULL}, /* for ACL framework */
    { FALLBACK_RAW_REQUEST,                 ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,           command_fallback, NULL},
    { FALLBACK_HTML_REQUEST,                ADMINTYPE_MOUNT,        ADMIN_FORMAT_HTML,          command_fallback, NULL},
    { METADATA_RAW_REQUEST,                 ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,           command_metadata, NULL},
    { METADATA_HTML_REQUEST,                ADMINTYPE_MOUNT,        ADMIN_FORMAT_HTML,          command_metadata, NULL},
    { SHOUTCAST_METADATA_REQUEST,           ADMINTYPE_MOUNT,        ADMIN_FORMAT_HTML,          command_shoutcast_metadata, NULL},
    { LISTCLIENTS_RAW_REQUEST,              ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,           command_show_listeners, NULL},
    { LISTCLIENTS_HTML_REQUEST,             ADMINTYPE_MOUNT,        ADMIN_FORMAT_HTML,          command_show_listeners, NULL},
    { STATS_RAW_REQUEST,                    ADMINTYPE_HYBRID,       ADMIN_FORMAT_RAW,           command_stats, NULL},
    { STATS_HTML_REQUEST,                   ADMINTYPE_HYBRID,       ADMIN_FORMAT_HTML,          command_stats, NULL},
    { "stats.xml",                          ADMINTYPE_HYBRID,       ADMIN_FORMAT_RAW,           command_stats, NULL},
    { QUEUE_RELOAD_RAW_REQUEST,             ADMINTYPE_GENERAL,      ADMIN_FORMAT_RAW,           command_queue_reload, NULL},
    { QUEUE_RELOAD_HTML_REQUEST,            ADMINTYPE_GENERAL,      ADMIN_FORMAT_HTML,          command_queue_reload, NULL},
    { LISTMOUNTS_RAW_REQUEST,               ADMINTYPE_GENERAL,      ADMIN_FORMAT_RAW,           command_list_mounts, NULL},
    { LISTMOUNTS_HTML_REQUEST,              ADMINTYPE_GENERAL,      ADMIN_FORMAT_HTML,          command_list_mounts, NULL},
    { STREAMLIST_RAW_REQUEST,               ADMINTYPE_GENERAL,      ADMIN_FORMAT_RAW,           command_list_mounts, NULL},
    { STREAMLIST_PLAINTEXT_REQUEST,         ADMINTYPE_GENERAL,      ADMIN_FORMAT_PLAINTEXT,     command_list_mounts, NULL},
    { STREAMLIST_HTML_REQUEST,              ADMINTYPE_GENERAL,      ADMIN_FORMAT_HTML,          command_list_mounts, NULL},
    { MOVECLIENTS_RAW_REQUEST,              ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,           command_move_clients, NULL},
    { MOVECLIENTS_HTML_REQUEST,             ADMINTYPE_HYBRID,       ADMIN_FORMAT_HTML,          command_move_clients, NULL},
    { KILLCLIENT_RAW_REQUEST,               ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,           command_kill_client, NULL},
    { KILLCLIENT_HTML_REQUEST,              ADMINTYPE_MOUNT,        ADMIN_FORMAT_HTML,          command_kill_client, NULL},
    { KILLSOURCE_RAW_REQUEST,               ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,           command_kill_source, NULL},
    { KILLSOURCE_HTML_REQUEST,              ADMINTYPE_MOUNT,        ADMIN_FORMAT_HTML,          command_kill_source, NULL},
    { MANAGEAUTH_RAW_REQUEST,               ADMINTYPE_GENERAL,      ADMIN_FORMAT_RAW,           command_manageauth, NULL},
    { MANAGEAUTH_HTML_REQUEST,              ADMINTYPE_GENERAL,      ADMIN_FORMAT_HTML,          command_manageauth, NULL},
    { UPDATEMETADATA_RAW_REQUEST,           ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,           command_updatemetadata, NULL},
    { UPDATEMETADATA_HTML_REQUEST,          ADMINTYPE_MOUNT,        ADMIN_FORMAT_HTML,          command_updatemetadata, NULL},
    { BUILDM3U_RAW_REQUEST,                 ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,           command_buildm3u, NULL},
    { DEFAULT_HTML_REQUEST,                 ADMINTYPE_HYBRID,       ADMIN_FORMAT_HTML,          command_stats, NULL},
    { DEFAULT_RAW_REQUEST,                  ADMINTYPE_HYBRID,       ADMIN_FORMAT_HTML,          command_stats, NULL}
Philipp Schafft's avatar
Philipp Schafft committed
148 149
};

150
static admin_command_table_t command_tables[ADMIN_MAX_COMMAND_TABLES] = {
151 152 153
    {.prefix = NULL, .length = (sizeof(handlers)/sizeof(*handlers)), .handlers = handlers},
};

154 155 156 157 158 159 160 161 162 163 164
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;
}

165 166 167 168 169 170 171
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;

172 173 174
    if (!__is_command_table_valid(&(command_tables[t])))
        return NULL;

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

        return NULL;
    }

    len = end - command;

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

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

218 219 220
    if (!__is_command_table_valid(table))
        return ADMIN_COMMAND_ERROR;

221 222 223 224 225 226 227
    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
228

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

235
    if (table == NULL)
236
        return ADMIN_COMMAND_ERROR;
237 238 239 240 241 242 243 244 245

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

    for (i = 0; i < table->length; i++)
246
        if (resourcematch_match(table->handlers[i].route, suffix, NULL) == RESOURCEMATCH_MATCH)
247
            return admin_get_command_by_table_and_index(table, i);
Philipp Schafft's avatar
Philipp Schafft committed
248

249
    return ADMIN_COMMAND_ERROR;
Philipp Schafft's avatar
Philipp Schafft committed
250 251
}

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

259 260 261 262 263 264 265
    if (table == NULL)
        return NULL;

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

    return &(table->handlers[index]);
Marvin Scholz's avatar
Marvin Scholz committed
266 267 268 269 270
}

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

Marvin Scholz's avatar
Marvin Scholz committed
275 276
    if (handler != NULL)
        return handler->type;
Philipp Schafft's avatar
Philipp Schafft committed
277 278

    return ADMINTYPE_ERROR;
279 280
}

281 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
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;
}

316 317 318 319 320 321 322 323 324 325 326
/* build an XML root node including some common tags */
xmlNodePtr admin_build_rootnode(xmlDocPtr doc, const char *name)
{
    xmlNodePtr rootnode = xmlNewDocNode(doc, NULL, XMLSTR(name), NULL);
    xmlNodePtr modules = module_container_get_modulelist_as_xml(global.modulecontainer);

    xmlAddChild(rootnode, modules);

    return rootnode;
}

327 328 329
/* 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
330
xmlDocPtr admin_build_sourcelist(const char *mount)
331 332 333 334 335 336 337 338
{
    avl_node *node;
    source_t *source;
    xmlNodePtr xmlnode, srcnode;
    xmlDocPtr doc;
    char buf[22];
    time_t now = time(NULL);

339
    doc = xmlNewDoc (XMLSTR("1.0"));
340
    xmlnode = admin_build_rootnode(doc, "icestats");
341
    xmlDocSetRootElement(doc, xmlnode);
342

343
    if (mount) {
344
        xmlNewTextChild (xmlnode, NULL, XMLSTR("current_source"), XMLSTR(mount));
345 346 347 348 349
    }

    node = avl_get_first(global.source_tree);
    while(node) {
        source = (source_t *)node->key;
350 351 352 353 354 355
        if (mount && strcmp (mount, source->mount) == 0)
        {
            node = avl_get_next (node);
            continue;
        }

356
        if (source->running || source->on_demand)
357
        {
358 359
            ice_config_t *config;
            mount_proxy *mountinfo;
Philipp Schafft's avatar
Philipp Schafft committed
360
            acl_t *acl = NULL;
361

362 363
            srcnode = xmlNewChild(xmlnode, NULL, XMLSTR("source"), NULL);
            xmlSetProp(srcnode, XMLSTR("mount"), XMLSTR(source->mount));
364

365
            xmlNewTextChild(srcnode, NULL, XMLSTR("fallback"),
366
                    (source->fallback_mount != NULL)?
367
                    XMLSTR(source->fallback_mount):XMLSTR(""));
368
            snprintf(buf, sizeof(buf), "%lu", source->listeners);
369
            xmlNewTextChild(srcnode, NULL, XMLSTR("listeners"), XMLSTR(buf));
370

Karl Heyes's avatar
Karl Heyes committed
371
            config = config_get_config();
Marvin Scholz's avatar
Marvin Scholz committed
372
            mountinfo = config_find_mount(config, source->mount, MOUNT_TYPE_NORMAL);
Philipp Schafft's avatar
Philipp Schafft committed
373
            if (mountinfo)
374
                acl = auth_stack_get_anonymous_acl(mountinfo->authstack, httpp_req_get);
Philipp Schafft's avatar
Philipp Schafft committed
375
            if (!acl)
376
                acl = auth_stack_get_anonymous_acl(config->authstack, httpp_req_get);
Philipp Schafft's avatar
Philipp Schafft committed
377
            if (acl && acl_test_web(acl) == ACL_POLICY_DENY) {
378
                xmlNewTextChild(srcnode, NULL, XMLSTR("authenticator"), XMLSTR("(dummy)"));
379
            }
Philipp Schafft's avatar
Philipp Schafft committed
380
            acl_release(acl);
381 382
            config_release_config();

Marvin Scholz's avatar
Marvin Scholz committed
383 384 385 386
            if (source->running) {
                if (source->client) {
                    snprintf(buf, sizeof(buf), "%lu",
                        (unsigned long)(now - source->con->con_time));
387
                    xmlNewTextChild(srcnode, NULL, XMLSTR("Connected"), XMLSTR(buf));
Karl Heyes's avatar
Karl Heyes committed
388
                }
389
                xmlNewTextChild(srcnode, NULL, XMLSTR("content-type"),
Marvin Scholz's avatar
Marvin Scholz committed
390
                    XMLSTR(source->format->contenttype));
391
            }
392
        }
393 394 395 396 397
        node = avl_get_next(node);
    }
    return(doc);
}

398 399 400 401
void admin_send_response(xmlDocPtr       doc,
                         client_t       *client,
                         admin_format_t  response,
                         const char     *xslt_template)
402
{
403
    if (response == ADMIN_FORMAT_RAW) {
404 405
        xmlChar *buff = NULL;
        int len = 0;
406 407 408
        size_t buf_len;
        ssize_t ret;

409
        xmlDocDumpMemory(doc, &buff, &len);
410 411 412 413

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

415 416
        client_set_queue(client, NULL);
        client->refbuf = refbuf_new(buf_len);
417

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

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

        client->refbuf->len = ret;
457 458 459
        xmlFree(buff);
        client->respcode = 200;
        fserve_add_client (client, NULL);
460
    }
461
    if (response == ADMIN_FORMAT_HTML) {
462
        char *fullpath_xslt_template;
463
        size_t fullpath_xslt_template_len;
464 465
        ice_config_t *config = config_get_config();

466
        fullpath_xslt_template_len = strlen(config->adminroot_dir) + strlen(xslt_template) + strlen(PATH_SEPARATOR) + 1;
467 468
        fullpath_xslt_template = malloc(fullpath_xslt_template_len);
        snprintf(fullpath_xslt_template, fullpath_xslt_template_len, "%s%s%s",
469
            config->adminroot_dir, PATH_SEPARATOR, xslt_template);
470
        config_release_config();
471

472
        ICECAST_LOG_DEBUG("Sending XSLT (%s)", fullpath_xslt_template);
473
        xslt_transform(doc, fullpath_xslt_template, client, 200, NULL);
474 475 476
        free(fullpath_xslt_template);
    }
}
477

478
void admin_handle_request(client_t *client, const char *uri)
479
{
Marvin Scholz's avatar
Marvin Scholz committed
480 481 482
    const char *mount;
    const admin_command_handler_t* handler;
    source_t *source = NULL;
483
    admin_format_t format;
484

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

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

Marvin Scholz's avatar
Marvin Scholz committed
489
    /* Check if admin command is valid */
490
    if (handler == NULL || (handler->function == NULL && handler->function_with_parameters == NULL)) {
Marvin Scholz's avatar
Marvin Scholz committed
491 492
        ICECAST_LOG_ERROR("Error parsing command string or unrecognised command: %H",
                uri);
493
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_UNRECOGNISED_COMMAND);
494 495 496
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
497
    /* Check ACL */
Philipp Schafft's avatar
Philipp Schafft committed
498
    if (acl_test_admin(client->acl, client->admin_command) != ACL_POLICY_ALLOW) {
Marvin Scholz's avatar
Marvin Scholz committed
499 500

        /* ACL disallows, check exceptions */
501
        if ((handler->function == command_metadata && handler->format == ADMIN_FORMAT_RAW) &&
Philipp Schafft's avatar
Philipp Schafft committed
502 503
            (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
504 505
            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
506
        } else {
507
            ICECAST_LOG_DEBUG("Client needs to authenticate.");
508
            client_send_error_by_id(client, ICECAST_ERROR_GEN_CLIENT_NEEDS_TO_AUTHENTICATE);
509 510
            return;
        }
511 512
    }

513
    COMMAND_OPTIONAL(client, "mount", mount);
514

Marvin Scholz's avatar
Marvin Scholz committed
515
    /* Find mountpoint source */
516
    if(mount != NULL) {
517

Philipp Schafft's avatar
Philipp Schafft committed
518
        /* This is a mount request, handle it as such */
519
        avl_tree_rlock(global.source_tree);
Michael Smith's avatar
Michael Smith committed
520
        source = source_find_mount_raw(mount);
521

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

Marvin Scholz's avatar
Marvin Scholz committed
541
    if (handler->type == ADMINTYPE_MOUNT && !source) {
542
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_MISSING_PARAMETER);
Marvin Scholz's avatar
Marvin Scholz committed
543
        return;
544 545
    }

546 547 548 549 550 551
    if (handler->format == ADMIN_FORMAT_AUTO) {
        format = client_get_admin_format_by_content_negotiation(client);
    } else {
        format = handler->format;
    }

552 553
    switch (client->parser->req_type) {
        case httpp_req_get:
554
        case httpp_req_post:
555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573
            if (handler->function) {
                handler->function(client, source, format);
            } else {
                resourcematch_extract_t *extract = NULL;
                const char *suffix = strchr(uri, '/');

                if (!suffix) {
                    client_send_error_by_id(client, ICECAST_ERROR_ADMIN_UNRECOGNISED_COMMAND);
                } else {
                    suffix++;

                    if (resourcematch_match(handler->route, suffix, &extract) == RESOURCEMATCH_MATCH) {
                        handler->function_with_parameters(client, source, format, extract);
                        resourcematch_extract_free(extract);
                    } else {
                        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_UNRECOGNISED_COMMAND);
                    }
                }
            }
574 575 576 577 578 579 580 581 582 583
        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
584 585
    if (source) {
        avl_tree_unlock(global.source_tree);
586
    }
Marvin Scholz's avatar
Marvin Scholz committed
587
    return;
588 589
}

590
static void html_success(client_t *client, char *message)
591
{
592 593
    ssize_t ret;

Marvin Scholz's avatar
Marvin Scholz committed
594 595
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
596
                                 "text/html", "utf-8",
597
                                 "", NULL, client);
598 599 600

    if (ret == -1 || ret >= PER_CLIENT_REFBUF_SIZE) {
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
601
        client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
602 603 604
        return;
    }

605
    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
606 607
        "<html><head><title>Admin request successful</title></head>"
        "<body><p>%s</p></body></html>", message);
608

609
    client->respcode = 200;
Marvin Scholz's avatar
Marvin Scholz committed
610 611
    client->refbuf->len = strlen(client->refbuf->data);
    fserve_add_client(client, NULL);
612 613
}

614

Marvin Scholz's avatar
Marvin Scholz committed
615 616
static void command_move_clients(client_t   *client,
                                 source_t   *source,
617
                                 admin_format_t response)
618
{
619
    const char *dest_source;
620
    source_t *dest;
621 622 623 624 625
    xmlDocPtr doc;
    xmlNodePtr node;
    char buf[255];
    int parameters_passed = 0;

626
    ICECAST_LOG_DEBUG("Doing optional check");
627
    if((COMMAND_OPTIONAL(client, "destination", dest_source))) {
628 629
        parameters_passed = 1;
    }
630
    ICECAST_LOG_DEBUG("Done optional check (%d)", parameters_passed);
631 632
    if (!parameters_passed) {
        doc = admin_build_sourcelist(source->mount);
633
        admin_send_response(doc, client, response,
634
             MOVECLIENTS_HTML_REQUEST);
635 636 637
        xmlFreeDoc(doc);
        return;
    }
638

Marvin Scholz's avatar
Marvin Scholz committed
639
    dest = source_find_mount(dest_source);
640

Marvin Scholz's avatar
Marvin Scholz committed
641
    if (dest == NULL) {
642
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_NO_SUCH_DESTINATION);
643 644 645
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
646
    if (strcmp(dest->mount, source->mount) == 0) {
647
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SUPPLIED_MOUNTPOINTS_ARE_IDENTICAL);
648 649 650
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
651
    if (dest->running == 0 && dest->on_demand == 0) {
652
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_DEST_NOT_RUNNING);
653 654 655
        return;
    }

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

Marvin Scholz's avatar
Marvin Scholz committed
658
    doc = xmlNewDoc(XMLSTR("1.0"));
659
    node = admin_build_rootnode(doc, "iceresponse");
660 661
    xmlDocSetRootElement(doc, node);

Marvin Scholz's avatar
Marvin Scholz committed
662
    source_move_clients(source, dest);
663

Marvin Scholz's avatar
Marvin Scholz committed
664
    snprintf(buf, sizeof(buf), "Clients moved from %s to %s",
665
        source->mount, dest_source);
666 667
    xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
    xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
668

Marvin Scholz's avatar
Marvin Scholz committed
669
    admin_send_response(doc, client, response, ADMIN_XSL_RESPONSE);
670
    xmlFreeDoc(doc);
671 672
}

Marvin Scholz's avatar
Marvin Scholz committed
673 674 675 676 677
static inline xmlNodePtr __add_listener(client_t        *client,
                                        xmlNodePtr      parent,
                                        time_t          now,
                                        operation_mode  mode)
{
678 679 680 681
    const char *tmp;
    xmlNodePtr node;
    char buf[22];

Philipp Schafft's avatar
Philipp Schafft committed
682
    /* TODO: kh has support for a child node "lag". We should add that.
683 684
     * 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
685
     */
686 687 688 689 690 691 692

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

696
    xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "IP" : "ip"), XMLSTR(client->con->ip));
697 698 699

    tmp = httpp_getvar(client->parser, "user-agent");
    if (tmp)
700
        xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "UserAgent" : "useragent"), XMLSTR(tmp));
701 702 703

    tmp = httpp_getvar(client->parser, "referer");
    if (tmp)
704
        xmlNewTextChild(node, NULL, XMLSTR("referer"), XMLSTR(tmp));
705 706

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

    if (client->username)
710
        xmlNewTextChild(node, NULL, XMLSTR("username"), XMLSTR(client->username));
711

Philipp Schafft's avatar
Philipp Schafft committed
712
    if (client->role)
713
        xmlNewTextChild(node, NULL, XMLSTR("role"), XMLSTR(client->role));
Philipp Schafft's avatar
Philipp Schafft committed
714

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

717 718 719 720 721 722 723 724 725
    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;
    }

726 727 728
    return node;
}

Marvin Scholz's avatar
Marvin Scholz committed
729 730 731 732
void admin_add_listeners_to_mount(source_t          *source,
                                  xmlNodePtr        parent,
                                  operation_mode    mode)
{
733 734 735 736 737 738
    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) {
739
        __add_listener((client_t *)client_node->key, parent, now, mode);
740 741 742 743 744
        client_node = avl_get_next(client_node);
    }
    avl_tree_unlock(source->client_tree);
}

Marvin Scholz's avatar
Marvin Scholz committed
745 746
static void command_show_listeners(client_t *client,
                                   source_t *source,
747
                                   admin_format_t response)
748
{
749
    xmlDocPtr doc;
750
    xmlNodePtr node, srcnode;
751
    char buf[22];
752

753
    doc = xmlNewDoc(XMLSTR("1.0"));
754
    node = admin_build_rootnode(doc, "icestats");
755 756
    srcnode = xmlNewChild(node, NULL, XMLSTR("source"), NULL);
    xmlSetProp(srcnode, XMLSTR("mount"), XMLSTR(source->mount));
757
    xmlDocSetRootElement(doc, node);
758

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

764
    admin_add_listeners_to_mount(source, srcnode, client->mode);
765

766
    admin_send_response(doc, client, response,
767
        LISTCLIENTS_HTML_REQUEST);
768
    xmlFreeDoc(doc);
769 770
}

771
static void command_buildm3u(client_t *client, source_t *source, admin_format_t format)
772
{
Marvin Scholz's avatar
Marvin Scholz committed
773
    const char *mount = source->mount;
774 775
    const char *username = NULL;
    const char *password = NULL;
776
    ssize_t ret;
777 778 779 780

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

Marvin Scholz's avatar
Marvin Scholz committed
781 782
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
783
                                 "audio/x-mpegurl", NULL,
784
                                 NULL, NULL, client);
785

Marvin Scholz's avatar
Marvin Scholz committed
786 787
    if (ret == -1 || ret >= (PER_CLIENT_REFBUF_SIZE - 512)) {
        /* we want at least 512 Byte left for data */
788
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
789
        client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
790 791 792 793
        return;
    }


794
    client_get_baseurl(client, NULL, client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret, username, password, "Content-Disposition: attachment; filename=listen.m3u\r\n\r\n", mount, "\r\n");
795

796
    client->respcode = 200;
797 798
    client->refbuf->len = strlen (client->refbuf->data);
    fserve_add_client (client, NULL);
799
}
800

Marvin Scholz's avatar
Marvin Scholz committed
801 802
xmlNodePtr admin_add_role_to_authentication(auth_t *auth, xmlNodePtr parent)
{
803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821
    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;
}
822

823
static void command_manageauth(client_t *client, source_t *source, admin_format_t response)
Marvin Scholz's avatar
Marvin Scholz committed
824
{
825
    xmlDocPtr doc;
826
    xmlNodePtr node, rolenode, usersnode, msgnode;
827 828
    const char *action = NULL;
    const char *username = NULL;
829
    const char *idstring = NULL;
830 831
    char *message = NULL;
    int ret = AUTH_OK;
832
    int error_id = ICECAST_ERROR_ADMIN_missing_parameter;
833 834
    long unsigned int id;
    ice_config_t *config = config_get_config();
Philipp Schafft's avatar
Philipp Schafft committed
835
    auth_t *auth;
836

Marvin Scholz's avatar
Marvin Scholz committed
837
    do {
838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858
        /* 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);
859
            error_id = ICECAST_ERROR_ADMIN_ROLEMGN_ROLE_NOT_FOUND;
860
            break;
861
        }
Philipp Schafft's avatar
Philipp Schafft committed
862

863
        COMMAND_OPTIONAL(client, "action", action);
864
        COMMAND_OPTIONAL(client, "username", username);
865 866

        if (action == NULL)
867
            action = "list";
868

Marvin Scholz's avatar
Marvin Scholz committed
869
        if (!strcmp(action, "add")) {
870
            const char *password = NULL;
871
            COMMAND_OPTIONAL(client, "password", password);
872

873 874
            if (username == NULL || password == NULL) {
                ICECAST_LOG_WARN("manage auth request add for %lu but no user/pass", id);
875 876
                break;
            }
877 878

            if (!auth->adduser) {
879
                error_id = ICECAST_ERROR_ADMIN_ROLEMGN_ADD_NOSYS;
880 881 882
                break;
            }

Philipp Schafft's avatar
Philipp Schafft committed
883
            ret = auth->adduser(auth, username, password);
884 885
            if (ret == AUTH_FAILED) {
                message = strdup("User add failed - check the icecast error log");
886
            } else if (ret == AUTH_USERADDED) {
887
                message = strdup("User added");
888
            } else if (ret == AUTH_USEREXISTS) {
889 890 891
                message = strdup("User already exists - not added");
            }
        }
Marvin Scholz's avatar
Marvin Scholz committed
892
        if (!strcmp(action, "delete")) {
893 894 895 896 897 898
            if (username == NULL) {
                ICECAST_LOG_WARN("manage auth request delete for %lu but no username", id);
                break;
            }

            if (!auth->deleteuser) {
899
                error_id = ICECAST_ERROR_ADMIN_ROLEMGN_DELETE_NOSYS;
900 901
                break;
            }
902

Philipp Schafft's avatar
Philipp Schafft committed
903
            ret = auth->deleteuser(auth, username);
904 905
            if (ret == AUTH_FAILED) {
                message = strdup("User delete failed - check the icecast error log");
906
            } else if (ret == AUTH_USERDELETED) {
907 908 909 910
                message = strdup("User deleted");
            }
        }

911
        doc = xmlNewDoc(XMLSTR("1.0"));
912
        node = admin_build_rootnode(doc, "icestats");
913

914
        rolenode = admin_add_role_to_authentication(auth, node);
915

916
        if (message) {
917
            msgnode = admin_build_rootnode(doc, "iceresponse");
918
            xmlNewTextChild(msgnode, NULL, XMLSTR("message"), XMLSTR(message));
919
        }
920

921
        xmlDocSetRootElement(doc, node);
922

923 924 925 926
        if (auth && auth->listuser) {
            usersnode = xmlNewChild(rolenode, NULL, XMLSTR("users"), NULL);
            auth->listuser(auth, usersnode);
        }
927

928 929
        config_release_config();
        auth_release(auth);
930

931
        admin_send_response(doc, client, response,
932
            MANAGEAUTH_HTML_REQUEST);
Marvin Scholz's avatar
Marvin Scholz committed
933
        free(message);
934 935 936 937
        xmlFreeDoc(doc);
        return;
    } while (0);

938 939
    config_release_config();
    auth_release(auth);
940
    client_send_error_by_id(client, error_id);
941 942
}

Marvin Scholz's avatar
Marvin Scholz committed
943 944
static void command_kill_source(client_t *client,
                                source_t *source,
945
                                admin_format_t response)
946
{
947 948 949
    xmlDocPtr doc;
    xmlNodePtr node;

950
    doc = xmlNewDoc (XMLSTR("1.0"));
951
    node = admin_build_rootnode(doc, "iceresponse");
952 953
    xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR("Source Removed"));
    xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
954 955
    xmlDocSetRootElement(doc, node);

956 957
    source->running = 0;

958
    admin_send_response(doc, client, response,
959 960
        ADMIN_XSL_RESPONSE);
    xmlFreeDoc(doc);
961 962
}

Marvin Scholz's avatar
Marvin Scholz committed
963 964
static void command_kill_client(client_t *client,
                                source_t *source,
965
                                admin_format_t response)
966
{
967
    const char *idtext;
968 969
    int id;
    client_t *listener;