/* XMMS2 - X Music Multiplexer System * Copyright (C) 2003-2007 XMMS2 Team * * PLUGINS ARE NOT CONSIDERED TO BE DERIVED WORK !!! * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. */ #include "xmms/xmms_xformplugin.h" #include "xmms/xmms_log.h" #include "xmms/xmms_medialib.h" #include #include #include #include #include #include #include /* * Type definitions */ typedef struct { CURL *curl_easy; CURLM *curl_multi; guint meta_offset; gint read_timeout; gchar *url; struct curl_slist *http_200_aliases; struct curl_slist *http_req_headers; gchar *buffer; guint bufferpos, bufferlen; gint curl_code; xmms_error_t status; gboolean broken_version; } xmms_curl_data_t; typedef void (*handler_func_t) (xmms_xform_t *xform, gchar *header); static void header_handler_contentlength (xmms_xform_t *xform, gchar *header); static void header_handler_icy_metaint (xmms_xform_t *xform, gchar *header); static void header_handler_icy_name (xmms_xform_t *xform, gchar *header); static void header_handler_icy_genre (xmms_xform_t *xform, gchar *header); static handler_func_t header_handler_find (gchar *header); typedef struct { const gchar *name; handler_func_t func; } handler_t; handler_t handlers[] = { { "content-length", header_handler_contentlength }, { "icy-metaint", header_handler_icy_metaint }, { "icy-name", header_handler_icy_name }, { "icy-genre", header_handler_icy_genre }, /* { "\r\n", header_handler_last }, */ { NULL, NULL } }; /* * Function prototypes */ static gboolean xmms_curl_plugin_setup (xmms_xform_plugin_t *xform_plugin); static gboolean xmms_curl_init (xmms_xform_t *xform); static void xmms_curl_destroy (xmms_xform_t *xform); static gint fill_buffer (xmms_xform_t *xform, xmms_curl_data_t *data, xmms_error_t *error); static gint xmms_curl_read (xmms_xform_t *xform, void *buffer, gint len, xmms_error_t *error); /* static gint64 xmms_curl_seek (xmms_xform_t *xform, gint64 offset, xmms_xform_seek_mode_t whence, xmms_error_t *error); */ static size_t xmms_curl_callback_write (void *ptr, size_t size, size_t nmemb, void *stream); static size_t xmms_curl_callback_header (void *ptr, size_t size, size_t nmemb, void *stream); /* * Plugin header */ XMMS_XFORM_PLUGIN ("curl", "Curl Transport for HTTP", XMMS_VERSION, "HTTP transport using CURL", xmms_curl_plugin_setup); static gboolean xmms_curl_plugin_setup (xmms_xform_plugin_t *xform_plugin) { xmms_xform_methods_t methods; XMMS_XFORM_METHODS_INIT (methods); methods.init = xmms_curl_init; methods.destroy = xmms_curl_destroy; methods.read = xmms_curl_read; /* methods.seek = xmms_curl_seek; */ xmms_xform_plugin_methods_set (xform_plugin, &methods); /* xmms_plugin_info_add (plugin, "URL", "http://www.xmms.org"); xmms_plugin_info_add (plugin, "INFO", "http://curl.haxx.se/libcurl"); xmms_plugin_info_add (plugin, "Author", "XMMS Team"); */ xmms_xform_plugin_config_property_register (xform_plugin, "shoutcastinfo", "1", NULL, NULL); xmms_xform_plugin_config_property_register (xform_plugin, "verbose", "0", NULL, NULL); xmms_xform_plugin_config_property_register (xform_plugin, "connecttimeout", "15", NULL, NULL); /* TODO is this timeout of 10 seconds really appropriate? */ xmms_xform_plugin_config_property_register (xform_plugin, "readtimeout", "10", NULL, NULL); xmms_xform_plugin_config_property_register (xform_plugin, "useproxy", "0", NULL, NULL); xmms_xform_plugin_config_property_register (xform_plugin, "proxyaddress", "127.0.0.1:8080", NULL, NULL); xmms_xform_plugin_config_property_register (xform_plugin, "authproxy", "0", NULL, NULL); xmms_xform_plugin_config_property_register (xform_plugin, "proxyuser", "user", NULL, NULL); xmms_xform_plugin_config_property_register (xform_plugin, "proxypass", "password", NULL, NULL); xmms_xform_plugin_indata_add (xform_plugin, XMMS_STREAM_TYPE_MIMETYPE, "application/x-url", XMMS_STREAM_TYPE_URL, "http://*", XMMS_STREAM_TYPE_END); xmms_xform_plugin_indata_add (xform_plugin, XMMS_STREAM_TYPE_MIMETYPE, "application/x-url", XMMS_STREAM_TYPE_URL, "https://*", XMMS_STREAM_TYPE_END); return TRUE; } /* * Member functions */ static gboolean xmms_curl_init (xmms_xform_t *xform) { xmms_curl_data_t *data; xmms_config_property_t *val; xmms_error_t error; gint metaint, verbose, connecttimeout, useproxy, authproxy; const gchar *proxyaddress, *proxyuser, *proxypass; gchar proxyuserpass[90]; const gchar *url; curl_version_info_data *version; url = xmms_xform_indata_get_str (xform, XMMS_STREAM_TYPE_URL); g_return_val_if_fail (xform, FALSE); data = g_new0 (xmms_curl_data_t, 1); data->broken_version = FALSE; val = xmms_xform_config_lookup (xform, "connecttimeout"); connecttimeout = xmms_config_property_get_int (val); val = xmms_xform_config_lookup (xform, "readtimeout"); data->read_timeout = xmms_config_property_get_int (val); val = xmms_xform_config_lookup (xform, "shoutcastinfo"); metaint = xmms_config_property_get_int (val); val = xmms_xform_config_lookup (xform, "verbose"); verbose = xmms_config_property_get_int (val); val = xmms_xform_config_lookup (xform, "useproxy"); useproxy = xmms_config_property_get_int (val); val = xmms_xform_config_lookup (xform, "authproxy"); authproxy = xmms_config_property_get_int (val); val = xmms_xform_config_lookup (xform, "proxyaddress"); proxyaddress = xmms_config_property_get_string (val); val = xmms_xform_config_lookup (xform, "proxyuser"); proxyuser = xmms_config_property_get_string (val); val = xmms_xform_config_lookup (xform, "proxypass"); proxypass = xmms_config_property_get_string (val); g_snprintf (proxyuserpass, sizeof (proxyuserpass), "%s:%s", proxyuser, proxypass); data->buffer = g_malloc (CURL_MAX_WRITE_SIZE); data->url = g_strdup (url); /* check for broken version of curl here */ version = curl_version_info (CURLVERSION_NOW); XMMS_DBG ("Using version %s of libcurl", version->version); if (version->version_num == 0x071001 || version->version_num == 0x071002) { xmms_log_info ("**********************************************"); xmms_log_info ("Your version of libcurl is incompatible with"); xmms_log_info ("XMMS2 and you will not be able to stream shout/ice-cast"); xmms_log_info ("radio stations. Please consider downgrade to 7.15 or"); xmms_log_info ("upgrade to a more recent version than 7.16.2"); xmms_log_info ("**********************************************"); data->broken_version = TRUE; } if (!data->broken_version && metaint == 1) { data->http_req_headers = curl_slist_append (data->http_req_headers, "Icy-MetaData: 1"); } data->curl_easy = curl_easy_init (); curl_easy_setopt (data->curl_easy, CURLOPT_URL, data->url); curl_easy_setopt (data->curl_easy, CURLOPT_HEADER, 0); curl_easy_setopt (data->curl_easy, CURLOPT_HTTPGET, 1); curl_easy_setopt (data->curl_easy, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt (data->curl_easy, CURLOPT_AUTOREFERER, 1); curl_easy_setopt (data->curl_easy, CURLOPT_FAILONERROR, 1); curl_easy_setopt (data->curl_easy, CURLOPT_NOPROGRESS, 1); curl_easy_setopt (data->curl_easy, CURLOPT_USERAGENT, "XMMS2/" XMMS_VERSION); curl_easy_setopt (data->curl_easy, CURLOPT_WRITEHEADER, xform); curl_easy_setopt (data->curl_easy, CURLOPT_WRITEDATA, xform); curl_easy_setopt (data->curl_easy, CURLOPT_WRITEFUNCTION, xmms_curl_callback_write); curl_easy_setopt (data->curl_easy, CURLOPT_HEADERFUNCTION, xmms_curl_callback_header); curl_easy_setopt (data->curl_easy, CURLOPT_CONNECTTIMEOUT, connecttimeout); if (!data->broken_version) { data->http_200_aliases = curl_slist_append (data->http_200_aliases, "ICY 200 OK"); data->http_200_aliases = curl_slist_append (data->http_200_aliases, "ICY 402 Service Unavailabe"); curl_easy_setopt (data->curl_easy, CURLOPT_HTTP200ALIASES, data->http_200_aliases); } if (useproxy == 1) { curl_easy_setopt (data->curl_easy, CURLOPT_PROXY, proxyaddress); if (authproxy == 1) { curl_easy_setopt (data->curl_easy, CURLOPT_PROXYUSERPWD, proxyuserpass); } } curl_easy_setopt (data->curl_easy, CURLOPT_NOSIGNAL, 1); curl_easy_setopt (data->curl_easy, CURLOPT_VERBOSE, verbose); curl_easy_setopt (data->curl_easy, CURLOPT_SSL_VERIFYPEER, 0); curl_easy_setopt (data->curl_easy, CURLOPT_SSL_VERIFYHOST, 0); if (data->http_req_headers) { curl_easy_setopt (data->curl_easy, CURLOPT_HTTPHEADER, data->http_req_headers); } data->curl_multi = curl_multi_init (); data->curl_code = CURLM_CALL_MULTI_PERFORM; curl_multi_add_handle (data->curl_multi, data->curl_easy); xmms_xform_private_data_set (xform, data); /* perform initial fill to see if it contains shoutcast metadata or not */ if (fill_buffer (xform, data, &error) <= 0) { /* something went wrong */ return FALSE; } if (data->meta_offset > 0) { XMMS_DBG ("icy-metadata detected"); xmms_xform_privdata_set_int (xform, "meta_offset", data->meta_offset); xmms_xform_outdata_type_add (xform, XMMS_STREAM_TYPE_MIMETYPE, "application/x-icy-stream", XMMS_STREAM_TYPE_END); } else { xmms_xform_outdata_type_add (xform, XMMS_STREAM_TYPE_MIMETYPE, "application/octet-stream", XMMS_STREAM_TYPE_END); } return TRUE; } static gint fill_buffer (xmms_xform_t *xform, xmms_curl_data_t *data, xmms_error_t *error) { gint handles; g_return_val_if_fail (xform, -1); g_return_val_if_fail (data, -1); g_return_val_if_fail (error, -1); while (TRUE) { if (data->curl_code == CURLM_OK) { fd_set fdread, fdwrite, fdexcp; struct timeval timeout; gint ret, maxfd; timeout.tv_sec = data->read_timeout; timeout.tv_usec = 0; FD_ZERO (&fdread); FD_ZERO (&fdwrite); FD_ZERO (&fdexcp); curl_multi_fdset (data->curl_multi, &fdread, &fdwrite, &fdexcp, &maxfd); ret = select (maxfd + 1, &fdread, &fdwrite, &fdexcp, &timeout); if (ret == -1) { xmms_error_set (error, XMMS_ERROR_GENERIC, "Error select"); return -1; } else if (ret == 0) { xmms_error_set (error, XMMS_ERROR_GENERIC, "Read timeout"); return -1; } } data->curl_code = curl_multi_perform (data->curl_multi, &handles); if (data->curl_code != CURLM_CALL_MULTI_PERFORM && data->curl_code != CURLM_OK) { xmms_error_set (error, XMMS_ERROR_GENERIC, curl_multi_strerror (data->curl_code)); return -1; } /* done */ if (handles == 0) { return 0; } if (data->bufferlen > 0) { return 1; } } } static gint xmms_curl_read (xmms_xform_t *xform, void *buffer, gint len, xmms_error_t *error) { xmms_curl_data_t *data; gint ret; g_return_val_if_fail (xform, -1); g_return_val_if_fail (buffer, -1); g_return_val_if_fail (error, -1); data = xmms_xform_private_data_get (xform); g_return_val_if_fail (data, -1); while (TRUE) { /* if we have data available, just pick it up (even if there's less bytes available than was requested) */ if (data->bufferlen) { len = MIN (len, data->bufferlen); memcpy (buffer, data->buffer, len); data->bufferlen -= len; if (data->bufferlen) { memmove (data->buffer, data->buffer + len, data->bufferlen); } return len; } ret = fill_buffer (xform, data, error); if (ret == 0 || ret == -1) { return ret; } } } static void xmms_curl_destroy (xmms_xform_t *xform) { xmms_curl_data_t *data; g_return_if_fail (xform); data = xmms_xform_private_data_get (xform); g_return_if_fail (data); curl_multi_cleanup (data->curl_multi); curl_easy_cleanup (data->curl_easy); curl_slist_free_all (data->http_200_aliases); curl_slist_free_all (data->http_req_headers); g_free (data->buffer); g_free (data->url); g_free (data); } /* * CURL callback functions */ static size_t xmms_curl_callback_write (void *ptr, size_t size, size_t nmemb, void *stream) { xmms_curl_data_t *data; xmms_xform_t *xform = (xmms_xform_t *) stream; gint len; g_return_val_if_fail (xform, 0); data = xmms_xform_private_data_get (xform); g_return_val_if_fail (data, 0); len = size * nmemb; g_return_val_if_fail ((data->bufferlen + len) <= CURL_MAX_WRITE_SIZE, 0); memcpy (data->buffer + data->bufferlen, ptr, len); data->bufferlen = data->bufferlen + len; return len; } static int strlen_no_crlf (char *ptr, int len) { int ep = len - 1; for (; ep >= 0; --ep) { if (ptr[ep] != '\r' && ptr[ep] != '\n') { break; } } return ep + 1; } static size_t xmms_curl_callback_header (void *ptr, size_t size, size_t nmemb, void *stream) { xmms_xform_t *xform = (xmms_xform_t *) stream; handler_func_t func; gchar *header; XMMS_DBG ("%.*s", strlen_no_crlf ((char*)ptr, size * nmemb), (char*)ptr); g_return_val_if_fail (xform, 0); g_return_val_if_fail (ptr, 0); header = g_strndup ((gchar*)ptr, size * nmemb); func = header_handler_find (header); if (func != NULL) { gchar *val = strchr (header, ':'); if (val) { g_strstrip (++val); } else { val = header; } func (xform, val); } g_free (header); return size * nmemb; } static handler_func_t header_handler_find (gchar *header) { guint i; g_return_val_if_fail (header, NULL); for (i = 0; handlers[i].name != NULL; i++) { guint len = strlen (handlers[i].name); if (g_ascii_strncasecmp (handlers[i].name, header, len) == 0) { return handlers[i].func; } } return NULL; } static void header_handler_contentlength (xmms_xform_t *xform, gchar *header) { xmms_curl_data_t *data; int length; data = xmms_xform_private_data_get (xform); length = strtoul (header, NULL, 10); xmms_xform_metadata_set_int (xform, XMMS_MEDIALIB_ENTRY_PROPERTY_SIZE, length); } static void header_handler_icy_metaint (xmms_xform_t *xform, gchar *header) { xmms_curl_data_t *data; data = xmms_xform_private_data_get (xform); data->meta_offset = strtoul (header, NULL, 10); } static void header_handler_icy_name (xmms_xform_t *xform, gchar *header) { xmms_xform_metadata_set_str (xform, XMMS_MEDIALIB_ENTRY_PROPERTY_CHANNEL, header); } static void header_handler_icy_genre (xmms_xform_t *xform, gchar *header) { xmms_xform_metadata_set_str (xform, XMMS_MEDIALIB_ENTRY_PROPERTY_GENRE, header); } /* static gint64 xmms_curl_seek (xmms_xform_t *xform, gint64 offset, xmms_xform_seek_mode_t whence, xmms_error_t *error) { xmms_error_set (error, XMMS_ERROR_INVAL, "Couldn't seek"); return -1; } */