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).
Philipp Schafft's avatar
Philipp Schafft committed
11
 * Copyright 2012-2014, 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 38 39 40

#include "format.h"

#include "logging.h"
41
#include "auth.h"
Ed "oddsock" Zaleski's avatar
Ed "oddsock" Zaleski committed
42 43 44
#ifdef _WIN32
#define snprintf _snprintf
#endif
45 46 47

#define CATMODULE "admin"

48 49 50
/* special commands */
#define COMMAND_ERROR                      ADMIN_COMMAND_ERROR
#define COMMAND_ANY                        ADMIN_COMMAND_ANY
51

52
/* Mount-specific commands (block 1-49 and 50-99) */
53 54 55 56 57 58 59
#define COMMAND_RAW_FALLBACK               1
#define COMMAND_RAW_METADATA_UPDATE        2
#define COMMAND_RAW_SHOW_LISTENERS         3
#define COMMAND_RAW_MOVE_CLIENTS           4
#define COMMAND_RAW_MANAGEAUTH             5
#define COMMAND_SHOUTCAST_METADATA_UPDATE  6
#define COMMAND_RAW_UPDATEMETADATA         7
60 61 62 63

#define COMMAND_TRANSFORMED_FALLBACK        50
#define COMMAND_TRANSFORMED_SHOW_LISTENERS  53
#define COMMAND_TRANSFORMED_MOVE_CLIENTS    54
64
#define COMMAND_TRANSFORMED_MANAGEAUTH      55
65 66
#define COMMAND_TRANSFORMED_UPDATEMETADATA  56
#define COMMAND_TRANSFORMED_METADATA_UPDATE 57
67

68
/* Global commands (block 101-199 and 201-299) */
Karl Heyes's avatar
Karl Heyes committed
69 70 71 72
#define COMMAND_RAW_LIST_MOUNTS             101
#define COMMAND_RAW_STATS                   102
#define COMMAND_RAW_LISTSTREAM              103
#define COMMAND_PLAINTEXT_LISTSTREAM        104
73
#define COMMAND_RAW_QUEUE_RELOAD            105
Karl Heyes's avatar
Karl Heyes committed
74 75 76
#define COMMAND_TRANSFORMED_LIST_MOUNTS     201
#define COMMAND_TRANSFORMED_STATS           202
#define COMMAND_TRANSFORMED_LISTSTREAM      203
77
#define COMMAND_TRANSFORMED_QUEUE_RELOAD    205
78

79
/* Client management commands (block 301-399 and 401-499) */
Karl Heyes's avatar
Karl Heyes committed
80 81 82 83
#define COMMAND_RAW_KILL_CLIENT             301
#define COMMAND_RAW_KILL_SOURCE             302
#define COMMAND_TRANSFORMED_KILL_CLIENT     401
#define COMMAND_TRANSFORMED_KILL_SOURCE     402
84

85
/* Admin commands requiring no auth (block 501-599) */
86 87
#define COMMAND_BUILDM3U                    501

88 89 90 91
/* Experimental features (e.g. in feature branches) (block 801-899) */

/* Private features (in branches not for merge with master) (block 901-999) */

92 93
#define FALLBACK_RAW_REQUEST "fallbacks"
#define FALLBACK_TRANSFORMED_REQUEST "fallbacks.xsl"
94
#define SHOUTCAST_METADATA_REQUEST "admin.cgi"
95 96
#define METADATA_RAW_REQUEST "metadata"
#define METADATA_TRANSFORMED_REQUEST "metadata.xsl"
97 98 99 100
#define LISTCLIENTS_RAW_REQUEST "listclients"
#define LISTCLIENTS_TRANSFORMED_REQUEST "listclients.xsl"
#define STATS_RAW_REQUEST "stats"
#define STATS_TRANSFORMED_REQUEST "stats.xsl"
101 102
#define QUEUE_RELOAD_RAW_REQUEST "reloadconfig"
#define QUEUE_RELOAD_TRANSFORMED_REQUEST "reloadconfig.xsl"
103 104 105 106
#define LISTMOUNTS_RAW_REQUEST "listmounts"
#define LISTMOUNTS_TRANSFORMED_REQUEST "listmounts.xsl"
#define STREAMLIST_RAW_REQUEST "streamlist"
#define STREAMLIST_TRANSFORMED_REQUEST "streamlist.xsl"
107
#define STREAMLIST_PLAINTEXT_REQUEST "streamlist.txt"
108 109 110 111 112 113 114
#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"
115 116
#define MANAGEAUTH_RAW_REQUEST "manageauth"
#define MANAGEAUTH_TRANSFORMED_REQUEST "manageauth.xsl"
117 118
#define UPDATEMETADATA_RAW_REQUEST "updatemetadata"
#define UPDATEMETADATA_TRANSFORMED_REQUEST "updatemetadata.xsl"
119 120
#define DEFAULT_RAW_REQUEST ""
#define DEFAULT_TRANSFORMED_REQUEST ""
121
#define BUILDM3U_RAW_REQUEST "buildm3u"
122

