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

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

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

26
#include "cfgfile.h"
27 28 29 30 31 32
#include "connection.h"
#include "refbuf.h"
#include "client.h"
#include "source.h"
#include "global.h"
#include "stats.h"
33
#include "compat.h"
34
#include "xslt.h"
35
#include "fserve.h"
36
#include "admin.h"
37
#include "errors.h"
38 39 40 41

#include "format.h"

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

#define CATMODULE "admin"

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

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

62 63 64
/* special commands */
#define COMMAND_ERROR                      ADMIN_COMMAND_ERROR
#define COMMAND_ANY                        ADMIN_COMMAND_ANY
65

Marvin Scholz's avatar
Marvin Scholz committed
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
#define FALLBACK_RAW_REQUEST                "fallbacks"
#define FALLBACK_TRANSFORMED_REQUEST        "fallbacks.xsl"
#define SHOUTCAST_METADATA_REQUEST          "admin.cgi"
#define METADATA_RAW_REQUEST                "metadata"
#define METADATA_TRANSFORMED_REQUEST        "metadata.xsl"
#define LISTCLIENTS_RAW_REQUEST             "listclients"
#define LISTCLIENTS_TRANSFORMED_REQUEST     "listclients.xsl"
#define STATS_RAW_REQUEST                   "stats"
#define STATS_TRANSFORMED_REQUEST           "stats.xsl"
#define QUEUE_RELOAD_RAW_REQUEST            "reloadconfig"
#define QUEUE_RELOAD_TRANSFORMED_REQUEST    "reloadconfig.xsl"
#define LISTMOUNTS_RAW_REQUEST              "listmounts"
#define LISTMOUNTS_TRANSFORMED_REQUEST      "listmounts.xsl"
#define STREAMLIST_RAW_REQUEST              "streamlist"
#define STREAMLIST_TRANSFORMED_REQUEST      "streamlist.xsl"
#define STREAMLIST_PLAINTEXT_REQUEST        "streamlist.txt"
#define MOVECLIENTS_RAW_REQUEST             "moveclients"
#define MOVECLIENTS_TRANSFORMED_REQUEST     "moveclients.xsl"
#define KILLCLIENT_RAW_REQUEST              "killclient"
#define KILLCLIENT_TRANSFORMED_REQUEST      "killclient.xsl"
#define KILLSOURCE_RAW_REQUEST              "killsource"
#define KILLSOURCE_TRANSFORMED_REQUEST      "killsource.xsl"
#define ADMIN_XSL_RESPONSE                  "response.xsl"
#define MANAGEAUTH_RAW_REQUEST              "manageauth"
#define MANAGEAUTH_TRANSFORMED_REQUEST      "manageauth.xsl"
#define UPDATEMETADATA_RAW_REQUEST          "updatemetadata"
#define UPDATEMETADATA_TRANSFORMED_REQUEST  "updatemetadata.xsl"
#define DEFAULT_RAW_REQUEST                 ""
#define DEFAULT_TRANSFORMED_REQUEST         ""
#define BUILDM3U_RAW_REQUEST                "buildm3u"
96

97
typedef void (*request_function_ptr)(client_t *, source_t *, admin_format_t);
Marvin Scholz's avatar
Marvin Scholz committed
98 99 100 101 102 103 104 105

typedef struct admin_command_handler {
    const char                     *route;
    const int                       type;
    const int                       format;
    const request_function_ptr      function;
} admin_command_handler_t;

