owntone-server/src/http.c
ejurgensen 83b8a4eb3f [http] Use curl for URL parsing instead of depending on httpd
Makes it easier to make the httpd parsing internal.
2023-03-07 21:01:10 +01:00

571 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (C) 2016 Espen Jürgensen <espenjurgensen@gmail.com>
*
* 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 <config.h>
#endif
#include <stdio.h>
#include <unistd.h>
#include <uniconv.h>
#include <unistr.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <limits.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <libavutil/opt.h>
#include <event2/event.h>
#include <curl/curl.h>
#include "http.h"
#include "logger.h"
#include "misc.h"
#include "conffile.h"
/* Formats we can read so far */
#define PLAYLIST_UNK 0
#define PLAYLIST_PLS 1
#define PLAYLIST_M3U 2
/* ======================= libevent HTTP client =============================*/
// Number of seconds the client will wait for a response before aborting
#define HTTP_CLIENT_TIMEOUT 8
void
http_client_session_init(struct http_client_session *session)
{
session->curl = curl_easy_init();
}
void
http_client_session_deinit(struct http_client_session *session)
{
curl_easy_cleanup(session->curl);
}
static void
curl_headers_save(struct keyval *kv, CURL *curl)
{
char *content_type;
int ret;
if (!kv || !curl)
return;
ret = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &content_type);
if (ret == CURLE_OK && content_type)
{
keyval_add(kv, "Content-Type", content_type);
}
}
static size_t
curl_request_cb(char *ptr, size_t size, size_t nmemb, void *userdata)
{
size_t realsize;
struct http_client_ctx *ctx;
int ret;
realsize = size * nmemb;
ctx = (struct http_client_ctx *)userdata;
if (!ctx->input_body)
return realsize;
ret = evbuffer_add(ctx->input_body, ptr, realsize);
if (ret < 0)
{
DPRINTF(E_LOG, L_HTTP, "Error adding reply from %s to input buffer\n", ctx->url);
return 0;
}
return realsize;
}
int
http_client_request(struct http_client_ctx *ctx, struct http_client_session *session)
{
CURL *curl;
CURLcode res;
struct curl_slist *headers;
struct onekeyval *okv;
const char *user_agent;
long verifypeer;
char header[1024];
long response_code;
if (session)
{
curl = session->curl;
curl_easy_reset(curl);
}
else
{
curl = curl_easy_init();
}
if (!curl)
{
DPRINTF(E_LOG, L_HTTP, "Error: Could not get curl handle\n");
return -1;
}
user_agent = cfg_getstr(cfg_getsec(cfg, "general"), "user_agent");
curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent);
curl_easy_setopt(curl, CURLOPT_URL, ctx->url);
verifypeer = cfg_getbool(cfg_getsec(cfg, "general"), "ssl_verifypeer");
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, verifypeer);
headers = NULL;
if (ctx->output_headers)
{
for (okv = ctx->output_headers->head; okv; okv = okv->next)
{
snprintf(header, sizeof(header), "%s: %s", okv->name, okv->value);
headers = curl_slist_append(headers, header);
}
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
}
if (ctx->headers_only)
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); // Makes curl make a HEAD request
else if (ctx->output_body)
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, ctx->output_body); // POST request
curl_easy_setopt(curl, CURLOPT_TIMEOUT, HTTP_CLIENT_TIMEOUT);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_request_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, ctx);
// Artwork and playlist requests might require redirects
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5);
/* Make request */
DPRINTF(E_INFO, L_HTTP, "Making request for %s\n", ctx->url);
res = curl_easy_perform(curl);
if (res != CURLE_OK)
{
DPRINTF(E_LOG, L_HTTP, "Request to %s failed: %s\n", ctx->url, curl_easy_strerror(res));
curl_slist_free_all(headers);
if (!session)
{
curl_easy_cleanup(curl);
}
return -1;
}
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
ctx->response_code = (int) response_code;
curl_headers_save(ctx->input_headers, curl);
curl_slist_free_all(headers);
if (!session)
{
curl_easy_cleanup(curl);
}
return 0;
}
char *
http_form_urlencode(struct keyval *kv)
{
struct evbuffer *evbuf;
struct onekeyval *okv;
char *body;
char *k;
char *v;
evbuf = evbuffer_new();
for (okv = kv->head; okv; okv = okv->next)
{
k = evhttp_encode_uri(okv->name);
if (!k)
continue;
v = evhttp_encode_uri(okv->value);
if (!v)
{
free(k);
continue;
}
evbuffer_add_printf(evbuf, "%s=%s", k, v);
if (okv->next)
evbuffer_add_printf(evbuf, "&");
free(k);
free(v);
}
evbuffer_add(evbuf, "\n", 1);
body = evbuffer_readln(evbuf, NULL, EVBUFFER_EOL_ANY);
evbuffer_free(evbuf);
DPRINTF(E_DBG, L_HTTP, "Parameters in request are: %s\n", body);
return body;
}
int
http_stream_setup(char **stream, const char *url)
{
CURLU *url_handle;
CURLUcode rc;
struct http_client_ctx ctx;
struct evbuffer *evbuf;
char *path;
const char *ext;
char *line;
char *pos;
int ret;
int n;
int pl_format;
bool in_playlist;
*stream = NULL;
CHECK_NULL(L_HTTP, url_handle = curl_url());
rc = curl_url_set(url_handle, CURLUPART_URL, url, 0);
if (rc != 0)
{
DPRINTF(E_LOG, L_HTTP, "Couldn't parse internet playlist: '%s'\n", url);
curl_url_cleanup(url_handle);
return -1;
}
rc = curl_url_get(url_handle, CURLUPART_PATH, &path, 0);
if (rc != 0)
{
DPRINTF(E_LOG, L_HTTP, "Couldn't find internet playlist path: '%s'\n", url);
curl_url_cleanup(url_handle);
return -1;
}
// path does not include query or fragment, so should work with any url's
// e.g. http://yp.shoutcast.com/sbin/tunein-station.pls?id=99179772#Air Jazz
pl_format = PLAYLIST_UNK;
if (path && (ext = strrchr(path, '.')))
{
if (strcasecmp(ext, ".m3u") == 0)
pl_format = PLAYLIST_M3U;
else if (strcasecmp(ext, ".pls") == 0)
pl_format = PLAYLIST_PLS;
}
curl_free(path);
curl_url_cleanup(url_handle);
if (pl_format==PLAYLIST_UNK)
{
*stream = strdup(url);
return 0;
}
// It was a m3u or pls playlist, so now retrieve it
memset(&ctx, 0, sizeof(struct http_client_ctx));
evbuf = evbuffer_new();
if (!evbuf)
return -1;
ctx.url = url;
ctx.input_body = evbuf;
ret = http_client_request(&ctx, NULL);
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.input_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
*/
in_playlist = false;
n = 0;
while ((line = evbuffer_readln(ctx.input_body, NULL, EVBUFFER_EOL_ANY)) && (n < 10))
{
// Skip comments and blank lines without counting for the limit
if (pl_format == PLAYLIST_M3U && (line[0] == '#' || line[0] == '\0'))
goto line_done;
n++;
if (pl_format == PLAYLIST_PLS && !in_playlist)
{
if (strncasecmp(line, "[playlist]", strlen("[playlist]")) == 0)
{
in_playlist = true;
n = 0;
}
goto line_done;
}
if (pl_format == PLAYLIST_PLS)
{
pos = line;
while (*pos == ' ')
++pos;
// We are only interested in `FileN=http://foo/bar.mp3` entries
if (strncasecmp(pos, "file", strlen("file")) != 0)
goto line_done;
while (*pos != '=' && *pos != '\0')
++pos;
if (*pos == '\0')
goto line_done;
++pos;
while (*pos == ' ')
++pos;
// allocate the value part and proceed as with m3u
pos = strdup(pos);
free(line);
line = pos;
}
if (net_is_http_or_https(line))
{
DPRINTF(E_DBG, L_HTTP, "Found internet playlist stream (line %d): %s\n", n, line);
n = -1;
break;
}
line_done:
free(line);
}
evbuffer_free(ctx.input_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 =============================*/
static int
metadata_packet_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx)
{
uint8_t *buffer;
uint8_t *utf;
char *icy_token;
char *save_pr;
char *ptr;
char *end;
av_opt_get(fmtctx, "icy_metadata_packet", AV_OPT_SEARCH_CHILDREN, &buffer);
if (!buffer)
return -1;
/* Some servers send ISO-8859-1 instead of UTF-8 */
if (u8_check(buffer, strlen((char *)buffer)))
{
utf = u8_strconv_from_encoding((char *)buffer, "ISO88591", iconveh_question_mark);
av_free(buffer);
if (utf == NULL)
return -1;
}
else
utf = buffer;
icy_token = strtok_r((char *)utf, ";", &save_pr);
while (icy_token != NULL)
{
ptr = strchr(icy_token, '=');
if (!ptr || (ptr[1] == '\0'))
{
icy_token = strtok_r(NULL, ";", &save_pr);
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 if (strlen(metadata->title) == 0)
metadata->title = NULL;
else
metadata->title = strdup(metadata->title);
}
else if ((strncmp(icy_token, "StreamUrl", strlen("StreamUrl")) == 0) && !metadata->url && strlen(ptr) > 0)
{
metadata->url = strdup(ptr);
}
if (end)
*end = '\'';
icy_token = strtok_r(NULL, ";", &save_pr);
}
av_free(utf);
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 *save_pr;
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, "ISO88591", iconveh_question_mark);
av_free(buffer);
if (!utf)
return -1;
icy_token = strtok_r((char *)utf, "\r\n", &save_pr);
while (icy_token != NULL)
{
ptr = strchr(icy_token, ':');
if (!ptr || (ptr[1] == '\0'))
{
icy_token = strtok_r(NULL, "\r\n", &save_pr);
continue;
}
ptr++;
if (ptr[0] == ' ')
ptr++;
if ((strncmp(icy_token, "icy-name", strlen("icy-name")) == 0) && ptr[0] != '\0' && !metadata->name)
metadata->name = strdup(ptr);
else if ((strncmp(icy_token, "icy-description", strlen("icy-description")) == 0) && ptr[0] != '\0' && !metadata->description)
metadata->description = strdup(ptr);
else if ((strncmp(icy_token, "icy-genre", strlen("icy-genre")) == 0) && ptr[0] != '\0' && !metadata->genre)
metadata->genre = strdup(ptr);
icy_token = strtok_r(NULL, "\r\n", &save_pr);
}
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;
CHECK_NULL(L_HTTP, metadata = calloc(1, 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->url,
metadata->hash
);
*/
return metadata;
}
void
http_icy_metadata_free(struct http_icy_metadata *metadata, int content_only)
{
if (!metadata)
return;
free(metadata->name);
free(metadata->description);
free(metadata->genre);
free(metadata->title);
free(metadata->artist);
free(metadata->url);
free(metadata);
}