Philipp Schafft's avatar
Philipp Schafft committed
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 152 153 154 155 156 157 158
typedef struct admin_command_tag {
    const int   id;
    const char *name;
    const int   type;
    const int   format;
} admin_command_t;

/*
COMMAND_TRANSFORMED_METADATA_UPDATE -> METADATA_TRANSFORMED_REQUEST
COMMAND_TRANSFORMED_UPDATEMETADATA  -> UPDATEMETADATA_TRANSFORMED_REQUEST
*/

static const admin_command_t commands[] = {
 {COMMAND_RAW_FALLBACK,                FALLBACK_RAW_REQUEST,               ADMINTYPE_MOUNT,   RAW},
 {COMMAND_TRANSFORMED_FALLBACK,        FALLBACK_TRANSFORMED_REQUEST,       ADMINTYPE_MOUNT,   TRANSFORMED},
 {COMMAND_RAW_METADATA_UPDATE,         METADATA_RAW_REQUEST,               ADMINTYPE_MOUNT,   RAW},
 {COMMAND_SHOUTCAST_METADATA_UPDATE,   SHOUTCAST_METADATA_REQUEST,         ADMINTYPE_MOUNT,   TRANSFORMED},
 {COMMAND_TRANSFORMED_METADATA_UPDATE, METADATA_TRANSFORMED_REQUEST,       ADMINTYPE_MOUNT,   TRANSFORMED},
 {COMMAND_RAW_SHOW_LISTENERS,          LISTCLIENTS_RAW_REQUEST,            ADMINTYPE_MOUNT,   RAW},
 {COMMAND_TRANSFORMED_SHOW_LISTENERS,  LISTCLIENTS_TRANSFORMED_REQUEST,    ADMINTYPE_MOUNT,   TRANSFORMED},
 {COMMAND_RAW_STATS,                   STATS_RAW_REQUEST,                  ADMINTYPE_HYBRID,  RAW},
 {COMMAND_TRANSFORMED_STATS,           STATS_TRANSFORMED_REQUEST,          ADMINTYPE_HYBRID,  TRANSFORMED},
 {COMMAND_RAW_STATS,                   "stats.xml",                        ADMINTYPE_HYBRID,  RAW}, /* The old way */
 {COMMAND_RAW_QUEUE_RELOAD,            QUEUE_RELOAD_RAW_REQUEST,           ADMINTYPE_GENERAL, RAW},
 {COMMAND_TRANSFORMED_QUEUE_RELOAD,    QUEUE_RELOAD_TRANSFORMED_REQUEST,   ADMINTYPE_GENERAL, TRANSFORMED},
 {COMMAND_RAW_LIST_MOUNTS,             LISTMOUNTS_RAW_REQUEST,             ADMINTYPE_GENERAL, RAW},
 {COMMAND_TRANSFORMED_LIST_MOUNTS,     LISTMOUNTS_TRANSFORMED_REQUEST,     ADMINTYPE_GENERAL, TRANSFORMED},
 {COMMAND_RAW_LISTSTREAM,              STREAMLIST_RAW_REQUEST,             ADMINTYPE_GENERAL, RAW},
 {COMMAND_PLAINTEXT_LISTSTREAM,        STREAMLIST_PLAINTEXT_REQUEST,       ADMINTYPE_GENERAL, PLAINTEXT},
 {COMMAND_TRANSFORMED_LISTSTREAM,      STREAMLIST_TRANSFORMED_REQUEST,     ADMINTYPE_GENERAL, TRANSFORMED},
 {COMMAND_RAW_MOVE_CLIENTS,            MOVECLIENTS_RAW_REQUEST,            ADMINTYPE_MOUNT,   RAW},
 {COMMAND_TRANSFORMED_MOVE_CLIENTS,    MOVECLIENTS_TRANSFORMED_REQUEST,    ADMINTYPE_HYBRID,  TRANSFORMED},
 {COMMAND_RAW_KILL_CLIENT,             KILLCLIENT_RAW_REQUEST,             ADMINTYPE_MOUNT,   RAW},
 {COMMAND_TRANSFORMED_KILL_CLIENT,     KILLCLIENT_TRANSFORMED_REQUEST,     ADMINTYPE_MOUNT,   TRANSFORMED},
 {COMMAND_RAW_KILL_SOURCE,             KILLSOURCE_RAW_REQUEST,             ADMINTYPE_MOUNT,   RAW},
 {COMMAND_TRANSFORMED_KILL_SOURCE,     KILLSOURCE_TRANSFORMED_REQUEST,     ADMINTYPE_MOUNT,   TRANSFORMED},
159 160
 {COMMAND_RAW_MANAGEAUTH,              MANAGEAUTH_RAW_REQUEST,             ADMINTYPE_GENERAL, RAW},
 {COMMAND_TRANSFORMED_MANAGEAUTH,      MANAGEAUTH_TRANSFORMED_REQUEST,     ADMINTYPE_GENERAL, TRANSFORMED},
Philipp Schafft's avatar
Philipp Schafft committed
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
 {COMMAND_RAW_UPDATEMETADATA,          UPDATEMETADATA_RAW_REQUEST,         ADMINTYPE_MOUNT,   RAW},
 {COMMAND_TRANSFORMED_UPDATEMETADATA,  UPDATEMETADATA_TRANSFORMED_REQUEST, ADMINTYPE_MOUNT,   TRANSFORMED},
 {COMMAND_BUILDM3U,                    BUILDM3U_RAW_REQUEST,               ADMINTYPE_MOUNT,   RAW},
 {COMMAND_TRANSFORMED_STATS,           DEFAULT_TRANSFORMED_REQUEST,        ADMINTYPE_HYBRID,  TRANSFORMED},
 {COMMAND_TRANSFORMED_STATS,           DEFAULT_RAW_REQUEST,                ADMINTYPE_HYBRID,  TRANSFORMED},
 {COMMAND_ANY,                         "*",                                ADMINTYPE_GENERAL, TRANSFORMED} /* for ACL framework */
};