106 107 108 109 110 111 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_TRANSFORMED,    NULL }, /* for ACL framework */
    { FALLBACK_RAW_REQUEST,                 ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_fallback },
    { FALLBACK_TRANSFORMED_REQUEST,         ADMINTYPE_MOUNT,        ADMIN_FORMAT_TRANSFORMED,    command_fallback },
    { METADATA_RAW_REQUEST,                 ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_metadata },
    { METADATA_TRANSFORMED_REQUEST,         ADMINTYPE_MOUNT,        ADMIN_FORMAT_TRANSFORMED,    command_metadata },
    { SHOUTCAST_METADATA_REQUEST,           ADMINTYPE_MOUNT,        ADMIN_FORMAT_TRANSFORMED,    command_shoutcast_metadata },
    { LISTCLIENTS_RAW_REQUEST,              ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_show_listeners },
    { LISTCLIENTS_TRANSFORMED_REQUEST,      ADMINTYPE_MOUNT,        ADMIN_FORMAT_TRANSFORMED,    command_show_listeners },
    { STATS_RAW_REQUEST,                    ADMINTYPE_HYBRID,       ADMIN_FORMAT_RAW,            command_stats },
    { STATS_TRANSFORMED_REQUEST,            ADMINTYPE_HYBRID,       ADMIN_FORMAT_TRANSFORMED,    command_stats },
    { "stats.xml",                          ADMINTYPE_HYBRID,       ADMIN_FORMAT_RAW,            command_stats },
    { QUEUE_RELOAD_RAW_REQUEST,             ADMINTYPE_GENERAL,      ADMIN_FORMAT_RAW,            command_queue_reload },
    { QUEUE_RELOAD_TRANSFORMED_REQUEST,     ADMINTYPE_GENERAL,      ADMIN_FORMAT_TRANSFORMED,    command_queue_reload },
    { LISTMOUNTS_RAW_REQUEST,               ADMINTYPE_GENERAL,      ADMIN_FORMAT_RAW,            command_list_mounts },
    { LISTMOUNTS_TRANSFORMED_REQUEST,       ADMINTYPE_GENERAL,      ADMIN_FORMAT_TRANSFORMED,    command_list_mounts },
    { STREAMLIST_RAW_REQUEST,               ADMINTYPE_GENERAL,      ADMIN_FORMAT_RAW,            command_list_mounts },
    { STREAMLIST_PLAINTEXT_REQUEST,         ADMINTYPE_GENERAL,      ADMIN_FORMAT_PLAINTEXT,      command_list_mounts },
    { STREAMLIST_TRANSFORMED_REQUEST,       ADMINTYPE_GENERAL,      ADMIN_FORMAT_TRANSFORMED,    command_list_mounts },
    { MOVECLIENTS_RAW_REQUEST,              ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_move_clients },
    { MOVECLIENTS_TRANSFORMED_REQUEST,      ADMINTYPE_HYBRID,       ADMIN_FORMAT_TRANSFORMED,    command_move_clients },
    { KILLCLIENT_RAW_REQUEST,               ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_kill_client },
    { KILLCLIENT_TRANSFORMED_REQUEST,       ADMINTYPE_MOUNT,        ADMIN_FORMAT_TRANSFORMED,    command_kill_client },
    { KILLSOURCE_RAW_REQUEST,               ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_kill_source },
    { KILLSOURCE_TRANSFORMED_REQUEST,       ADMINTYPE_MOUNT,        ADMIN_FORMAT_TRANSFORMED,    command_kill_source },
    { MANAGEAUTH_RAW_REQUEST,               ADMINTYPE_GENERAL,      ADMIN_FORMAT_RAW,            command_manageauth },
    { MANAGEAUTH_TRANSFORMED_REQUEST,       ADMINTYPE_GENERAL,      ADMIN_FORMAT_TRANSFORMED,    command_manageauth },
    { UPDATEMETADATA_RAW_REQUEST,           ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_updatemetadata },
    { UPDATEMETADATA_TRANSFORMED_REQUEST,   ADMINTYPE_MOUNT,        ADMIN_FORMAT_TRANSFORMED,    command_updatemetadata },
    { BUILDM3U_RAW_REQUEST,                 ADMINTYPE_MOUNT,        ADMIN_FORMAT_RAW,            command_buildm3u },
    { DEFAULT_TRANSFORMED_REQUEST,          ADMINTYPE_HYBRID,       ADMIN_FORMAT_TRANSFORMED,    command_stats },
    { DEFAULT_RAW_REQUEST,                  ADMINTYPE_HYBRID,       ADMIN_FORMAT_TRANSFORMED,    command_stats }
Philipp Schafft's avatar
Philipp Schafft committed
152 153
};

Marvin Scholz's avatar
Marvin Scholz committed
154 155
#define HANDLERS_COUNT (sizeof(handlers)/sizeof(*handlers))

Marvin Scholz's avatar
Marvin Scholz committed
156 157
int admin_get_command(const char *command)
{
Philipp Schafft's avatar
Philipp Schafft committed
158 159
    size_t i;

Marvin Scholz's avatar
Marvin Scholz committed
160 161 162
    for (i = 0; i < HANDLERS_COUNT; i++)
        if (strcmp(handlers[i].route, command) == 0)
            return i;
Philipp Schafft's avatar
Philipp Schafft committed
163 164 165 166

    return COMMAND_ERROR;
}

Marvin Scholz's avatar
Marvin Scholz committed
167 168 169
/* Get the command handler for command or NULL
 */
const admin_command_handler_t* admin_get_handler(int command)
Marvin Scholz's avatar
Marvin Scholz committed
170
{
Marvin Scholz's avatar
Marvin Scholz committed
171 172
    if (command > 0 && command < HANDLERS_COUNT)
        return &handlers[command];
Philipp Schafft's avatar
Philipp Schafft committed
173

Marvin Scholz's avatar
Marvin Scholz committed
174 175 176 177 178 179 180 181 182
    return NULL;
}

