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

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

18 19 20 21 22 23 24
#include <string.h>
#include <libxml/xmlmemory.h>
#include <libxml/debugXML.h>
#include <libxml/HTMLtree.h>
#include <libxml/xmlIO.h>
#include <libxml/xinclude.h>
#include <libxml/catalog.h>
25
#include <libxml/uri.h>
26 27 28 29
#include <libxslt/xslt.h>
#include <libxslt/xsltInternals.h>
#include <libxslt/transform.h>
#include <libxslt/xsltutils.h>
30
#include <libxslt/documents.h>
31

32 33 34 35
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>

36
#ifdef  HAVE_SYS_TIME_H
37 38 39
#include <sys/time.h>
#endif

40 41 42
#ifdef WIN32
#define snprintf _snprintf
#endif
43

44 45 46 47
#include "common/thread/thread.h"
#include "common/avl/avl.h"
#include "common/httpp/httpp.h"
#include "common/net/sock.h"
48

49
#include "xslt.h"
50 51
#include "refbuf.h"
#include "client.h"
52
#include "errors.h"
53
#include "stats.h"
54
#include "fserve.h"
Philipp Schafft's avatar
Philipp Schafft committed
55
#include "util.h"
56
#include "cfgfile.h"
57

58
#define CATMODULE "xslt"
59

60 61 62
#include "logging.h"

typedef struct {
63 64 65 66
    char *filename;
    time_t last_modified;
    time_t cache_age;
    xsltStylesheetPtr stylesheet;
67 68
} stylesheet_cache_t;

69 70 71 72 73 74 75
#ifndef HAVE_XSLTSAVERESULTTOSTRING
int xsltSaveResultToString(xmlChar **doc_txt_ptr, int * doc_txt_len, xmlDocPtr result, xsltStylesheetPtr style) {
    xmlOutputBufferPtr buf;

    *doc_txt_ptr = NULL;
    *doc_txt_len = 0;
    if (result->children == NULL)
76
        return (0);
77

78
    buf = xmlAllocOutputBuffer(NULL);
79 80

    if (buf == NULL)
81
        return (-1);
82 83
    xsltSaveResultTo(buf, result, style);
    if (buf->conv != NULL) {
84 85
        *doc_txt_len = buf->conv->use;
        *doc_txt_ptr = xmlStrndup(buf->conv->content, *doc_txt_len);
86
    } else {
87 88
        *doc_txt_len = buf->buffer->use;
        *doc_txt_ptr = xmlStrndup(buf->buffer->content, *doc_txt_len);
89
    }
90
    (void) xmlOutputBufferClose(buf);
91 92 93 94
    return 0;
}
#endif

95 96 97
/* Keep it small... */
#define CACHESIZE 3

98 99
static stylesheet_cache_t cache[CACHESIZE];
static mutex_t xsltlock;
100

101 102
/* Reference to the original xslt loader func */
static xsltDocLoaderFunc xslt_loader;
103 104
/* Admin URI cache */
static xmlChar *admin_URI = NULL;
105

106
void xslt_initialize(void)
107
{
108
    memset(cache, 0, sizeof(stylesheet_cache_t) * CACHESIZE);
109
    thread_mutex_create(&xsltlock);
110 111
    xmlInitParser();
    LIBXML_TEST_VERSION
112 113
    xmlSubstituteEntitiesDefault(1);
    xmlLoadExtDtdDefaultValue = 1;
114
    xslt_loader = xsltDocDefaultLoader;
115 116
}

117
void xslt_shutdown(void) {
118

119
    xslt_clear_cache();
120

121
    thread_mutex_destroy (&xsltlock);
122
    xmlCleanupParser();
123
    xsltCleanupGlobals();
124 125
    if (admin_URI)
        xmlFree(admin_URI);
126 127
}

128 129
static void clear_cache_entry(size_t idx) {
    free(cache[idx].filename);
130
    cache[idx].filename = NULL;
131 132
    if (cache[idx].stylesheet)
        xsltFreeStylesheet(cache[idx].stylesheet);
133
    cache[idx].stylesheet = NULL;
134 135 136 137 138 139 140 141 142 143 144 145 146
}