int admin_get_command(const char *command) {
    size_t i;

    for (i = 0; i < (sizeof(commands)/sizeof(*commands)); i++)
        if (strcmp(commands[i].name, command) == 0)
            return commands[i].id;

    return COMMAND_ERROR;
}

int admin_get_command_type(int command) {
    size_t i;

    if (command == ADMIN_COMMAND_ERROR || command == COMMAND_ANY)
        return ADMINTYPE_ERROR;

    for (i = 0; i < (sizeof(commands)/sizeof(*commands)); i++)
        if (commands[i].id == command)
            return commands[i].type;

    return ADMINTYPE_ERROR;
190 191
}

192
static void command_fallback(client_t *client, source_t *source, int response);
193
static void command_metadata(client_t *client, source_t *source, int response);
194
static void command_shoutcast_metadata(client_t *client, source_t *source);
195 196 197 198
static void command_show_listeners(client_t *client, source_t *source,
        int response);
static void command_move_clients(client_t *client, source_t *source,
        int response);
199
static void command_stats(client_t *client, const char *mount, int response);
200
static void command_queue_reload(client_t *client, int response);
201 202 203
static void command_list_mounts(client_t *client, int response);
static void command_kill_client(client_t *client, source_t *source,
        int response);
204
static void command_manageauth(client_t *client, int response);
205
static void command_buildm3u(client_t *client, const char *mount);
206 207
static void command_kill_source(client_t *client, source_t *source,
        int response);
208 209
static void command_updatemetadata(client_t *client, source_t *source,
        int response);
Philipp Schafft's avatar
Philipp Schafft committed
210 211
static void admin_handle_mount_request(client_t *client, source_t *source);
static void admin_handle_general_request(client_t *client);
212