/* Get the command type for command
 * If the command is invalid, ADMINTYPE_ERROR is returned.
 */
int admin_get_command_type(int command)
{
    const admin_command_handler_t* handler = admin_get_handler(command);
Philipp Schafft's avatar
Philipp Schafft committed
183

Marvin Scholz's avatar
Marvin Scholz committed
184 185
    if (handler != NULL)
        return handler->type;
Philipp Schafft's avatar
Philipp Schafft committed
186 187

    return ADMINTYPE_ERROR;
188 189
}

190 191 192
/* 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
193
xmlDocPtr admin_build_sourcelist(const char *mount)
194 195 196 197 198 199 200 201
{
    avl_node *node;
    source_t *source;
    xmlNodePtr xmlnode, srcnode;
    xmlDocPtr doc;
    char buf[22];
    time_t now = time(NULL);

202 203
    doc = xmlNewDoc (XMLSTR("1.0"));
    xmlnode = xmlNewDocNode (doc, NULL, XMLSTR("icestats"), NULL);
204
    xmlDocSetRootElement(doc, xmlnode);
205

206
    if (mount) {
207
        xmlNewTextChild (xmlnode, NULL, XMLSTR("current_source"), XMLSTR(mount));
208 209 210 211 212
    }

    node = avl_get_first(global.source_tree);
    while(node) {
        source = (source_t *)node->key;
213 214 215 216 217 218
        if (mount && strcmp (mount, source->mount) == 0)
        {
            node = avl_get_next (node);
            continue;
        }

219
        if (source->running || source->on_demand)
220
        {
221 222
            ice_config_t *config;
            mount_proxy *mountinfo;
Philipp Schafft's avatar
Philipp Schafft committed
223
            acl_t *acl = NULL;
224

225 226
            srcnode = xmlNewChild(xmlnode, NULL, XMLSTR("source"), NULL);
            xmlSetProp(srcnode, XMLSTR("mount"), XMLSTR(source->mount));
227

228
            xmlNewTextChild(srcnode, NULL, XMLSTR("fallback"),
229
                    (source->fallback_mount != NULL)?
230
                    XMLSTR(source->fallback_mount):XMLSTR(""));
231
            snprintf(buf, sizeof(buf), "%lu", source->listeners);
232
            xmlNewTextChild(srcnode, NULL, XMLSTR("listeners"), XMLSTR(buf));
233

Karl Heyes's avatar
Karl Heyes committed
234
            config = config_get_config();
Marvin Scholz's avatar
Marvin Scholz committed
235
            mountinfo = config_find_mount(config, source->mount, MOUNT_TYPE_NORMAL);
Philipp Schafft's avatar
Philipp Schafft committed
236
            if (mountinfo)
237
                acl = auth_stack_get_anonymous_acl(mountinfo->authstack, httpp_req_get);
Philipp Schafft's avatar
Philipp Schafft committed
238
            if (!acl)
239
                acl = auth_stack_get_anonymous_acl(config->authstack, httpp_req_get);
Philipp Schafft's avatar
Philipp Schafft committed
240
            if (acl && acl_test_web(acl) == ACL_POLICY_DENY) {
241
                xmlNewTextChild(srcnode, NULL, XMLSTR("authenticator"), XMLSTR("(dummy)"));
242
            }
Philipp Schafft's avatar
Philipp Schafft committed
243
            acl_release(acl);
244 245
            config_release_config();

Marvin Scholz's avatar
Marvin Scholz committed
246 247 248 249
            if (source->running) {
                if (source->client) {
                    snprintf(buf, sizeof(buf), "%lu",
                        (unsigned long)(now - source->con->con_time));
250
                    xmlNewTextChild(srcnode, NULL, XMLSTR("Connected"), XMLSTR(buf));
Karl Heyes's avatar
Karl Heyes committed
251
                }
252
                xmlNewTextChild(srcnode, NULL, XMLSTR("content-type"),
Marvin Scholz's avatar
Marvin Scholz committed
253
                    XMLSTR(source->format->contenttype));
254
            }
255
        }
256 257 258 259 260
        node = avl_get_next(node);
    }
    return(doc);
}

261 262 263 264
void admin_send_response(xmlDocPtr       doc,
                         client_t       *client,
                         admin_format_t  response,
                         const char     *xslt_template)
265
{
266
    if (response == ADMIN_FORMAT_RAW) {
267 268
        xmlChar *buff = NULL;
        int len = 0;
269 270 271
        size_t buf_len;
        ssize_t ret;

272
        xmlDocDumpMemory(doc, &buff, &len);
273 274 275 276

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

278 279
        client_set_queue(client, NULL);
        client->refbuf = refbuf_new(buf_len);
280

281 282 283
        ret = util_http_build_header(client->refbuf->data, buf_len, 0,
                                     0, 200, NULL,
                                     "text/xml", "utf-8",
284
                                     NULL, NULL, client);
Philipp Schafft's avatar
Philipp Schafft committed
285
        if (ret < 0) {
286
            ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
287
            client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
288 289
            xmlFree(buff);
            return;
Philipp Schafft's avatar
Philipp Schafft committed
290
        } else if (buf_len < (size_t)(len + ret + 64)) {
291 292 293 294 295 296 297 298 299 300
            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",
301
                                             NULL, NULL, client);
302 303
                if (ret == -1) {
                    ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
304
                    client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
305 306 307 308 309
                    xmlFree(buff);
                    return;
                }
            } else {
                ICECAST_LOG_ERROR("Client buffer reallocation failed. Dropping client.");
310
                client_send_error_by_id(client, ICECAST_ERROR_GEN_BUFFER_REALLOC);
311 312
                xmlFree(buff);
                return;
313
            }
314
        }
315

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

        client->refbuf->len = ret;
320 321 322
        xmlFree(buff);
        client->respcode = 200;
        fserve_add_client (client, NULL);
323
    }
324
    if (response == ADMIN_FORMAT_TRANSFORMED) {
325 326 327 328
        char *fullpath_xslt_template;
        int fullpath_xslt_template_len;
        ice_config_t *config = config_get_config();

329
        fullpath_xslt_template_len = strlen (config->adminroot_dir) +
330
            strlen (xslt_template) + 2;
331 332
        fullpath_xslt_template = malloc(fullpath_xslt_template_len);
        snprintf(fullpath_xslt_template, fullpath_xslt_template_len, "%s%s%s",
333
            config->adminroot_dir, PATH_SEPARATOR, xslt_template);
334
        config_release_config();
335

336
        ICECAST_LOG_DEBUG("Sending XSLT (%s)", fullpath_xslt_template);
337 338 339 340
        xslt_transform(doc, fullpath_xslt_template, client);
        free(fullpath_xslt_template);
    }
}
341

342
void admin_handle_request(client_t *client, const char *uri)
343
{
Marvin Scholz's avatar
Marvin Scholz committed
344 345 346
    const char *mount;
    const admin_command_handler_t* handler;
    source_t *source = NULL;
347

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

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

Marvin Scholz's avatar
Marvin Scholz committed
352 353 354 355
    /* Check if admin command is valid */
    if (handler == NULL) {
        ICECAST_LOG_ERROR("Error parsing command string or unrecognised command: %H",
                uri);
356
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_UNRECOGNISED_COMMAND);
357 358 359
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
360
    /* Check ACL */
