/* * Copyright (C) 2015 Espen Jürgensen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include "http.h" #include "logger.h" #include "misc.h" /* ======================= libevent HTTP client =============================*/ // Number of seconds the client will wait for a response before aborting #define HTTP_CLIENT_TIMEOUT 8 /* The strict libevent api does not permit walking through an evkeyvalq and saving * all the http headers, so we predefine what we are looking for. You can add * extra headers here that you would like to save. */ static char *header_list[] = { "icy-name", "icy-description", "icy-metaint", "icy-genre", "Content-Type", }; /* Copies headers we are searching for from one keyval struct to another * */ static void headers_save(struct keyval *kv, struct evkeyvalq *headers) { const char *value; uint8_t *utf; int i; if (!kv || !headers) return; for (i = 0; i < (sizeof(header_list) / sizeof(header_list[0])); i++) if ( (value = evhttp_find_header(headers, header_list[i])) ) { utf = u8_strconv_from_encoding(value, "ISO−8859−1", iconveh_question_mark); if (!utf) continue; keyval_add(kv, header_list[i], (char *)utf); free(utf); } } static void request_cb(struct evhttp_request *req, void *arg) { struct http_client_ctx *ctx; const char *response_code_line; int response_code; ctx = (struct http_client_ctx *)arg; if (ctx->headers_only) { ctx->ret = 0; event_base_loopbreak(ctx->evbase); return; } if (!req) { DPRINTF(E_WARN, L_HTTP, "Connection to %s failed: Connection timed out\n", ctx->url); goto connection_error; } response_code = evhttp_request_get_response_code(req); #ifndef HAVE_LIBEVENT2_OLD response_code_line = evhttp_request_get_response_code_line(req); #else response_code_line = "no error text"; #endif if (response_code == 0) { DPRINTF(E_WARN, L_HTTP, "Connection to %s failed: Connection refused\n", ctx->url); goto connection_error; } else if (response_code != 200) { DPRINTF(E_WARN, L_HTTP, "Connection to %s failed: %s (error %d)\n", ctx->url, response_code_line, response_code); goto connection_error; } ctx->ret = 0; if (ctx->headers) headers_save(ctx->headers, evhttp_request_get_input_headers(req)); if (ctx->body) evbuffer_add_buffer(ctx->body, evhttp_request_get_input_buffer(req)); event_base_loopbreak(ctx->evbase); return; connection_error: ctx->ret = -1; event_base_loopbreak(ctx->evbase); return; } /* This callback is only invoked if ctx->headers_only is set. Since that means * we only want headers, it will always return -1 to make evhttp close the * connection. The headers will be saved in a keyval struct in ctx, since we * cannot address the *evkeyvalq after the connection is free'd. */ #ifndef HAVE_LIBEVENT2_OLD static int request_header_cb(struct evhttp_request *req, void *arg) { struct http_client_ctx *ctx; ctx = (struct http_client_ctx *)arg; if (!ctx->headers) { DPRINTF(E_LOG, L_HTTP, "BUG: Header callback invoked but caller did not say where to save the headers\n"); return -1; } headers_save(ctx->headers, evhttp_request_get_input_headers(req)); return -1; } #endif int http_client_request(struct http_client_ctx *ctx) { struct evhttp_connection *evcon; struct evhttp_request *req; struct evkeyvalq *headers; char hostname[PATH_MAX]; char path[PATH_MAX]; char s[PATH_MAX]; int port; int ret; ctx->ret = -1; av_url_split(NULL, 0, NULL, 0, hostname, sizeof(hostname), &port, path, sizeof(path), ctx->url); if (strlen(hostname) == 0) { DPRINTF(E_LOG, L_HTTP, "Error extracting hostname from URL: %s\n", ctx->url); return ctx->ret; } if (port <= 0) snprintf(s, PATH_MAX, "%s", hostname); else snprintf(s, PATH_MAX, "%s:%d", hostname, port); if (port <= 0) port = 80; if (strlen(path) == 0) { path[0] = '/'; path[1] = '\0'; } ctx->evbase = event_base_new(); if (!ctx->evbase) { DPRINTF(E_LOG, L_HTTP, "Could not create or find http client event base\n"); return ctx->ret; } evcon = evhttp_connection_base_new(ctx->evbase, NULL, hostname, (unsigned short)port); if (!evcon) { DPRINTF(E_LOG, L_HTTP, "Could not create connection to %s\n", hostname); event_base_free(ctx->evbase); return ctx->ret; } evhttp_connection_set_timeout(evcon, HTTP_CLIENT_TIMEOUT); /* Set up request */ req = evhttp_request_new(request_cb, ctx); if (!req) { DPRINTF(E_LOG, L_HTTP, "Could not create request to %s\n", hostname); evhttp_connection_free(evcon); event_base_free(ctx->evbase); return ctx->ret; } #ifndef HAVE_LIBEVENT2_OLD if (ctx->headers_only) evhttp_request_set_header_cb(req, request_header_cb); #endif headers = evhttp_request_get_output_headers(req); evhttp_add_header(headers, "Host", s); evhttp_add_header(headers, "Content-Length", "0"); evhttp_add_header(headers, "User-Agent", "forked-daapd/" VERSION); evhttp_add_header(headers, "Icy-MetaData", "1"); /* Make request */ DPRINTF(E_INFO, L_HTTP, "Making request for http://%s%s\n", s, path); ret = evhttp_make_request(evcon, req, EVHTTP_REQ_GET, path); if (ret < 0) { DPRINTF(E_LOG, L_HTTP, "Error making request for http://%s%s\n", s, path); evhttp_connection_free(evcon); event_base_free(ctx->evbase); return ctx->ret; } event_base_dispatch(ctx->evbase); evhttp_connection_free(evcon); event_base_free(ctx->evbase); return ctx->ret; } int http_stream_setup(char **stream, const char *url) { struct http_client_ctx ctx; struct evbuffer *evbuf; const char *ext; char *line; int ret; int n; *stream = NULL; ext = strrchr(url, '.'); if (strcasecmp(ext, ".m3u") != 0) { *stream = strdup(url); return 0; } // It was a m3u playlist, so now retrieve it memset(&ctx, 0, sizeof(struct http_client_ctx)); evbuf = evbuffer_new(); if (!evbuf) return -1; ctx.url = url; ctx.body = evbuf; ret = http_client_request(&ctx); if (ret < 0) { DPRINTF(E_LOG, L_HTTP, "Couldn't fetch internet playlist: %s\n", url); evbuffer_free(evbuf); return -1; } // Pad with CRLF because evbuffer_readln() might not read the last line otherwise evbuffer_add(ctx.body, "\r\n", 2); /* Read the playlist until the first stream link is found, but give up if * nothing is found in the first 10 lines */ n = 0; while ((line = evbuffer_readln(ctx.body, NULL, EVBUFFER_EOL_ANY)) && (n < 10)) { n++; if (strncasecmp(line, "http://", strlen("http://")) == 0) { DPRINTF(E_DBG, L_HTTP, "Found internet playlist stream (line %d): %s\n", n, line); n = -1; break; } free(line); } evbuffer_free(ctx.body); if (n != -1) { DPRINTF(E_LOG, L_HTTP, "Couldn't find stream in internet playlist: %s\n", url); return -1; } *stream = line; return 0; } /* ======================= ICY metadata handling =============================*/ #if LIBAVFORMAT_VERSION_MAJOR >= 56 || (LIBAVFORMAT_VERSION_MAJOR == 55 && LIBAVFORMAT_VERSION_MINOR >= 13) static int metadata_packet_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx) { uint8_t *buffer; char *icy_token; char *ptr; char *end; av_opt_get(fmtctx, "icy_metadata_packet", AV_OPT_SEARCH_CHILDREN, &buffer); if (!buffer) return -1; icy_token = strtok((char *)buffer, ";"); while (icy_token != NULL) { ptr = strchr(icy_token, '='); if (!ptr || (ptr[1] == '\0')) { icy_token = strtok(NULL, ";"); continue; } ptr++; if (ptr[0] == '\'') ptr++; end = strrchr(ptr, '\''); if (end) *end = '\0'; if ((strncmp(icy_token, "StreamTitle", strlen("StreamTitle")) == 0) && !metadata->title) { metadata->title = ptr; /* Dash separates artist from title, if no dash assume all is title */ ptr = strstr(ptr, " - "); if (ptr) { *ptr = '\0'; metadata->artist = strdup(metadata->title); *ptr = ' '; metadata->title = strdup(ptr + 3); } else metadata->title = strdup(metadata->title); } else if ((strncmp(icy_token, "StreamUrl", strlen("StreamUrl")) == 0) && !metadata->artwork_url) { metadata->artwork_url = strdup(ptr); } if (end) *end = '\''; icy_token = strtok(NULL, ";"); } av_free(buffer); if (metadata->title) metadata->hash = djb_hash(metadata->title, strlen(metadata->title)); return 0; } static int metadata_header_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx) { uint8_t *buffer; uint8_t *utf; char *icy_token; char *ptr; av_opt_get(fmtctx, "icy_metadata_headers", AV_OPT_SEARCH_CHILDREN, &buffer); if (!buffer) return -1; /* Headers are ascii or iso-8859-1 according to: * http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2 */ utf = u8_strconv_from_encoding((char *)buffer, "ISO−8859−1", iconveh_question_mark); av_free(buffer); if (!utf) return -1; icy_token = strtok((char *)utf, "\r\n"); while (icy_token != NULL) { ptr = strchr(icy_token, ':'); if (!ptr || (ptr[1] == '\0')) { icy_token = strtok(NULL, "\r\n"); continue; } ptr++; if (ptr[0] == ' ') ptr++; if ((strncmp(icy_token, "icy-name", strlen("icy-name")) == 0) && !metadata->name) metadata->name = strdup(ptr); else if ((strncmp(icy_token, "icy-description", strlen("icy-description")) == 0) && !metadata->description) metadata->description = strdup(ptr); else if ((strncmp(icy_token, "icy-genre", strlen("icy-genre")) == 0) && !metadata->genre) metadata->genre = strdup(ptr); icy_token = strtok(NULL, "\r\n"); } free(utf); return 0; } struct http_icy_metadata * http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) { struct http_icy_metadata *metadata; int got_packet; int got_header; metadata = malloc(sizeof(struct http_icy_metadata)); if (!metadata) return NULL; memset(metadata, 0, sizeof(struct http_icy_metadata)); got_packet = (metadata_packet_get(metadata, fmtctx) == 0); got_header = (!packet_only) && (metadata_header_get(metadata, fmtctx) == 0); if (!got_packet && !got_header) { free(metadata); return NULL; } /* DPRINTF(E_DBG, L_HTTP, "Found ICY: N %s, D %s, G %s, T %s, A %s, U %s, I %" PRIu32 "\n", metadata->name, metadata->description, metadata->genre, metadata->title, metadata->artist, metadata->artwork_url, metadata->hash ); */ return metadata; } #elif defined(HAVE_LIBEVENT2_OLD) struct http_icy_metadata * http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) { DPRINTF(E_INFO, L_HTTP, "Skipping Shoutcast metadata request for %s (requires libevent>=2.1.4 or libav 10)\n", fmtctx->filename); return NULL; } #else /* Earlier versions of ffmpeg/libav do not seem to allow access to the http * headers, so we must instead open the stream ourselves to get the metadata. * Sorry about the extra connections, you radio streaming people! * * It is not possible to get the packet metadata with these versions of ffmpeg */ struct http_icy_metadata * http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) { struct http_icy_metadata *metadata; struct http_client_ctx ctx; struct keyval *kv; const char *value; int got_header; int ret; /* Can only get header metadata */ if (packet_only) return NULL; kv = keyval_alloc(); if (!kv) return NULL; memset(&ctx, 0, sizeof(struct http_client_ctx)); ctx.url = fmtctx->filename; ctx.headers = kv; ctx.headers_only = 1; ctx.body = NULL; ret = http_client_request(&ctx); if (ret < 0) { DPRINTF(E_LOG, L_HTTP, "Error fetching %s\n", fmtctx->filename); free(kv); return NULL; } metadata = malloc(sizeof(struct http_icy_metadata)); if (!metadata) return NULL; memset(metadata, 0, sizeof(struct http_icy_metadata)); got_header = 0; if ( (value = keyval_get(ctx.headers, "icy-name")) ) { metadata->name = strdup(value); got_header = 1; } if ( (value = keyval_get(ctx.headers, "icy-description")) ) { metadata->description = strdup(value); got_header = 1; } if ( (value = keyval_get(ctx.headers, "icy-genre")) ) { metadata->genre = strdup(value); got_header = 1; } keyval_clear(kv); free(kv); if (!got_header) { free(metadata); return NULL; } /* DPRINTF(E_DBG, L_HTTP, "Found ICY: N %s, D %s, G %s, T %s, A %s, U %s, I %" PRIu32 "\n", metadata->name, metadata->description, metadata->genre, metadata->title, metadata->artist, metadata->artwork_url, metadata->hash );*/ return metadata; } #endif void http_icy_metadata_free(struct http_icy_metadata *metadata, int content_only) { if (metadata->name) free(metadata->name); if (metadata->description) free(metadata->description); if (metadata->genre) free(metadata->genre); if (metadata->title) free(metadata->title); if (metadata->artist) free(metadata->artist); if (metadata->artwork_url) free(metadata->artwork_url); if (!content_only) free(metadata); }