admin.c 40.1 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
    admin_format_t format;
348

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

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

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

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

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

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

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

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

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

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

409 410 411 412 413 414 415
    if (handler->format == ADMIN_FORMAT_AUTO) {
        format = client_get_admin_format_by_content_negotiation(client);
    } else {
        format = handler->format;
    }

    handler->function(client, source, format);
Marvin Scholz's avatar
Marvin Scholz committed
416 417
    if (source) {
        avl_tree_unlock(global.source_tree);
418
    }
Marvin Scholz's avatar
Marvin Scholz committed
419
    return;
420 421
}

422
static void html_success(client_t *client, char *message)
423
{
424 425
    ssize_t ret;

Marvin Scholz's avatar
Marvin Scholz committed
426 427
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
428
                                 "text/html", "utf-8",
429
                                 "", NULL, client);
430 431 432

    if (ret == -1 || ret >= PER_CLIENT_REFBUF_SIZE) {
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
433
        client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
434 435 436
        return;
    }

437
    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
438 439
        "<html><head><title>Admin request successful</title></head>"
        "<body><p>%s</p></body></html>", message);
440

441
    client->respcode = 200;
Marvin Scholz's avatar
Marvin Scholz committed
442 443
    client->refbuf->len = strlen(client->refbuf->data);
    fserve_add_client(client, NULL);
444 445
}

446

Marvin Scholz's avatar
Marvin Scholz committed
447 448
static void command_move_clients(client_t   *client,
                                 source_t   *source,
449
                                 admin_format_t response)
450
{
451
    const char *dest_source;
452
    source_t *dest;
453 454 455 456 457
    xmlDocPtr doc;
    xmlNodePtr node;
    char buf[255];
    int parameters_passed = 0;

458
    ICECAST_LOG_DEBUG("Doing optional check");
459
    if((COMMAND_OPTIONAL(client, "destination", dest_source))) {
460 461
        parameters_passed = 1;
    }
462
    ICECAST_LOG_DEBUG("Done optional check (%d)", parameters_passed);
463 464
    if (!parameters_passed) {
        doc = admin_build_sourcelist(source->mount);
465
        admin_send_response(doc, client, response,
466 467 468 469
             MOVECLIENTS_TRANSFORMED_REQUEST);
        xmlFreeDoc(doc);
        return;
    }
470

Marvin Scholz's avatar
Marvin Scholz committed
471
    dest = source_find_mount(dest_source);
472

Marvin Scholz's avatar
Marvin Scholz committed
473
    if (dest == NULL) {
474
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_NO_SUCH_DESTINATION);
475 476 477
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
478
    if (strcmp(dest->mount, source->mount) == 0) {
479
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_SUPPLIED_MOUNTPOINTS_ARE_IDENTICAL);
480 481 482
        return;
    }

Marvin Scholz's avatar
Marvin Scholz committed
483
    if (dest->running == 0 && dest->on_demand == 0) {
484
        client_send_error_by_id(client, ICECAST_ERROR_ADMIN_DEST_NOT_RUNNING);
485 486 487
        return;
    }

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

Marvin Scholz's avatar
Marvin Scholz committed
490
    doc = xmlNewDoc(XMLSTR("1.0"));
491
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
492 493
    xmlDocSetRootElement(doc, node);

Marvin Scholz's avatar
Marvin Scholz committed
494
    source_move_clients(source, dest);
495

Marvin Scholz's avatar
Marvin Scholz committed
496
    snprintf(buf, sizeof(buf), "Clients moved from %s to %s",
497
        source->mount, dest_source);
498 499
    xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR(buf));
    xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
500

Marvin Scholz's avatar
Marvin Scholz committed
501
    admin_send_response(doc, client, response, ADMIN_XSL_RESPONSE);
502
    xmlFreeDoc(doc);
503 504
}

Marvin Scholz's avatar
Marvin Scholz committed
505 506 507 508 509
static inline xmlNodePtr __add_listener(client_t        *client,
                                        xmlNodePtr      parent,
                                        time_t          now,
                                        operation_mode  mode)
{
510 511 512 513
    const char *tmp;
    xmlNodePtr node;
    char buf[22];

Philipp Schafft's avatar
Philipp Schafft committed
514
    /* TODO: kh has support for a child node "lag". We should add that.
515 516
     * 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
517
     */
518 519 520 521 522 523 524

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

528
    xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "IP" : "ip"), XMLSTR(client->con->ip));