213 214 215 216
/* 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 */
xmlDocPtr admin_build_sourcelist (const char *mount)
217 218 219 220 221 222 223 224
{
    avl_node *node;
    source_t *source;
    xmlNodePtr xmlnode, srcnode;
    xmlDocPtr doc;
    char buf[22];
    time_t now = time(NULL);

225 226
    doc = xmlNewDoc (XMLSTR("1.0"));
    xmlnode = xmlNewDocNode (doc, NULL, XMLSTR("icestats"), NULL);
227
    xmlDocSetRootElement(doc, xmlnode);
228

229
    if (mount) {
230
        xmlNewChild (xmlnode, NULL, XMLSTR("current_source"), XMLSTR(mount));
231 232 233 234 235
    }

    node = avl_get_first(global.source_tree);
    while(node) {
        source = (source_t *)node->key;
236 237 238 239 240 241
        if (mount && strcmp (mount, source->mount) == 0)
        {
            node = avl_get_next (node);
            continue;
        }

242
        if (source->running || source->on_demand)
243
        {
244 245
            ice_config_t *config;
            mount_proxy *mountinfo;
Philipp Schafft's avatar
Philipp Schafft committed
246
            acl_t *acl = NULL;
247

248 249
            srcnode = xmlNewChild(xmlnode, NULL, XMLSTR("source"), NULL);
            xmlSetProp(srcnode, XMLSTR("mount"), XMLSTR(source->mount));
250

251
            xmlNewChild(srcnode, NULL, XMLSTR("fallback"),
252
                    (source->fallback_mount != NULL)?
253
                    XMLSTR(source->fallback_mount):XMLSTR(""));
254
            snprintf(buf, sizeof(buf), "%lu", source->listeners);
255
            xmlNewChild(srcnode, NULL, XMLSTR("listeners"), XMLSTR(buf));
256

Karl Heyes's avatar
Karl Heyes committed
257
            config = config_get_config();
258
            mountinfo = config_find_mount (config, source->mount, MOUNT_TYPE_NORMAL);
Philipp Schafft's avatar
Philipp Schafft committed
259
            if (mountinfo)
260
                acl = auth_stack_get_anonymous_acl(mountinfo->authstack, httpp_req_get);
Philipp Schafft's avatar
Philipp Schafft committed
261
            if (!acl)
262
                acl = auth_stack_get_anonymous_acl(config->authstack, httpp_req_get);
Philipp Schafft's avatar
Philipp Schafft committed
263 264
            if (acl && acl_test_web(acl) == ACL_POLICY_DENY) {
                xmlNewChild(srcnode, NULL, XMLSTR("authenticator"), XMLSTR("(dummy)"));
265
            }
Philipp Schafft's avatar
Philipp Schafft committed
266
            acl_release(acl);
267 268
            config_release_config();

269 270
            if (source->running)
            {
271
                if (source->client)
Karl Heyes's avatar
Karl Heyes committed
272 273 274
                {
                    snprintf (buf, sizeof(buf), "%lu",
                            (unsigned long)(now - source->con->con_time));
275
                    xmlNewChild (srcnode, NULL, XMLSTR("Connected"), XMLSTR(buf));
Karl Heyes's avatar
Karl Heyes committed
276
                }
277
                xmlNewChild (srcnode, NULL, XMLSTR("content-type"),
278
                        XMLSTR(source->format->contenttype));
279
            }
280
        }
281 282 283 284 285
        node = avl_get_next(node);
    }
    return(doc);
}

286 287
void admin_send_response (xmlDocPtr doc, client_t *client,
        int response, const char *xslt_template)
288
{
289 290 291 292
    if (response == RAW)
    {
        xmlChar *buff = NULL;
        int len = 0;
293 294 295
        size_t buf_len;
        ssize_t ret;

296
        xmlDocDumpMemory(doc, &buff, &len);
297 298 299 300

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

302 303
        client_set_queue(client, NULL);
        client->refbuf = refbuf_new(buf_len);
304

305 306 307 308
        ret = util_http_build_header(client->refbuf->data, buf_len, 0,
                                     0, 200, NULL,
                                     "text/xml", "utf-8",
                                     NULL, NULL);
Philipp Schafft's avatar
Philipp Schafft committed
309
        if (ret < 0) {
310
            ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
311
            client_send_error(client, 500, 0, "Header generation failed.");
312 313
            xmlFree(buff);
            return;
Philipp Schafft's avatar
Philipp Schafft committed
314
        } else if (buf_len < (size_t)(len + ret + 64)) {
315 316 317 318 319 320 321 322 323 324 325 326 327
            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",
                                             NULL, NULL);
                if (ret == -1) {
                    ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
328
                    client_send_error(client, 500, 0, "Header generation failed.");
329 330 331 332 333
                    xmlFree(buff);
                    return;
                }
            } else {
                ICECAST_LOG_ERROR("Client buffer reallocation failed. Dropping client.");
334
                client_send_error(client, 500, 0, "Buffer reallocation failed.");
335 336
                xmlFree(buff);
                return;
337
            }
338
        }
339

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

        client->refbuf->len = ret;
344 345 346
        xmlFree(buff);
        client->respcode = 200;
        fserve_add_client (client, NULL);
347
    }
348 349 350 351 352 353
    if (response == TRANSFORMED)
    {
        char *fullpath_xslt_template;
        int fullpath_xslt_template_len;
        ice_config_t *config = config_get_config();

354
        fullpath_xslt_template_len = strlen (config->adminroot_dir) +
355
            strlen (xslt_template) + 2;
356 357
        fullpath_xslt_template = malloc(fullpath_xslt_template_len);
        snprintf(fullpath_xslt_template, fullpath_xslt_template_len, "%s%s%s",
358
            config->adminroot_dir, PATH_SEPARATOR, xslt_template);
359
        config_release_config();
360

361
        ICECAST_LOG_DEBUG("Sending XSLT (%s)", fullpath_xslt_template);
362 363 364 365
        xslt_transform(doc, fullpath_xslt_template, client);
        free(fullpath_xslt_template);
    }
}
366 367