void xslt_clear_cache(void)
{
    size_t i;

    ICECAST_LOG_DEBUG("Clearing stylesheet cache.");

    thread_mutex_lock(&xsltlock);

    for (i = 0; i < CACHESIZE; i++)
        clear_cache_entry(i);

147 148 149 150 151
    if (admin_URI) {
        xmlFree(admin_URI);
        admin_URI = NULL;
    }

152 153 154
    thread_mutex_unlock(&xsltlock);
}

155
static int evict_cache_entry(void) {
brendan's avatar
brendan committed
156
    int i, age=0, oldest=0;
157 158 159 160 161 162 163 164

    for(i=0; i < CACHESIZE; i++) {
        if(cache[i].cache_age > age) {
            age = cache[i].cache_age;
            oldest = i;
        }
    }

165
    clear_cache_entry(oldest);
166 167 168 169

    return oldest;
}

170
static xsltStylesheetPtr xslt_get_stylesheet(const char *fn) {
171 172 173 174
    int i;
    int empty = -1;
    struct stat file;

175 176
    ICECAST_LOG_DEBUG("Looking up stylesheet file \"%s\".", fn);

Philipp Schafft's avatar
Philipp Schafft committed
177
    if (stat(fn, &file) != 0) {
178
        ICECAST_LOG_WARN("Error checking for stylesheet file \"%s\": %s", fn,
179
                strerror(errno));
180 181 182
        return NULL;
    }

Philipp Schafft's avatar
Philipp Schafft committed
183
    for (i = 0; i < CACHESIZE; i++) {
Philipp Schafft's avatar
Philipp Schafft committed
184
        if(cache[i].filename) {
185
#ifdef _WIN32
Philipp Schafft's avatar
Philipp Schafft committed
186
            if(!stricmp(fn, cache[i].filename)) {
187
#else
Philipp Schafft's avatar
Philipp Schafft committed
188
            if(!strcmp(fn, cache[i].filename)) {
189
#endif
Philipp Schafft's avatar
Philipp Schafft committed
190
                if(file.st_mtime > cache[i].last_modified) {
191
                    ICECAST_LOG_DEBUG("Source file newer than cached copy. Reloading slot %i", i);
192
                    xsltFreeStylesheet(cache[i].stylesheet);
193

194
                    cache[i].last_modified = file.st_mtime;
195
                    cache[i].stylesheet = xsltParseStylesheetFile(XMLSTR(fn));
196 197
                    cache[i].cache_age = time(NULL);
                }
198
                ICECAST_LOG_DEBUG("Using cached sheet %i", i);
199 200
                return cache[i].stylesheet;
            }
Philipp Schafft's avatar
Philipp Schafft committed
201
        } else {
202
            empty = i;
Philipp Schafft's avatar
Philipp Schafft committed
203
        }
204 205
    }

Philipp Schafft's avatar
Philipp Schafft committed
206
    if (empty >= 0) {
207
        i = empty;
208
        ICECAST_LOG_DEBUG("Using empty slot %i", i);
Philipp Schafft's avatar
Philipp Schafft committed
209
    } else {
210
        i = evict_cache_entry();
211
        ICECAST_LOG_DEBUG("Using evicted slot %i", i);
Philipp Schafft's avatar
Philipp Schafft committed
212
    }
213 214 215

    cache[i].last_modified = file.st_mtime;
    cache[i].filename = strdup(fn);
216
    cache[i].stylesheet = xsltParseStylesheetFile(XMLSTR(fn));
217
    cache[i].cache_age = time(NULL);
Philipp Schafft's avatar
Philipp Schafft committed
218

219 220
    return cache[i].stylesheet;
}
221

222 223 224 225 226 227 228 229
/* Custom xslt loader */
static xmlDocPtr custom_loader(const        xmlChar *URI,
                               xmlDictPtr   dict,
                               int          options,
                               void        *ctxt,
                               xsltLoadType type)
{
    xmlDocPtr ret;
230
    xmlChar *rel_URI, *fn, *final_URI = NULL;
231
    char *path_URI = NULL;
232
    xsltStylesheet *c;
233 234
    ice_config_t *config;

235 236 237
    switch (type) {
        /* In case an include is loaded */
        case XSLT_LOAD_STYLESHEET:
238 239 240 241 242 243
            /* URI is an escaped URI, make an unescaped version */
            path_URI = util_url_unescape((const char*)URI);
            /* Error if we can't unescape */
            if (path_URI == NULL)
                return NULL;

244
            /* Not look in admindir if the include file exists */
245 246
            if (access(path_URI, F_OK) == 0) {
                free(path_URI);
247
                break;
248 249
            }
            free(path_URI);
250

251 252 253 254
            c = (xsltStylesheet *) ctxt;
            /* Check if we actually have context/path */
            if (ctxt == NULL || c->doc->URL == NULL)
                break;
255

256
            /* Construct the right path */
257 258 259
            rel_URI = xmlBuildRelativeURI(URI, c->doc->URL);
            if (rel_URI != NULL && admin_URI != NULL) {
                fn = xmlBuildURI(rel_URI, admin_URI);
260
                final_URI = fn;
261
                xmlFree(rel_URI);
262
            }
263

264 265
            /* Fail if there was an error constructing the path */
            if (final_URI == NULL) {
266 267
                if (rel_URI)
                    xmlFree(rel_URI);
268
                return NULL;
269 270
            }
        break;
271

272 273
        /* In case a top stylesheet is loaded */
        case XSLT_LOAD_START:
274 275 276 277
            /* Check if the admin URI is already cached */
            if (admin_URI != NULL) {
                break;
            }
278

279
            config = config_get_config();
280
                /* Append path separator to path */
281
                size_t len = strlen(config->adminroot_dir);
282
                xmlChar* admin_path = xmlMalloc(len+2);
283
                xmlStrPrintf(admin_path, len+2, XMLSTR("%s/"), XMLSTR(config->adminroot_dir));
284

285 286 287
                /* Convert admin path to URI */
                admin_URI = xmlPathToURI(admin_path);
                xmlFree(admin_path);
288

289 290 291 292 293
                if (!admin_URI) {
                    return NULL;
                } else {
                    ICECAST_LOG_DEBUG("Loaded and cached admin_URI \"%s\"", admin_URI);
                }
294 295 296
            config_release_config();
        break;

297
        /* Avoid warnings about other events we don't care for */
298 299 300
        default:
        break;
    }
301

302
    /* Get the actual xmlDoc */
303
    if (final_URI) {
304
        ICECAST_LOG_DEBUG("Calling xslt_loader() for \"%s\" (was: \"%s\").", final_URI, URI);
305 306 307
        ret = xslt_loader(final_URI, dict, options, ctxt, type);
        xmlFree(final_URI);
    } else {
308
        ICECAST_LOG_DEBUG("Calling xslt_loader() for \"%s\".", URI);
309 310
        ret = xslt_loader(URI, dict, options, ctxt, type);
    }
311 312 313
    return ret;
}

314 315 316 317 318 319 320 321 322
static inline void _send_error(client_t *client, icecast_error_id_t id, int old_status) {
    if (old_status >= 400) {
        client_send_error_by_id(client, ICECAST_ERROR_RECURSIVE_ERROR);
        return;
    }

    client_send_error_by_id(client, id);
}

323
void xslt_transform(xmlDocPtr doc, const char *xslfilename, client_t *client, int status, const char *location)
324
{
325
    xmlDocPtr res;
326
    xsltStylesheetPtr cur;
327
    xmlChar *string;
328
    int len, problem = 0;
329
    const char *mediatype = NULL;
330
    const char *charset = NULL;
331

332 333
    xmlSetGenericErrorFunc("", log_parse_failure);
    xsltSetGenericErrorFunc("", log_parse_failure);
334
    xsltSetLoaderFunc(custom_loader);
335

336 337 338
    thread_mutex_lock(&xsltlock);
    cur = xslt_get_stylesheet(xslfilename);

339 340
    if (cur == NULL)
    {
341
        thread_mutex_unlock(&xsltlock);
342
        ICECAST_LOG_ERROR("problem reading stylesheet \"%s\"", xslfilename);
343
        _send_error(client, ICECAST_ERROR_XSLT_PARSE, status);
344
        return;
345
    }
346

347
    res = xsltApplyStylesheet(cur, doc, NULL);
348 349 350 351
    if (res != NULL) {
        if (xsltSaveResultToString(&string, &len, res, cur) < 0)
            problem = 1;
    } else {
352
        problem = 1;
353
    }
354

355 356 357 358
    /* lets find out the content type and character encoding to use */
    if (cur->encoding)
       charset = (char *)cur->encoding;

359 360 361 362 363
    if (cur->mediaType)
        mediatype = (char *)cur->mediaType;
    else
    {
        /* check method for the default, a missing method assumes xml */
364
        if (cur->method && xmlStrcmp (cur->method, XMLSTR("html")) == 0)
365 366
            mediatype = "text/html";
        else
367
            if (cur->method && xmlStrcmp (cur->method, XMLSTR("text")) == 0)
368 369 370 371
                mediatype = "text/plain";
            else
                mediatype = "text/xml";
    }
372
    if (problem == 0)
373
    {
374 375 376
        ssize_t ret;
        int failed = 0;
        refbuf_t *refbuf;
377
        size_t location_length = 0;
378
        ssize_t full_len = strlen(mediatype) + (ssize_t)len + (ssize_t)1024;
379 380 381 382 383 384

        if (location) {
            location_length = strlen(location);
            full_len += location_length;
        }

385 386
        if (full_len < 4096)
            full_len = 4096;
387
        refbuf = refbuf_new (full_len);
388

389
        if (string == NULL)
390
            string = xmlCharStrdup ("");
391
        ret = util_http_build_header(refbuf->data, full_len, 0, 0, status, NULL, mediatype, charset, NULL, NULL, client);
392 393
        if (ret == -1) {
            ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
394
            _send_error(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED, status);
395
        } else {
396
            if ( full_len < (ret + (ssize_t)len + (ssize_t)128) ) {
397
                void *new_data;
398
                full_len = ret + (ssize_t)len + (ssize_t)128;
399 400 401 402 403
                new_data = realloc(refbuf->data, full_len);
                if (new_data) {
                    ICECAST_LOG_DEBUG("Client buffer reallocation succeeded.");
                    refbuf->data = new_data;
                    refbuf->len = full_len;
404
                    ret = util_http_build_header(refbuf->data, full_len, 0, 0, status, NULL, mediatype, charset, NULL, NULL, client);
405 406
                    if (ret == -1) {
                        ICECAST_LOG_ERROR("Dropping client as we can not build response headers.");
407
                        _send_error(client, ICECAST_ERROR_GEN_HEADER_GEN_FAILED, status);
408 409 410 411
                        failed = 1;
                    }
                } else {
                    ICECAST_LOG_ERROR("Client buffer reallocation failed. Dropping client.");
412
                    _send_error(client, ICECAST_ERROR_GEN_BUFFER_REALLOC, status);
413 414 415 416 417
                    failed = 1;
                }
            }

            if (!failed) {
418 419 420 421 422
                /* FIXME: in this section we hope no function will ever return -1 */
                if (location) {
                    ret += snprintf(refbuf->data + ret, full_len - ret, "Location: %s\r\n", location);
                }
                ret += snprintf(refbuf->data + ret, full_len - ret, "Content-Length: %d\r\n\r\n%s", len, string);
423

424
                client->respcode = status;
425 426 427 428 429 430
                client_set_queue (client, NULL);
                client->refbuf = refbuf;
                refbuf->len = strlen (refbuf->data);
                fserve_add_client (client, NULL);
            }
        }
431 432
        xmlFree (string);
    }
433 434
    else
    {
435
        ICECAST_LOG_WARN("problem applying stylesheet \"%s\"", xslfilename);
436
        _send_error(client, ICECAST_ERROR_XSLT_problem, status);
437
    }
438
    thread_mutex_unlock (&xsltlock);
439 440 441
    xmlFreeDoc(res);
}