529 530 531

    tmp = httpp_getvar(client->parser, "user-agent");
    if (tmp)
532
        xmlNewTextChild(node, NULL, XMLSTR(mode == OMODE_LEGACY ? "UserAgent" : "useragent"), XMLSTR(tmp));
533 534 535

    tmp = httpp_getvar(client->parser, "referer");
    if (tmp)
536
        xmlNewTextChild(node, NULL, XMLSTR("referer"), XMLSTR(tmp));
537 538

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

    if (client->username)
542
        xmlNewTextChild(node, NULL, XMLSTR("username"), XMLSTR(client->username));
543

Philipp Schafft's avatar
Philipp Schafft committed
544
    if (client->role)
545
        xmlNewTextChild(node, NULL, XMLSTR("role"), XMLSTR(client->role));
Philipp Schafft's avatar
Philipp Schafft committed
546

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

549 550 551 552 553 554 555 556 557
    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;
    }

558 559 560
    return node;
}

Marvin Scholz's avatar
Marvin Scholz committed
561 562 563 564
void admin_add_listeners_to_mount(source_t          *source,
                                  xmlNodePtr        parent,
                                  operation_mode    mode)
{
565 566 567 568 569 570
    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) {
571
        __add_listener((client_t *)client_node->key, parent, now, mode);
572 573 574 575 576
        client_node = avl_get_next(client_node);
    }
    avl_tree_unlock(source->client_tree);
}

Marvin Scholz's avatar
Marvin Scholz committed
577 578
static void command_show_listeners(client_t *client,
                                   source_t *source,
579
                                   admin_format_t response)
580
{
581
    xmlDocPtr doc;
582
    xmlNodePtr node, srcnode;
583
    char buf[22];
584

585
    doc = xmlNewDoc(XMLSTR("1.0"));
586 587 588
    node = xmlNewDocNode(doc, NULL, XMLSTR("icestats"), NULL);
    srcnode = xmlNewChild(node, NULL, XMLSTR("source"), NULL);
    xmlSetProp(srcnode, XMLSTR("mount"), XMLSTR(source->mount));
589
    xmlDocSetRootElement(doc, node);
590

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

596
    admin_add_listeners_to_mount(source, srcnode, client->mode);
597

598
    admin_send_response(doc, client, response,
599 600
        LISTCLIENTS_TRANSFORMED_REQUEST);
    xmlFreeDoc(doc);
601 602
}

603
static void command_buildm3u(client_t *client, source_t *source, admin_format_t format)
604
{
Marvin Scholz's avatar
Marvin Scholz committed
605
    const char *mount = source->mount;
606 607
    const char *username = NULL;
    const char *password = NULL;
608
    ice_config_t *config;
609
    ssize_t ret;
610 611 612 613

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

Marvin Scholz's avatar
Marvin Scholz committed
614 615
    ret = util_http_build_header(client->refbuf->data, PER_CLIENT_REFBUF_SIZE,
                                 0, 0, 200, NULL,
616
                                 "audio/x-mpegurl", NULL,
617
                                 NULL, NULL, client);
618

Marvin Scholz's avatar
Marvin Scholz committed
619 620
    if (ret == -1 || ret >= (PER_CLIENT_REFBUF_SIZE - 512)) {
        /* we want at least 512 Byte left for data */
621
        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
622
        client_send_error_by_id(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED);
623 624 625 626
        return;
    }


Karl Heyes's avatar
Karl Heyes committed
627
    config = config_get_config();
628
    snprintf(client->refbuf->data + ret, PER_CLIENT_REFBUF_SIZE - ret,
629
        "Content-Disposition: attachment; filename=listen.m3u\r\n\r\n"
630 631 632
        "http://%s:%s@%s:%d%s\r\n",
        username,
        password,
Karl Heyes's avatar
Karl Heyes committed
633 634
        config->hostname,
        config->port,
635
        mount
636
    );
Karl Heyes's avatar
Karl Heyes committed
637
    config_release_config();
638

639
    client->respcode = 200;
Karl Heyes's avatar
Karl Heyes committed
640 641
    client->refbuf->len = strlen (client->refbuf->data);
    fserve_add_client (client, NULL);
642
}
643