368
void admin_handle_request(client_t *client, const char *uri)
369
{
370
    const char *mount, *command_string;
371

372
    ICECAST_LOG_DEBUG("Admin request (%s)", uri);
373 374
    if (!((strcmp(uri, "/admin.cgi") == 0) ||
         (strncmp("/admin/", uri, 7) == 0))) {
375
        ICECAST_LOG_ERROR("Internal error: admin request isn't");
376
        client_send_error(client, 401, 1, "You need to authenticate\r\n");
377 378 379
        return;
    }

380 381 382 383 384 385
    if (strcmp(uri, "/admin.cgi") == 0) {
        command_string = uri + 1;
    }
    else {
        command_string = uri + 7;
    }
386

387
    ICECAST_LOG_DEBUG("Got command (%s)", command_string);
388

Philipp Schafft's avatar
Philipp Schafft committed
389
    if (client->admin_command <= 0) {
390
        ICECAST_LOG_ERROR("Error parsing command string or unrecognised command: %s",
391
                command_string);
392
        client_send_error(client, 400, 0, "Unrecognised command");
393 394 395
        return;
    }

Philipp Schafft's avatar
Philipp Schafft committed
396 397 398 399 400 401 402
    if (acl_test_admin(client->acl, client->admin_command) != ACL_POLICY_ALLOW) {
        if (client->admin_command == COMMAND_RAW_METADATA_UPDATE &&
            (acl_test_method(client->acl, httpp_req_source) == ACL_POLICY_ALLOW ||
             acl_test_method(client->acl, httpp_req_put)    == ACL_POLICY_ALLOW)) {
            ICECAST_LOG_DEBUG("Granted right to call COMMAND_RAW_METADATA_UPDATE to client because it is allowed to do SOURCE or PUT.");
        } else {
            client_send_error(client, 401, 1, "You need to authenticate\r\n");
403 404
            return;
        }
405 406
    }

407 408
    mount = httpp_get_query_param(client->parser, "mount");

409 410 411
    if(mount != NULL) {
        source_t *source;

412
        /* this request does not require auth but can apply to files on webroot */
Philipp Schafft's avatar
Philipp Schafft committed
413 414
        if (client->admin_command == COMMAND_BUILDM3U) {
            command_buildm3u(client, mount);
415
            return;
416
        }
417

Philipp Schafft's avatar
Philipp Schafft committed
418
        /* This is a mount request, handle it as such */
419
        avl_tree_rlock(global.source_tree);
Michael Smith's avatar
Michael Smith committed
420
        source = source_find_mount_raw(mount);
421

422 423
        if (source == NULL)
        {
424
            ICECAST_LOG_WARN("Admin command %s on non-existent source %s",
425
                    command_string, mount);
426
            avl_tree_unlock(global.source_tree);
427
            client_send_error(client, 400, 0, "Source does not exist");
428
        }
429 430
        else
        {
431
            if (source->running == 0 && source->on_demand == 0)
432 433
            {
                avl_tree_unlock (global.source_tree);
434
                ICECAST_LOG_INFO("Received admin command %s on unavailable mount \"%s\"",
435
                        command_string, mount);
436
                client_send_error(client, 400, 0, "Source is not available");
437 438
                return;
            }
Philipp Schafft's avatar
Philipp Schafft committed
439
            if (client->admin_command == COMMAND_SHOUTCAST_METADATA_UPDATE &&
440 441 442
                    source->shoutcast_compat == 0)
            {
                avl_tree_unlock (global.source_tree);
443
                ICECAST_LOG_ERROR("illegal change of metadata on non-shoutcast "
444
                        "compatible stream");
445
                client_send_error(client, 400, 0, "illegal metadata call");
446
                return;
447
            }
448
            ICECAST_LOG_INFO("Received admin command %s on mount \"%s\"",
449
                    command_string, mount);
Philipp Schafft's avatar
Philipp Schafft committed
450
            admin_handle_mount_request(client, source);
451
            avl_tree_unlock(global.source_tree);
452
        }
453 454
    }
    else {
Philipp Schafft's avatar
Philipp Schafft committed
455
        admin_handle_general_request(client);
456 457 458
    }
}