Philipp Schafft's avatar
Philipp Schafft committed
361
    if (acl_test_admin(client->acl, client->admin_command) != ACL_POLICY_ALLOW) {
Marvin Scholz's avatar
Marvin Scholz committed
362 363

        /* ACL disallows, check exceptions */
364
        if ((handler->function == command_metadata && handler->format == ADMIN_FORMAT_RAW) &&
Philipp Schafft's avatar
Philipp Schafft committed
365 366
            (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
367 368
            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
369
        } else {
370
            client_send_error_by_id(client, ICECAST_ERROR_GEN_CLIENT_NEEDS_TO_AUTHENTICATE);
371 372
            return;
        }
373 374
    }

375 376
    mount = httpp_get_query_param(client->parser, "mount");

Marvin Scholz's avatar
Marvin Scholz committed
377
    /* Find mountpoint source */
378
    if(mount != NULL) {
379

Philipp Schafft's avatar
Philipp Schafft committed
380
        /* This is a mount request, handle it as such */
381
        avl_tree_rlock(global.source_tree);
Michael Smith's avatar
Michael Smith committed
382
        source = source_find_mount_raw(mount);
383

Marvin Scholz's avatar
Marvin Scholz committed
384
        /* No Source found */
Marvin Scholz's avatar
Marvin Scholz committed
385
        if (source == NULL) {
386
            avl_tree_unlock(global.source_tree);
Marvin Scholz's avatar
Marvin Scholz committed
387 388
            ICECAST_LOG_WARN("Admin command \"%H\" on non-existent source \"%H\"",
                    uri, mount);
389
            client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SOURCE_DOES_NOT_EXIST);
Marvin Scholz's avatar
Marvin Scholz committed
390 391 392
            return;
        } /* No Source running */
        else if (source->running == 0 && source->on_demand == 0) {
393
            avl_tree_unlock(global.source_tree);
Marvin Scholz's avatar
Marvin Scholz committed
394 395
            ICECAST_LOG_INFO("Received admin command \"%H\" on unavailable mount \"%H\"",
                    uri, mount);
396
            client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SOURCE_IS_NOT_AVAILABLE);
Marvin Scholz's avatar
Marvin Scholz committed
397
            return;
398
        }