Marvin Scholz's avatar
Marvin Scholz committed
644 645
xmlNodePtr admin_add_role_to_authentication(auth_t *auth, xmlNodePtr parent)
{
646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664
    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;
}
665

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

Marvin Scholz's avatar
Marvin Scholz committed
680
    do {
681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701
        /* 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);
702
            error_id = ICECAST_ERROR_ADMIN_ROLEMGN_ROLE_NOT_FOUND;
703
            break;
704
        }
Philipp Schafft's avatar
Philipp Schafft committed
705

706
        COMMAND_OPTIONAL(client, "action", action);
707
        COMMAND_OPTIONAL(client, "username", username);
708 709

        if (action == NULL)
710
            action = "list";
711

Marvin Scholz's avatar
Marvin Scholz committed
712
        if (!strcmp(action, "add")) {
713
            const char *password = NULL;
714
            COMMAND_OPTIONAL(client, "password", password);
715

716 717
            if (username == NULL || password == NULL) {
                ICECAST_LOG_WARN("manage auth request add for %lu but no user/pass", id);
718 719
                break;
            }
720 721

            if (!auth->adduser) {
722
                error_id = ICECAST_ERROR_ADMIN_ROLEMGN_ADD_NOSYS;
723 724 725
                break;
            }

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

            if (!auth->deleteuser) {
742
                error_id = ICECAST_ERROR_ADMIN_ROLEMGN_DELETE_NOSYS;
743 744
                break;
            }
745

Philipp Schafft's avatar
Philipp Schafft committed
746
            ret = auth->deleteuser(auth, username);
747 748
            if (ret == AUTH_FAILED) {
                message = strdup("User delete failed - check the icecast error log");
749
            } else if (ret == AUTH_USERDELETED) {
750 751 752 753
                message = strdup("User deleted");
            }
        }

754
        doc = xmlNewDoc(XMLSTR("1.0"));
755
        node = xmlNewDocNode(doc, NULL, XMLSTR("icestats"), NULL);
756

757
        rolenode = admin_add_role_to_authentication(auth, node);
758

759
        if (message) {
760
            msgnode = xmlNewChild(node, NULL, XMLSTR("iceresponse"), NULL);
761
            xmlNewTextChild(msgnode, NULL, XMLSTR("message"), XMLSTR(message));
762
        }
763

764
        xmlDocSetRootElement(doc, node);
765

766 767 768 769
        if (auth && auth->listuser) {
            usersnode = xmlNewChild(rolenode, NULL, XMLSTR("users"), NULL);
            auth->listuser(auth, usersnode);
        }
770

771 772
        config_release_config();
        auth_release(auth);
773

774
        admin_send_response(doc, client, response,
Marvin Scholz's avatar
Marvin Scholz committed
775 776
            MANAGEAUTH_TRANSFORMED_REQUEST);
        free(message);
777 778 779 780
        xmlFreeDoc(doc);
        return;
    } while (0);

781 782
    config_release_config();
    auth_release(auth);
783
    client_send_error_by_id(client, error_id);
784 785
}

Marvin Scholz's avatar
Marvin Scholz committed
786 787
static void command_kill_source(client_t *client,
                                source_t *source,
788
                                admin_format_t response)
789
{
790 791 792
    xmlDocPtr doc;
    xmlNodePtr node;

793 794
    doc = xmlNewDoc (XMLSTR("1.0"));
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
795 796
    xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR("Source Removed"));
    xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("1"));
797 798
    xmlDocSetRootElement(doc, node);

799 800
    source->running = 0;

801
    admin_send_response(doc, client, response,
802 803
        ADMIN_XSL_RESPONSE);
    xmlFreeDoc(doc);
804 805
}

Marvin Scholz's avatar
Marvin Scholz committed
806 807
static void command_kill_client(client_t *client,
                                source_t *source,
808
                                admin_format_t response)
809
{
810
    const char *idtext;
811 812
    int id;
    client_t *listener;
813 814 815
    xmlDocPtr doc;
    xmlNodePtr node;
    char buf[50] = "";
816 817 818 819 820 821 822

    COMMAND_REQUIRE(client, "id", idtext);

    id = atoi(idtext);

    listener = source_find_client(source, id);

Marvin Scholz's avatar
Marvin Scholz committed
823
    doc = xmlNewDoc(XMLSTR("1.0"));
824
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
825
    xmlDocSetRootElement(doc, node);
826
    ICECAST_LOG_DEBUG("Response is %d", response);
827

828
    if(listener != NULL) {
829
        ICECAST_LOG_INFO("Admin request: client %d removed", id);
830 831 832 833 834

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

Marvin Scholz's avatar
Marvin Scholz committed
851 852
static void command_fallback(client_t *client,
                             source_t *source,
853
                             admin_format_t response)
854
{
855
    const char *fallback;
856 857
    char *old;

858
    ICECAST_LOG_DEBUG("Got fallback request");
859 860 861 862 863 864 865

    COMMAND_REQUIRE(client, "fallback", fallback);

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

866
    html_success(client, "Fallback configured");
867 868
}

Marvin Scholz's avatar
Marvin Scholz committed
869 870
static void command_metadata(client_t *client,
                             source_t *source,
871
                             admin_format_t response)
872
{
873
    const char *action;
874
    const char *song, *title, *artist, *charset;
875
    format_plugin_t *plugin;
876 877
    xmlDocPtr doc;
    xmlNodePtr node;
878
    int same_ip = 1;
879

880
    doc = xmlNewDoc(XMLSTR("1.0"));
Marvin Scholz's avatar
Marvin Scholz committed
881
    node = xmlNewDocNode(doc, NULL, XMLSTR("iceresponse"), NULL);
882
    xmlDocSetRootElement(doc, node);
883

884
    ICECAST_LOG_DEBUG("Got metadata update request");
885

886
    if (source->parser && source->parser->req_type == httpp_req_put) {
Marvin Scholz's avatar
Marvin Scholz committed
887 888
        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
889 890
    }

891
    COMMAND_REQUIRE(client, "mode", action);
892 893 894
    COMMAND_OPTIONAL(client, "song", song);
    COMMAND_OPTIONAL(client, "title", title);
    COMMAND_OPTIONAL(client, "artist", artist);
895
    COMMAND_OPTIONAL(client, "charset", charset);
896

Marvin Scholz's avatar
Marvin Scholz committed
897
    if (strcmp (action, "updinfo") != 0) {
898 899
        xmlNewTextChild(node, NULL, XMLSTR("message"), XMLSTR("No such action"));
        xmlNewTextChild(node, NULL, XMLSTR("return"), XMLSTR("0"));
Marvin Scholz's avatar
Marvin Scholz committed
900
        admin_send_response(doc, client, response, ADMIN_XSL_RESPONSE);
901
        xmlFreeDoc(doc);
902 903
        return;
    }
904

905
    plugin = source->format;
Marvin Scholz's avatar
Marvin Scholz committed
906
    if (source->client && strcmp(client->con->ip, source->client->con->ip) != 0)
907
        if (response == ADMIN_FORMAT_RAW && acl_test_admin(client->acl, client->admin_command) != ACL_POLICY_ALLOW)
908
            same_ip = 0;
Karl Heyes's avatar
Karl Heyes committed
909

Marvin Scholz's avatar
Marvin Scholz committed
910 911
    if (same_ip && plugin && plugin->set_tag) {
        if (song) {
912
            plugin->set_tag (plugin, "song", song, charset);
913
            ICECAST_LOG_INFO("Metadata on mountpoint %s changed to \"%s\"", source->mount, song);
Marvin Scholz's avatar
Marvin Scholz committed
914 915 916 917
        } else {
            if (artist && title) {
                plugin->set_tag(plugin, "title", title, charset);
                plugin->set_tag(plugin, "artist", artist, charset);
918
                ICECAST_LOG_INFO("Metadata on mountpoint %s changed to \"%s - %s\"",
Marvin Scholz's avatar
Marvin Scholz committed
919
                    source->mount, artist, title);
920 921
            }
        }
922 923
        /* 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
924
    } else {
925