Philipp Schafft's avatar
Philipp Schafft committed
459
static void admin_handle_general_request(client_t *client)
460
{
Philipp Schafft's avatar
Philipp Schafft committed
461
    switch(client->admin_command) {
462
        case COMMAND_RAW_STATS:
463
            command_stats(client, NULL, RAW);
464
            break;
465 466 467
        case COMMAND_RAW_QUEUE_RELOAD:
            command_queue_reload(client, RAW);
            break;
468 469
        case COMMAND_RAW_LIST_MOUNTS:
            command_list_mounts(client, RAW);
470 471
            break;
        case COMMAND_RAW_LISTSTREAM:
472 473
            command_list_mounts(client, RAW);
            break;
474 475 476
        case COMMAND_PLAINTEXT_LISTSTREAM:
            command_list_mounts(client, PLAINTEXT);
            break;
477
        case COMMAND_TRANSFORMED_STATS:
478
            command_stats(client, NULL, TRANSFORMED);
479
            break;
480 481 482
        case COMMAND_TRANSFORMED_QUEUE_RELOAD:
            command_queue_reload(client, TRANSFORMED);
            break;
483 484 485 486 487 488 489 490
        case COMMAND_TRANSFORMED_LIST_MOUNTS:
            command_list_mounts(client, TRANSFORMED);
            break;
        case COMMAND_TRANSFORMED_LISTSTREAM:
            command_list_mounts(client, TRANSFORMED);
            break;
        case COMMAND_TRANSFORMED_MOVE_CLIENTS:
            command_list_mounts(client, TRANSFORMED);
491
            break;
492 493 494 495 496 497
        case COMMAND_TRANSFORMED_MANAGEAUTH:
            command_manageauth(client, TRANSFORMED);
            break;
        case COMMAND_RAW_MANAGEAUTH:
            command_manageauth(client, RAW);
            break;
498
        default:
499
            ICECAST_LOG_WARN("General admin request not recognised");
500
            client_send_error(client, 400, 0, "Unknown admin request");
501 502 503 504
            return;
    }
}

Philipp Schafft's avatar
Philipp Schafft committed
505 506
static void admin_handle_mount_request(client_t *client, source_t *source) {
    switch(client->admin_command) {
507 508 509
        case COMMAND_RAW_STATS:
            command_stats(client, source->mount, RAW);
            break;
510 511
        case COMMAND_RAW_FALLBACK:
            command_fallback(client, source, RAW);
512
            break;
513 514 515 516 517
        case COMMAND_RAW_METADATA_UPDATE:
            command_metadata(client, source, RAW);
            break;
        case COMMAND_TRANSFORMED_METADATA_UPDATE:
            command_metadata(client, source, TRANSFORMED);
518
            break;
519 520 521
        case COMMAND_SHOUTCAST_METADATA_UPDATE:
            command_shoutcast_metadata(client, source);
            break;
522 523 524 525 526 527 528 529
        case COMMAND_RAW_SHOW_LISTENERS:
            command_show_listeners(client, source, RAW);
            break;
        case COMMAND_RAW_MOVE_CLIENTS:
            command_move_clients(client, source, RAW);
            break;
        case COMMAND_RAW_KILL_CLIENT:
            command_kill_client(client, source, RAW);
530
            break;
531 532
        case COMMAND_RAW_KILL_SOURCE:
            command_kill_source(client, source, RAW);
533
            break;
534 535 536
        case COMMAND_TRANSFORMED_STATS:
            command_stats(client, source->mount, TRANSFORMED);
            break;
537 538
        case COMMAND_TRANSFORMED_FALLBACK:
            command_fallback(client, source, RAW);
539
            break;
540 541 542 543 544 545 546 547 548 549 550
        case COMMAND_TRANSFORMED_SHOW_LISTENERS:
            command_show_listeners(client, source, TRANSFORMED);
            break;
        case COMMAND_TRANSFORMED_MOVE_CLIENTS:
            command_move_clients(client, source, TRANSFORMED);
            break;
        case COMMAND_TRANSFORMED_KILL_CLIENT:
            command_kill_client(client, source, TRANSFORMED);
            break;
        case COMMAND_TRANSFORMED_KILL_SOURCE:
            command_kill_source(client, source, TRANSFORMED);
551
            break;
552 553 554 555 556 557
        case COMMAND_TRANSFORMED_UPDATEMETADATA:
            command_updatemetadata(client, source, TRANSFORMED);
            break;
        case COMMAND_RAW_UPDATEMETADATA:
            command_updatemetadata(client, source, RAW);
            break;
558
        default:
559
            ICECAST_LOG_WARN("Mount request not recognised");
560
            client_send_error(client, 400, 0, "Mount request unknown");
561
            break;
562 563 564 565 566 567 568
    }
}

#define COMMAND_REQUIRE(client,name,var) \
    do { \
        (var) = httpp_get_query_param((client)->parser, (name)); \
        if((var) == NULL) { \
569
            client_send_error((client), 400, 0, "Missing parameter"); \
570 571 572
            return; \
        } \
    } while(0);
