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

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

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

26 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

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

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

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

106 107 108 109 110 111 112 113 114 115 116 117 118
static void command_fallback            (client_t *client, source_t *source, admin_format_t response);
static void command_metadata            (client_t *client, source_t *source, admin_format_t response);
static void command_shoutcast_metadata  (client_t *client, source_t *source, admin_format_t response);
static void command_show_listeners      (client_t *client, source_t *source, admin_format_t response);
static void command_stats               (client_t *client, source_t *source, admin_format_t response);
static void command_queue_reload        (client_t *client, source_t *source, admin_format_t response);
static void command_list_mounts         (client_t *client, source_t *source, admin_format_t response);
static void command_move_clients        (client_t *client, source_t *source, admin_format_t response);
static void command_kill_client         (client_t *client, source_t *source, admin_format_t response);
static void command_kill_source         (client_t *client, source_t *source, admin_format_t response);
static void command_manageauth          (client_t *client, source_t *source, admin_format_t response);
static void command_updatemetadata      (client_t *client, source_t *source, admin_format_t response);
static void command_buildm3u            (client_t *client, source_t *source, admin_format_t response);
Marvin Scholz's avatar
Marvin Scholz committed
119 120

static const admin_command_handler_t handlers[] = {
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
    { "*",                                  ADMINTYPE_GENERAL,      ADMIN_FORMAT_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
152 153
};

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

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

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

    return 1;
}

169 170 171 172 173 174 175
static inline const admin_command_table_t * admin_get_table(admin_command_id_t command)
{
    size_t t = (command & 0x00FF0000) >> 16;

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

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

179 180 181 182 183 184 185 186 187 188 189 190 191
    return &(command_tables[t]);
}

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

    end = strchr(command, '/');

    if (end == NULL) {
        for (i = 0; i < (sizeof(command_tables)/sizeof(*command_tables)); i++)
192
            if (command_tables[i].prefix == NULL && __is_command_table_valid(&(command_tables[i])))
193 194 195 196 197 198 199 200
                return &(command_tables[i]);

        return NULL;
    }

    len = end - command;

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

204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
        if (command_tables[i].prefix != NULL && strlen(command_tables[i].prefix) == len && strncmp(command_tables[i].prefix, command, len) == 0) {
            return &(command_tables[i]);
        }
    }

    return NULL;
}

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

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

    if (index > 0x0FFFF)
        return ADMIN_COMMAND_ERROR;

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

225 226 227 228 229 230 231
    return (t << 16) | index;
}

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

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

239 240 241 242 243 244 245 246 247 248 249
    if (table == NULL)
        return COMMAND_ERROR;

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

    for (i = 0; i < table->length; i++)
250
        if (resourcematch_match(table->handlers[i].route, suffix, NULL) == RESOURCEMATCH_MATCH)
251
            return admin_get_command_by_table_and_index(table, i);
Philipp Schafft's avatar
Philipp Schafft committed
252 253 254 255

    return COMMAND_ERROR;
}

Marvin Scholz's avatar
Marvin Scholz committed
256 257
/* Get the command handler for command or NULL
 */
258
const admin_command_handler_t* admin_get_handler(admin_command_id_t command)
Marvin Scholz's avatar
Marvin Scholz committed
259
{
260 261
    const admin_command_table_t *table = admin_get_table(command);
    size_t index = admin_get_index_by_command(command);
Philipp Schafft's avatar
Philipp Schafft committed
262

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

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

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

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

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

    return ADMINTYPE_ERROR;
283 284
}

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

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

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

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

        return 0;
    }

    return -1;
}

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

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

    return -1;
}