Marvin Scholz's avatar
Marvin Scholz committed
399 400
        ICECAST_LOG_INFO("Received admin command %H on mount '%s'",
                    uri, mount);
401 402
    }

Marvin Scholz's avatar
Marvin Scholz committed
403
    if (handler->type == ADMINTYPE_MOUNT && !source) {
404
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_MISSING_PARAMETER);
Marvin Scholz's avatar
Marvin Scholz committed
405
        return;
406 407
    }

Marvin Scholz's avatar
Marvin Scholz committed
408 409 410
    handler->function(client, source, handler->format);
    if (source) {
        avl_tree_unlock(global.source_tree);
411
    }
Marvin Scholz's avatar
Marvin Scholz committed
412
    return;
413 414
}

415
static void html_success(client_t *client, char *message)
416
{
417 418
    ssize_t ret;

Marvin Scholz's avatar
Marvin Scholz committed
419 420
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
421
                                 "text/html", "utf-8",
422
                                 "", NULL, client);
423 424 425

    if (ret == -1 || ret >= PER_CLIENT_REFBUF_SIZE) {
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
426
        client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
427 428 429
        return;
    }

430
    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
431 432
        "<html><head><title>Admin request successful</title></head>"
        "<body><p>%s</p></body></html>", message);
433

434
    client->respcode = 200;
Marvin Scholz's avatar
Marvin Scholz committed
435 436
    client->refbuf->len = strlen(client->refbuf->data);
    fserve_add_client(client, NULL);
437 438
}

439

Marvin Scholz's avatar
Marvin Scholz committed
440 441
static void command_move_clients(client_t   *client,
                                 source_t   *source,
442
                                 admin_format_t response)
443
{
444
    const char *dest_source;
445
    source_t *dest;
446 447 448 449 450
    xmlDocPtr doc;
    xmlNodePtr node;
    char buf[255];
    int parameters_passed = 0;

451
    ICECAST_LOG_DEBUG("Doing optional check");
452
    if((COMMAND_OPTIONAL(client, "destination", dest_source))) {
453 454
        parameters_passed = 1;
    }
455
    ICECAST_LOG_DEBUG("Done optional check (%d)", parameters_passed);
456 457
    if (!parameters_passed) {
        doc = admin_build_sourcelist(source->mount);
458
        admin_send_response(doc, client, response,
459 460 461 462
             MOVECLIENTS_TRANSFORMED_REQUEST);
        xmlFreeDoc(doc);
        return;
    }
463

Marvin Scholz's avatar
Marvin Scholz committed
464
    dest = source_find_mount(dest_source);
465

Marvin Scholz's avatar
Marvin Scholz committed
466
    if (dest == NULL) {
467
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_NO_SUCH_DESTINATION);
468 469 470
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
471
    if (strcmp(dest->mount, source->mount) == 0) {
472
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SUPPLIED_MOUNTPOINTS_ARE_IDENTICAL);
473 474 475
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
476
    if (dest->running == 0 && dest->on_demand == 0) {
477
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_DEST_NOT_RUNNING);
478 479 480
        return;
    }

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

Marvin Scholz's avatar
Marvin Scholz committed
483
    doc = xmlNewDoc(XMLSTR("1.0"));
484
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
485 486
    xmlDocSetRootElement(doc, node);

Marvin Scholz's avatar
Marvin Scholz committed
487
    source_move_clients(source, dest);
488

Marvin Scholz's avatar
Marvin Scholz committed
489
    snprintf(buf, sizeof(buf), "Clients moved from %s to %s",
490
        source->mount, dest_source);
491 492
    xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
    xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
493

Marvin Scholz's avatar
Marvin Scholz committed
494
    admin_send_response(doc, client, response, ADMIN_XSL_RESPONSE);
495
    xmlFreeDoc(doc);
496 497
}

Marvin Scholz's avatar
Marvin Scholz committed
498 499 500 501 502
static inline xmlNodePtr __add_listener(client_t        *client,
                                        xmlNodePtr      parent,
                                        time_t          now,
                                        operation_mode  mode)
{
503 504 505 506
    const char *tmp;
    xmlNodePtr node;
    char buf[22];

Philipp Schafft's avatar
Philipp Schafft committed
507
    /* TODO: kh has support for a child node "lag". We should add that.
508 509
     * 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
510
     */
511 512 513 514 515 516 517

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

521
    xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "IP" : "ip"), XMLSTR(client->con->ip));
522 523 524

    tmp = httpp_getvar(client->parser, "user-agent");
    if (tmp)
525
        xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "UserAgent" : "useragent"), XMLSTR(tmp));