573 574
#define COMMAND_OPTIONAL(client,name,var) \
    (var) = httpp_get_query_param((client)->parser, (name))
575

576
static void html_success(client_t *client, char *message)
577
{
578 579 580 581
    ssize_t ret;

    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE, 0,
                                 0, 200, NULL,
582 583
                                 "text/html", "utf-8",
                                 "", NULL);
584 585 586

    if (ret == -1 || ret >= PER_CLIENT_REFBUF_SIZE) {
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
587
        client_send_error(client, 500, 0, "Header generation failed.");
588 589 590
        return;
    }

591
    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
592 593
        "<html><head><title>Admin request successful</title></head>"
        "<body><p>%s</p></body></html>", message);
594

595
    client->respcode = 200;
Karl Heyes's avatar
Karl Heyes committed
596 597
    client->refbuf->len = strlen (client->refbuf->data);
    fserve_add_client (client, NULL);
598 599
}

600

601 602
static void command_move_clients(client_t *client, source_t *source,
    int response)
603
{
604
    const char *dest_source;
605
    source_t *dest;
606 607 608 609 610
    xmlDocPtr doc;
    xmlNodePtr node;
    char buf[255];
    int parameters_passed = 0;

611
    ICECAST_LOG_DEBUG("Doing optional check");
612
    if((COMMAND_OPTIONAL(client, "destination", dest_source))) {
613 614
        parameters_passed = 1;
    }
615
    ICECAST_LOG_DEBUG("Done optional check (%d)", parameters_passed);
616 617
    if (!parameters_passed) {
        doc = admin_build_sourcelist(source->mount);
618
        admin_send_response(doc, client, response,
619 620 621 622
             MOVECLIENTS_TRANSFORMED_REQUEST);
        xmlFreeDoc(doc);
        return;
    }
623

624 625 626 627
    dest = source_find_mount (dest_source);

    if (dest == NULL)
    {
628
        client_send_error(client, 400, 0, "No such destination");
629 630 631
        return;
    }

Karl Heyes's avatar
Karl Heyes committed
632
    if (strcmp (dest->mount, source->mount) == 0)
633
    {
634
        client_send_error(client, 400, 0, "supplied mountpoints are identical");
635 636 637
        return;
    }

638
    if (dest->running == 0 && dest->on_demand == 0)
639
    {
640
        client_send_error(client, 400, 0, "Destination not running");
641 642 643
        return;
    }

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

646 647
    doc = xmlNewDoc (XMLSTR("1.0"));
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
648 649
    xmlDocSetRootElement(doc, node);

650
    source_move_clients (source, dest);
651

652
    memset(buf, '\000', sizeof(buf));
653
    snprintf (buf, sizeof(buf), "Clients moved from %s to %s",
654
        source->mount, dest_source);
655 656
    xmlNewChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
    xmlNewChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
657

658
    admin_send_response(doc, client, response,
659 660
        ADMIN_XSL_RESPONSE);
    xmlFreeDoc(doc);
661 662
}

663
static inline xmlNodePtr __add_listener(client_t *client, xmlNodePtr parent, time_t now, operation_mode mode) {
664 665 666 667
    const char *tmp;
    xmlNodePtr node;
    char buf[22];

Philipp Schafft's avatar
Philipp Schafft committed
668 669 670 671 672
    /* TODO: kh has support for a child node "lag". We should add that.
     * BEFORE RELEASE 2.5.0 REVIEW #2097: Check if we are on-track for lowercasing child nodes.
     * BEFORE RELEASE 2.6.0 TODO #2097: Change case of child nodes to lower case.
     * The case of <ID>, <IP>, <UserAgent> and <Connected> should be converted to lower case.
     */
673 674 675 676 677 678 679

    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
680
    xmlSetProp(node, XMLSTR("id"), XMLSTR(buf));
681
    xmlNewChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "ID" : "id"), XMLSTR(buf));
682

683
    xmlNewChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "IP" : "ip"), XMLSTR(client->con->ip));
684 685 686

    tmp = httpp_getvar(client->parser, "user-agent");
    if (tmp)
687
        xmlNewChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "UserAgent" : "useragent"), XMLSTR(tmp));
688 689 690

    tmp = httpp_getvar(client->parser, "referer");
    if (tmp)
Philipp Schafft's avatar
Philipp Schafft committed
691
        xmlNewChild(node, NULL, XMLSTR("referer"), XMLSTR(tmp));
692 693 694

    memset(buf, '\000', sizeof(buf));
    snprintf(buf, sizeof(buf), "%lu", (unsigned long)(now - client->con->con_time));