320 321 322 323 324 325 326 327 328 329 330
/* 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;
}

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

343
    doc = xmlNewDoc (XMLSTR("1.0"));
344
    xmlnode = admin_build_rootnode(doc, "icestats");
345
    xmlDocSetRootElement(doc, xmlnode);
346

347
    if (mount) {
348
        xmlNewTextChild (xmlnode, NULL, XMLSTR("current_source"), XMLSTR(mount));
349 350 351 352 353
    }

    node = avl_get_first(global.source_tree);
    while(node) {
        source = (source_t *)node->key;
354 355 356 357 358 359
        if (mount && strcmp (mount, source->mount) == 0)
        {
            node = avl_get_next (node);
            continue;
        }

360
        if (source->running || source->on_demand)
361
        {
362 363
            ice_config_t *config;
            mount_proxy *mountinfo;
Philipp Schafft's avatar
Philipp Schafft committed
364
            acl_t *acl = NULL;
365

366 367
            srcnode = xmlNewChild(xmlnode, NULL, XMLSTR("source"), NULL);
            xmlSetProp(srcnode, XMLSTR("mount"), XMLSTR(source->mount));
368

369
            xmlNewTextChild(srcnode, NULL, XMLSTR("fallback"),
370
                    (source->fallback_mount != NULL)?
371
                    XMLSTR(source->fallback_mount):XMLSTR(""));
372
            snprintf(buf, sizeof(buf), "%lu", source->listeners);
373
            xmlNewTextChild(srcnode, NULL, XMLSTR("listeners"), XMLSTR(buf));
374

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

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

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

413
        xmlDocDumpMemory(doc, &buff, &len);
414 415 416 417

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

419 420
        client_set_queue(client, NULL);
        client->refbuf = refbuf_new(buf_len);
421

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

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

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

470
        fullpath_xslt_template_len = strlen(config->adminroot_dir) + strlen(xslt_template) + strlen(PATH_SEPARATOR) + 1;
471 472
        fullpath_xslt_template = malloc(fullpath_xslt_template_len);
        snprintf(fullpath_xslt_template, fullpath_xslt_template_len, "%s%s%s",
473
            config->adminroot_dir, PATH_SEPARATOR, xslt_template);
474
        config_release_config();
475

476
        ICECAST_LOG_DEBUG("Sending XSLT (%s)", fullpath_xslt_template);
477
        xslt_transform(doc, fullpath_xslt_template, client, 200);
478 479 480
        free(fullpath_xslt_template);
    }
}
481

482
void admin_handle_request(client_t *client, const char *uri)
483
{
Marvin Scholz's avatar
Marvin Scholz committed
484 485 486
    const char *mount;
    const admin_command_handler_t* handler;
    source_t *source = NULL;
487
    admin_format_t format;
488

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

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

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

Marvin Scholz's avatar
Marvin Scholz committed
501
    /* Check ACL */
Philipp Schafft's avatar
Philipp Schafft committed
502
    if (acl_test_admin(client->acl, client->admin_command) != ACL_POLICY_ALLOW) {
Marvin Scholz's avatar
Marvin Scholz committed
503 504

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

517
    COMMAND_OPTIONAL(client, "mount", mount);
518

Marvin Scholz's avatar
Marvin Scholz committed
519
    /* Find mountpoint source */
520
    if(mount != NULL) {
521

Philipp Schafft's avatar
Philipp Schafft committed
522
        /* This is a mount request, handle it as such */
523
        avl_tree_rlock(global.source_tree);
Michael Smith's avatar
Michael Smith committed
524
        source = source_find_mount_raw(mount);
525

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

Marvin Scholz's avatar
Marvin Scholz committed
545
    if (handler->type == ADMINTYPE_MOUNT && !source) {
546
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_MISSING_PARAMETER);
Marvin Scholz's avatar
Marvin Scholz committed
547
        return;
548 549
    }

550 551 552 553 554 555
    if (handler->format == ADMIN_FORMAT_AUTO) {
        format = client_get_admin_format_by_content_negotiation(client);
    } else {
        format = handler->format;
    }

556 557
    switch (client->parser->req_type) {
        case httpp_req_get:
558
        case httpp_req_post:
559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577
            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);
                    }
                }
            }
578 579 580 581 582 583 584 585 586 587
        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
588 589
    if (source) {
        avl_tree_unlock(global.source_tree);
590
    }
Marvin Scholz's avatar
Marvin Scholz committed
591
    return;
592 593
}

594
static void html_success(client_t *client, char *message)
595
{
596 597
    ssize_t ret;

Marvin Scholz's avatar
Marvin Scholz committed
598 599
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
600
                                 "text/html", "utf-8",
601
                                 "", NULL, client);
602 603 604

    if (ret == -1 || ret >= PER_CLIENT_REFBUF_SIZE) {
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
605
        client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
606 607 608
        return;
    }

609
    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
610 611
        "<html><head><title>Admin request successful</title></head>"
        "<body><p>%s</p></body></html>", message);
612

613
    client->respcode = 200;
Marvin Scholz's avatar
Marvin Scholz committed
614 615
    client->refbuf->len = strlen(client->refbuf->data);
    fserve_add_client(client, NULL);
616 617
}

618

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

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

Marvin Scholz's avatar
Marvin Scholz committed
643
    dest = source_find_mount(dest_source);
644

Marvin Scholz's avatar
Marvin Scholz committed
645
    if (dest == NULL) {
646
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_NO_SUCH_DESTINATION);
647 648 649
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
650
    if (strcmp(dest->mount, source->mount) == 0) {
651
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SUPPLIED_MOUNTPOINTS_ARE_IDENTICAL);
652 653 654
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
655
    if (dest->running == 0 && dest->on_demand == 0) {
656
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_DEST_NOT_RUNNING);
657 658 659
        return;
    }

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

Marvin Scholz's avatar
Marvin Scholz committed
662
    doc = xmlNewDoc(XMLSTR("1.0"));
663
    node = admin_build_rootnode(doc, "iceresponse");
664 665
    xmlDocSetRootElement(doc, node);

Marvin Scholz's avatar
Marvin Scholz committed
666
    source_move_clients(source, dest);
667

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

Marvin Scholz's avatar
Marvin Scholz committed
673
    admin_send_response(doc, client, response, ADMIN_XSL_RESPONSE);