526 527 528

    tmp = httpp_getvar(client->parser, "referer");
    if (tmp)
529
        xmlNewTextChild(node, NULL, XMLSTR("referer"), XMLSTR(tmp));
530 531

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

    if (client->username)
535
        xmlNewTextChild(node, NULL, XMLSTR("username"), XMLSTR(client->username));
536

Philipp Schafft's avatar
Philipp Schafft committed
537
    if (client->role)
538
        xmlNewTextChild(node, NULL, XMLSTR("role"), XMLSTR(client->role));
Philipp Schafft's avatar
Philipp Schafft committed
539

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

542 543 544 545 546 547 548 549 550
    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;
    }

551 552 553
    return node;
}

Marvin Scholz's avatar
Marvin Scholz committed
554 555 556 557
void admin_add_listeners_to_mount(source_t          *source,
                                  xmlNodePtr        parent,
                                  operation_mode    mode)
{
558 559 560 561 562 563
    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) {
564
        __add_listener((client_t *)client_node->key, parent, now, mode);
565 566 567 568 569
        client_node = avl_get_next(client_node);
    }
    avl_tree_unlock(source->client_tree);
}

Marvin Scholz's avatar
Marvin Scholz committed
570 571
static void command_show_listeners(client_t *client,
                                   source_t *source,
572
                                   admin_format_t response)
573
{
574
    xmlDocPtr doc;
575
    xmlNodePtr node, srcnode;
576
    char buf[22];
577

578
    doc = xmlNewDoc(XMLSTR("1.0"));
579 580 581
    node = xmlNewDocNode(doc, NULL, XMLSTR("icestats"), NULL);
    srcnode = xmlNewChild(node, NULL, XMLSTR("source"), NULL);
    xmlSetProp(srcnode, XMLSTR("mount"), XMLSTR(source->mount));
582
    xmlDocSetRootElement(doc, node);
583

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

589
    admin_add_listeners_to_mount(source, srcnode, client->mode);
590

591
    admin_send_response(doc, client, response,
592 593
        LISTCLIENTS_TRANSFORMED_REQUEST);
    xmlFreeDoc(doc);
594 595
}

596
static void command_buildm3u(client_t *client, source_t *source, admin_format_t format)
597
{
Marvin Scholz's avatar
Marvin Scholz committed
598
    const char *mount = source->mount;
599 600
    const char *username = NULL;
    const char *password = NULL;
601
    ice_config_t *config;
602
    ssize_t ret;
603 604 605 606

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

Marvin Scholz's avatar
Marvin Scholz committed
607 608
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
609
                                 "audio/x-mpegurl", NULL,
610
                                 NULL, NULL, client);
611

Marvin Scholz's avatar
Marvin Scholz committed
612 613
    if (ret == -1 || ret >= (PER_CLIENT_REFBUF_SIZE - 512)) {
        /* we want at least 512 Byte left for data */
614
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
615
        client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
616 617 618 619
        return;
    }


Karl Heyes's avatar
Karl Heyes committed
620
    config = config_get_config();
621
    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
622
        "Content-Disposition: attachment; filename=listen.m3u\r\n\r\n"
623 624 625
        "http://%s:%s@%s:%d%s\r\n",
        username,
        password,
Karl Heyes's avatar
Karl Heyes committed
626 627
        config->hostname,
        config->port,
628
        mount
629
    );
Karl Heyes's avatar
Karl Heyes committed
630
    config_release_config();
631

632
    client->respcode = 200;
Karl Heyes's avatar
Karl Heyes committed
633 634
    client->refbuf->len = strlen (client->refbuf->data);
    fserve_add_client (client, NULL);
635
}
636

Marvin Scholz's avatar
Marvin Scholz committed
637 638
xmlNodePtr admin_add_role_to_authentication(auth_t *auth, xmlNodePtr parent)
{
639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657
    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;
}
658