695
    xmlNewChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "Connected" : "connected"), XMLSTR(buf));
696 697 698 699

    if (client->username)
        xmlNewChild(node, NULL, XMLSTR("username"), XMLSTR(client->username));

Philipp Schafft's avatar
Philipp Schafft committed
700 701 702
    if (client->role)
        xmlNewChild(node, NULL, XMLSTR("role"), XMLSTR(client->role));

703 704 705
    return node;
}

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

719 720
static void command_show_listeners(client_t *client, source_t *source,
    int response)
721
{
722
    xmlDocPtr doc;
723
    xmlNodePtr node, srcnode;
724
    char buf[22];
725

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

732
    memset(buf, '\000', sizeof(buf));
733
    snprintf (buf, sizeof(buf), "%lu", source->listeners);
734
    xmlNewChild(srcnode, NULL, XMLSTR("Listeners"), XMLSTR(buf));
735

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

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

743
static void command_buildm3u(client_t *client,  const char *mount)
744
{
745 746
    const char *username = NULL;
    const char *password = NULL;
747
    ice_config_t *config;
748
    ssize_t ret;
749 750 751 752

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

753 754
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE, 0,
                                 0, 200, NULL,
755 756
                                 "audio/x-mpegurl", NULL,
                                 NULL, NULL);
757

758 759
    if (ret == -1 || ret >= (PER_CLIENT_REFBUF_SIZE - 512)) { /* we want at least 512 Byte left for data */
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
760
        client_send_error(client, 500, 0, "Header generation failed.");
761 762 763 764
        return;
    }


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

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

782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801
xmlNodePtr admin_add_role_to_authentication(auth_t *auth, xmlNodePtr parent) {
    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;
}
802

803
static void command_manageauth(client_t *client, int response) {
804
    xmlDocPtr doc;
805
    xmlNodePtr node, rolenode, usersnode, msgnode;
806 807
    const char *action = NULL;
    const char *username = NULL;
808
    const char *idstring = NULL;
809 810
    char *message = NULL;
    int ret = AUTH_OK;
811 812 813 814
    int error_code = 400;
    const char *error_message = "missing parameter";
    long unsigned int id;
    ice_config_t *config = config_get_config();
Philipp Schafft's avatar
Philipp Schafft committed
815
    auth_t *auth;
816

817 818
    do
    {
819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841
        /* 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);
            error_code = 404;
            error_message = "Role not found";
842
            break;
843
        }
Philipp Schafft's avatar
Philipp Schafft committed
844

845
        COMMAND_OPTIONAL(client, "action", action);
846
        COMMAND_OPTIONAL(client, "username", username);
847 848

        if (action == NULL)
849
            action = "list";
850 851 852 853

        if (!strcmp(action, "add"))
        {
            const char *password = NULL;
854
            COMMAND_OPTIONAL(client, "password", password);
855

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

            if (!auth->adduser) {
                error_message = "Adding users to role not supported by role";
                break;
            }

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

            if (!auth->deleteuser) {
                error_message = "Deleting users from role not supported by role";
884 885
                break;
            }
886

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

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

898
        rolenode = admin_add_role_to_authentication(auth, node);
899

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

905
        xmlDocSetRootElement(doc, node);
906

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

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

915
        admin_send_response(doc, client, response,
916 917 918 919 920 921
                MANAGEAUTH_TRANSFORMED_REQUEST);
        free (message);
        xmlFreeDoc(doc);
        return;
    } while (0);

922 923 924
    config_release_config();
    auth_release(auth);
    client_send_error(client, error_code, 0, error_message);
925 926
}

927 928
static void command_kill_source(client_t *client, source_t *source,
    int response)
929
{
930 931 932
    xmlDocPtr doc;
    xmlNodePtr node;

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

939 940
    source->running = 0;

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

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

    COMMAND_REQUIRE(client, "id", idtext);

    id = atoi(idtext);

    listener = source_find_client(source, id);

962 963
    doc = xmlNewDoc (XMLSTR("1.0"));
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
964
    xmlDocSetRootElement(doc, node);
965
    ICECAST_LOG_DEBUG("Response is %d", response);
966

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

        /* This tags it for removal on the next iteration of the main source
         * loop
         */
        listener->con->error = 1;
974 975
        memset(buf, '\000', sizeof(buf));
        snprintf(buf, sizeof(buf)-1, "Client %d removed", id);
976 977
        xmlNewChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
        xmlNewChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
978 979
    }
    else {
980 981
        memset(buf, '\000', sizeof(buf));
        snprintf(buf, sizeof(buf)-1, "Client %d not found", id);