674
    xmlFreeDoc(doc);
675 676
}

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

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

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

700
    xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "IP" : "ip"), XMLSTR(client->con->ip));
701 702 703

    tmp = httpp_getvar(client->parser, "user-agent");
    if (tmp)
704
        xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "UserAgent" : "useragent"), XMLSTR(tmp));
705 706 707

    tmp = httpp_getvar(client->parser, "referer");
    if (tmp)
708
        xmlNewTextChild(node, NULL, XMLSTR("referer"), XMLSTR(tmp));
709 710

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

    if (client->username)
714
        xmlNewTextChild(node, NULL, XMLSTR("username"), XMLSTR(client->username));
715

Philipp Schafft's avatar
Philipp Schafft committed
716
    if (client->role)
717
        xmlNewTextChild(node, NULL, XMLSTR("role"), XMLSTR(client->role));
Philipp Schafft's avatar
Philipp Schafft committed
718

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

721 722 723 724 725 726 727 728 729
    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;
    }

730 731 732
    return node;
}

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

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

757
    doc = xmlNewDoc(XMLSTR("1.0"));
758
    node = admin_build_rootnode(doc, "icestats");
759 760
    srcnode = xmlNewChild(node, NULL, XMLSTR("source"), NULL);
    xmlSetProp(srcnode, XMLSTR("mount"), XMLSTR(source->mount));
761
    xmlDocSetRootElement(doc, node);
762

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

768
    admin_add_listeners_to_mount(source, srcnode, client->mode);
769

770
    admin_send_response(doc, client, response,
771
        LISTCLIENTS_HTML_REQUEST);
772
    xmlFreeDoc(doc);
773 774
}

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

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

Marvin Scholz's avatar
Marvin Scholz committed
785 786
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
787
                                 "audio/x-mpegurl", NULL,
788
                                 NULL, NULL, client);
789

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


798
    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");
799

800
    client->respcode = 200;
Karl Heyes's avatar
Karl Heyes committed
801 802
    client->refbuf->len = strlen (client->refbuf->data);
    fserve_add_client (client, NULL);
803
}
804

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

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

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

867
        COMMAND_OPTIONAL(client, "action", action);
868
        COMMAND_OPTIONAL(client, "username", username);
869 870

        if (action == NULL)
871
            action = "list";
872

Marvin Scholz's avatar
Marvin Scholz committed
873
        if (!strcmp(action, "add")) {
874
            const char *password = NULL;
875
            COMMAND_OPTIONAL(client, "password", password);
876

877 878
            if (username == NULL || password == NULL) {
                ICECAST_LOG_WARN("manage auth request add for %lu but no user/pass", id);
879 880
                break;
            }
881 882

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

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

            if (!auth->deleteuser) {
903
                error_id = ICECAST_ERROR_ADMIN_ROLEMGN_DELETE_NOSYS;
904 905
                break;
            }
906

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

915
        doc = xmlNewDoc(XMLSTR("1.0"));
916
        node = admin_build_rootnode(doc, "icestats");
917

918
        rolenode = admin_add_role_to_authentication(auth, node);
919

920
        if (message) {
921
            msgnode = admin_build_rootnode(doc, "iceresponse");
922
            xmlNewTextChild(msgnode, NULL, XMLSTR("message"), XMLSTR(message));
923
        }
924

925
        xmlDocSetRootElement(doc, node);
926

927 928 929 930
        if (auth && auth->listuser) {
            usersnode = xmlNewChild(rolenode, NULL, XMLSTR("users"), NULL);
            auth->listuser(auth, usersnode);
        }
931

932 933
        config_release_config();
        auth_release(auth);
934

935
        admin_send_response(doc, client, response,
936
            MANAGEAUTH_HTML_REQUEST);
Marvin Scholz's avatar
Marvin Scholz committed
937
        free(message);
938 939 940 941
        xmlFreeDoc(doc);
        return;
    } while (0);

942 943
    config_release_config();
    auth_release(auth);
944
    client_send_error_by_id(client, error_id);
945 946
}

Marvin Scholz's avatar
Marvin Scholz committed
947 948
static void command_kill_source(client_t *client,
                                source_t *source,
949
                                admin_format_t response)
950
{
951 952 953
    xmlDocPtr doc;
    xmlNodePtr node;

954
    doc = xmlNewDoc (XMLSTR("1.0"));
955
    node = admin_build_rootnode(doc, "iceresponse");
956 957
    xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR("Source Removed"));
    xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
958 959
    xmlDocSetRootElement(doc, node);

960 961
    source->running = 0;

962
    admin_send_response(doc, client, response,
963 964
        ADMIN_XSL_RESPONSE);
    xmlFreeDoc(doc);
965 966
}

Marvin Scholz's avatar
Marvin Scholz committed
967 968
static void command_kill_client(client_t *client,
                                source_t *source,
969
                                admin_format_t response)
970
{
971
    const char *idtext;
972 973
    int id;
    client_t *listener;
974 975 976
    xmlDocPtr