659
static void command_manageauth(client_t *client, source_t *source, admin_format_t response)
Marvin Scholz's avatar
Marvin Scholz committed
660
{
661
    xmlDocPtr doc;
662
    xmlNodePtr node, rolenode, usersnode, msgnode;
663 664
    const char *action = NULL;
    const char *username = NULL;
665
    const char *idstring = NULL;
666 667
    char *message = NULL;
    int ret = AUTH_OK;
668
    int error_id = ICECAST_ERROR_ADMIN_missing_parameter;
669 670
    long unsigned int id;
    ice_config_t *config = config_get_config();
Philipp Schafft's avatar
Philipp Schafft committed
671
    auth_t *auth;
672

Marvin Scholz's avatar
Marvin Scholz committed
673
    do {
674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694
        /* 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);
695
            error_id = ICECAST_ERROR_ADMIN_ROLEMGN_ROLE_NOT_FOUND;
696
            break;
697
        }
Philipp Schafft's avatar
Philipp Schafft committed
698

699
        COMMAND_OPTIONAL(client, "action", action);
700
        COMMAND_OPTIONAL(client, "username", username);
701 702

        if (action == NULL)
703
            action = "list";
704

Marvin Scholz's avatar
Marvin Scholz committed
705
        if (!strcmp(action, "add")) {
706
            const char *password = NULL;
707
            COMMAND_OPTIONAL(client, "password", password);
708

709 710
            if (username == NULL || password == NULL) {
                ICECAST_LOG_WARN("manage auth request add for %lu but no user/pass", id);
711 712
                break;
            }
713 714

            if (!auth->adduser) {
715
                error_id = ICECAST_ERROR_ADMIN_ROLEMGN_ADD_NOSYS;
716 717 718
                break;
            }

Philipp Schafft's avatar
Philipp Schafft committed
719
            ret = auth->adduser(auth, username, password);
720 721
            if (ret == AUTH_FAILED) {
                message = strdup("User add failed - check the icecast error log");
722
            } else if (ret == AUTH_USERADDED) {
723
                message = strdup("User added");
724
            } else if (ret == AUTH_USEREXISTS) {
725 726 727
                message = strdup("User already exists - not added");
            }
        }
Marvin Scholz's avatar
Marvin Scholz committed
728
        if (!strcmp(action, "delete")) {
729 730 731 732 733 734
            if (username == NULL) {
                ICECAST_LOG_WARN("manage auth request delete for %lu but no username", id);
                break;
            }

            if (!auth->deleteuser) {
735
                error_id = ICECAST_ERROR_ADMIN_ROLEMGN_DELETE_NOSYS;
736 737
                break;
            }
738

Philipp Schafft's avatar
Philipp Schafft committed
739
            ret = auth->deleteuser(auth, username);
740 741
            if (ret == AUTH_FAILED) {
                message = strdup("User delete failed - check the icecast error log");
742
            } else if (ret == AUTH_USERDELETED) {
743 744 745 746
                message = strdup("User deleted");
            }
        }

747
        doc = xmlNewDoc(XMLSTR("1.0"));
748
        node = xmlNewDocNode(doc, NULL, XMLSTR("icestats"), NULL);
749

750
        rolenode = admin_add_role_to_authentication(auth, node);
751

752
        if (message) {
753
            msgnode = xmlNewChild(node, NULL, XMLSTR("iceresponse"), NULL);
754
            xmlNewTextChild(msgnode, NULL, XMLSTR("message"), XMLSTR(message));
755
        }
756

757
        xmlDocSetRootElement(doc, node);
758

759 760 761 762
        if (auth && auth->listuser) {
            usersnode = xmlNewChild(rolenode, NULL, XMLSTR("users"), NULL);
            auth->listuser(auth, usersnode);
        }
763

764 765
        config_release_config();
        auth_release(auth);
766

767
        admin_send_response(doc, client, response,
Marvin Scholz's avatar
Marvin Scholz committed
768 769
            MANAGEAUTH_TRANSFORMED_REQUEST);
        free(message);
770 771 772 773
        xmlFreeDoc(doc);
        return;
    } while (0);

774 775
    config_release_config();
    auth_release(auth);
776
    client_send_error_by_id(client, error_id);
777 778
}

Marvin Scholz's avatar
Marvin Scholz committed
779 780
static void command_kill_source(client_t *client,
                                source_t *source,
781
                                admin_format_t response)
782
{
783 784 785
    xmlDocPtr doc;
    xmlNodePtr node;

786 787
    doc = xmlNewDoc (XMLSTR("1.0"));
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
788 789
    xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR("Source Removed"));
    xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
790 791
    xmlDocSetRootElement(doc, node);

792 793
    source->running = 0;

794
    admin_send_response(doc, client, response,
795 796
        ADMIN_XSL_RESPONSE);
    xmlFreeDoc(doc);
797 798
}

Marvin Scholz's avatar
Marvin Scholz committed
799 800
static void command_kill_client(client_t *client,
                                source_t *source,
801
                                admin_format_t response)
802
{
803
    const char *idtext;
804 805
    int id;
    client_t *listener;
806 807 808
    xmlDocPtr doc;
    xmlNodePtr node;
    char buf[50] = "";
809 810 811 812 813 814 815

    COMMAND_REQUIRE(client, "id", idtext);

    id = atoi(idtext);

    listener = source_find_client(source, id);

Marvin Scholz's avatar
Marvin Scholz committed
816
    doc = xmlNewDoc(XMLSTR("1.0"));
817
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
818
    xmlDocSetRootElement(doc, node);
819
    ICECAST_LOG_DEBUG("Response is %d", response);
820

821
    if(listener != NULL) {
822
        ICECAST_LOG_INFO("Admin request: client %d removed", id);
823 824 825 826 827

        /* This tags it for removal on the next iteration of the main source
         * loop
         */
        listener->con->error = 1;
828 829
        memset(buf, '\000', sizeof(buf));
        snprintf(buf, sizeof(buf)-1, "Client %d removed", id);
830 831
        xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
        xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
832 833
    }
    else {
834 835
        memset(buf, '\000', sizeof(buf));
        snprintf(buf, sizeof(buf)-1, "Client %d not found", id);
836 837
        xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
        xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("0"));
838
    }
839
    admin_send_response(doc, client, response,
840 841
        ADMIN_XSL_RESPONSE);
    xmlFreeDoc(doc);
842 843
}

Marvin Scholz's avatar
Marvin Scholz committed
844 845
static void command_fallback(client_t *client,
                             source_t *source,
846
                             admin_format_t response)
847
{
848
    const char *fallback;
849 850
    char *old;

851
    ICECAST_LOG_DEBUG("Got fallback request");
852 853 854 855 856 857 858

    COMMAND_REQUIRE(client, "fallback", fallback);

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

859
    html_success(client, "Fallback configured");
860 861
}

Marvin Scholz's avatar
Marvin Scholz committed
862 863
static void command_metadata(client_t *client,
                             source_t *source,
864
                             admin_format_t response)
865
{
866
    const char *action;
867
    const char *song, *title, *artist, *charset;
868
    format_plugin_t *plugin;
869 870
    xmlDocPtr doc;
    xmlNodePtr node;
871
    int same_ip = 1;
872

873
    doc = xmlNewDoc(XMLSTR("1.0"));
Marvin Scholz's avatar
Marvin Scholz committed
874
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
875
    xmlDocSetRootElement(doc, node);
876

877
    ICECAST_LOG_DEBUG("Got metadata update request");
878

879
    if (source->parser && source->parser->req_type == httpp_req_put) {
Marvin Scholz's avatar
Marvin Scholz committed
880 881
        ICECAST_LOG_ERROR("Got legacy SOURCE-style metadata update command on "
            "source connected with PUT at mountpoint %s", source->mount);
Philipp Schafft's avatar
Philipp Schafft committed
882 883
    }

884
    COMMAND_REQUIRE(client, "mode", action);
885 886 887
    COMMAND_OPTIONAL(client, "song", song);
    COMMAND_OPTIONAL(client, "title", title);
    COMMAND_OPTIONAL(client, "artist", artist);
888
    COMMAND_OPTIONAL(client, "charset", charset);
889

Marvin Scholz's avatar
Marvin Scholz committed
890
    if (strcmp (action, "updinfo") != 0) {
891 892
        xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR("No such action"));
        xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("0"));
Marvin Scholz's avatar
Marvin Scholz committed
893
        admin_send_response(doc, client, response, ADMIN_XSL_RESPONSE);
894
        xmlFreeDoc(doc);
895 896
        return;
    }
897

898
    plugin = source->format;
Marvin Scholz's avatar
Marvin Scholz committed
899
    if (source->client && strcmp(client->con->ip, source->client->con->ip) != 0)
900
        if (response == ADMIN_FORMAT_RAW && acl_test_admin(client->acl, client->admin_command) != ACL_POLICY_ALLOW)
901
            same_ip = 0;
Karl Heyes's avatar
Karl Heyes committed
902

Marvin Scholz's avatar
Marvin Scholz committed
903 904
    if (same_ip && plugin && plugin->set_tag) {
        if (song) {
905
            plugin->set_tag (plugin, "song", song, charset);
906
            ICECAST_LOG_INFO("Metadata on mountpoint %s changed to \"%s\"", source->mount, song);
Marvin Scholz's avatar
Marvin Scholz committed
907 908 909 910
        } else {
            if (artist && title) {
                plugin->set_tag(plugin, "title", title, charset);
                plugin->set_tag(plugin, "artist", artist, charset);
911
                ICECAST_LOG_INFO("Metadata on mountpoint %s changed to \"%s - %s\"",
Marvin Scholz's avatar
Marvin Scholz committed
912
                    source->mount, artist, title);
913 914
            }
        }
915 916
        /* updates are now done, let them be pushed into the stream */
        plugin->set_tag (plugin, NULL, NULL, NULL);
Marvin Scholz's avatar
Marvin Scholz committed
917
    } else {
918
        xmlNewTextChild(node, NULL, XMLSTR("message"),
919
            XMLSTR("Mountpoint will not accept URL updates"));
920
        xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));