X-Git-Url: http://review.tizen.org/git/?a=blobdiff_plain;f=ext%2Fsoup%2Fgstsouphttpsrc.c;h=fc7cba71971ab0e588931696b8dce6a7679bb6aa;hb=37f991f06e9af9c312246783e684c444894c648e;hp=79cd41f5b44021139a86aa21bfa0b37535022b12;hpb=106bc2b17b6ab5c29f297e4979d55dcf6588fbb8;p=platform%2Fupstream%2Fgst-plugins-good.git diff --git a/ext/soup/gstsouphttpsrc.c b/ext/soup/gstsouphttpsrc.c index 79cd41f..fc7cba7 100644 --- a/ext/soup/gstsouphttpsrc.c +++ b/ext/soup/gstsouphttpsrc.c @@ -17,7 +17,7 @@ * * This plugin reads data from a remote location specified by a URI. * Supported protocols are 'http', 'https'. - * + * * An HTTP proxy must be specified by its URL. * If the "http_proxy" environment variable is set, its value is used. * If built with libsoup's GNOME integration features, the GNOME proxy @@ -108,23 +108,50 @@ enum PROP_TIMEOUT, PROP_EXTRA_HEADERS, PROP_SOUP_LOG_LEVEL, - PROP_COMPRESS + PROP_COMPRESS, + PROP_KEEP_ALIVE, + PROP_SSL_STRICT, + PROP_SSL_CA_FILE, + PROP_SSL_USE_SYSTEM_CA_FILE, + PROP_TLS_DATABASE, + PROP_RETRIES, + PROP_METHOD, + PROP_TLS_INTERACTION, }; #define DEFAULT_USER_AGENT "GStreamer souphttpsrc " #define DEFAULT_IRADIO_MODE TRUE -#define DEFAULT_SOUP_LOG_LEVEL SOUP_LOGGER_LOG_NONE +#define DEFAULT_SOUP_LOG_LEVEL SOUP_LOGGER_LOG_HEADERS #define DEFAULT_COMPRESS FALSE +#define DEFAULT_KEEP_ALIVE FALSE +#define DEFAULT_SSL_STRICT TRUE +#define DEFAULT_SSL_CA_FILE NULL +#define DEFAULT_SSL_USE_SYSTEM_CA_FILE TRUE +#define DEFAULT_TLS_DATABASE NULL +#define DEFAULT_TLS_INTERACTION NULL +#define DEFAULT_TIMEOUT 15 +#define DEFAULT_RETRIES 3 +#define DEFAULT_SOUP_METHOD NULL + +#define GROW_BLOCKSIZE_LIMIT 1 +#define GROW_BLOCKSIZE_COUNT 1 +#define GROW_BLOCKSIZE_FACTOR 2 +#define REDUCE_BLOCKSIZE_LIMIT 0.20 +#define REDUCE_BLOCKSIZE_COUNT 2 +#define REDUCE_BLOCKSIZE_FACTOR 0.5 static void gst_soup_http_src_uri_handler_init (gpointer g_iface, gpointer iface_data); static void gst_soup_http_src_finalize (GObject * gobject); +static void gst_soup_http_src_dispose (GObject * gobject); static void gst_soup_http_src_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec); static void gst_soup_http_src_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec); +static GstStateChangeReturn gst_soup_http_src_change_state (GstElement * + element, GstStateChange transition); static GstFlowReturn gst_soup_http_src_create (GstPushSrc * psrc, GstBuffer ** outbuf); static gboolean gst_soup_http_src_start (GstBaseSrc * bsrc); @@ -144,28 +171,14 @@ static char *gst_soup_http_src_unicodify (const char *str); static gboolean gst_soup_http_src_build_message (GstSoupHTTPSrc * src, const gchar * method); static void gst_soup_http_src_cancel_message (GstSoupHTTPSrc * src); -static void gst_soup_http_src_queue_message (GstSoupHTTPSrc * src); static gboolean gst_soup_http_src_add_range_header (GstSoupHTTPSrc * src, guint64 offset, guint64 stop_offset); -static void gst_soup_http_src_session_unpause_message (GstSoupHTTPSrc * src); -static void gst_soup_http_src_session_pause_message (GstSoupHTTPSrc * src); static gboolean gst_soup_http_src_session_open (GstSoupHTTPSrc * src); static void gst_soup_http_src_session_close (GstSoupHTTPSrc * src); static void gst_soup_http_src_parse_status (SoupMessage * msg, GstSoupHTTPSrc * src); -static void gst_soup_http_src_chunk_free (gpointer gstbuf); -static SoupBuffer *gst_soup_http_src_chunk_allocator (SoupMessage * msg, - gsize max_len, gpointer user_data); -static void gst_soup_http_src_got_chunk_cb (SoupMessage * msg, - SoupBuffer * chunk, GstSoupHTTPSrc * src); -static void gst_soup_http_src_response_cb (SoupSession * session, - SoupMessage * msg, GstSoupHTTPSrc * src); -static void gst_soup_http_src_got_headers_cb (SoupMessage * msg, - GstSoupHTTPSrc * src); -static void gst_soup_http_src_got_body_cb (SoupMessage * msg, - GstSoupHTTPSrc * src); -static void gst_soup_http_src_finished_cb (SoupMessage * msg, - GstSoupHTTPSrc * src); +static void gst_soup_http_src_got_headers (GstSoupHTTPSrc * src, + SoupMessage * msg); static void gst_soup_http_src_authenticate_cb (SoupSession * session, SoupMessage * msg, SoupAuth * auth, gboolean retrying, GstSoupHTTPSrc * src); @@ -191,6 +204,7 @@ gst_soup_http_src_class_init (GstSoupHTTPSrcClass * klass) gobject_class->set_property = gst_soup_http_src_set_property; gobject_class->get_property = gst_soup_http_src_get_property; gobject_class->finalize = gst_soup_http_src_finalize; + gobject_class->dispose = gst_soup_http_src_dispose; g_object_class_install_property (gobject_class, PROP_LOCATION, @@ -238,7 +252,7 @@ gst_soup_http_src_class_init (GstSoupHTTPSrcClass * klass) g_object_class_install_property (gobject_class, PROP_TIMEOUT, g_param_spec_uint ("timeout", "timeout", "Value in seconds to timeout a blocking I/O (0 = No timeout).", 0, - 3600, 0, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + 3600, DEFAULT_TIMEOUT, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_EXTRA_HEADERS, g_param_spec_boxed ("extra-headers", "Extra Headers", "Extra headers to append to the HTTP request", @@ -248,6 +262,7 @@ gst_soup_http_src_class_init (GstSoupHTTPSrcClass * klass) "Enable internet radio mode (ask server to send shoutcast/icecast " "metadata interleaved with the actual stream data)", DEFAULT_IRADIO_MODE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + /** * GstSoupHTTPSrc::http-log-level: * @@ -277,13 +292,127 @@ gst_soup_http_src_class_init (GstSoupHTTPSrcClass * klass) "Allow compressed content encodings", DEFAULT_COMPRESS, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); - gst_element_class_add_pad_template (gstelement_class, - gst_static_pad_template_get (&srctemplate)); + /** + * GstSoupHTTPSrc::keep-alive: + * + * If set to %TRUE, souphttpsrc will keep alive connections when being + * set to READY state and only will close connections when connecting + * to a different server or when going to NULL state.. + * + * Since: 1.4 + */ + g_object_class_install_property (gobject_class, PROP_KEEP_ALIVE, + g_param_spec_boolean ("keep-alive", "keep-alive", + "Use HTTP persistent connections", DEFAULT_KEEP_ALIVE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * GstSoupHTTPSrc::ssl-strict: + * + * If set to %TRUE, souphttpsrc will reject all SSL certificates that + * are considered invalid. + * + * Since: 1.4 + */ + g_object_class_install_property (gobject_class, PROP_SSL_STRICT, + g_param_spec_boolean ("ssl-strict", "SSL Strict", + "Strict SSL certificate checking", DEFAULT_SSL_STRICT, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * GstSoupHTTPSrc::ssl-ca-file: + * + * A SSL anchor CA file that should be used for checking certificates + * instead of the system CA file. + * + * If this property is non-%NULL, #GstSoupHTTPSrc::ssl-use-system-ca-file + * value will be ignored. + * + * Deprecated: Use #GstSoupHTTPSrc::tls-database property instead. + * Since: 1.4 + */ + g_object_class_install_property (gobject_class, PROP_SSL_CA_FILE, + g_param_spec_string ("ssl-ca-file", "SSL CA File", + "Location of a SSL anchor CA file to use", DEFAULT_SSL_CA_FILE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * GstSoupHTTPSrc::ssl-use-system-ca-file: + * + * If set to %TRUE, souphttpsrc will use the system's CA file for + * checking certificates, unless #GstSoupHTTPSrc::ssl-ca-file or + * #GstSoupHTTPSrc::tls-database are non-%NULL. + * + * Since: 1.4 + */ + g_object_class_install_property (gobject_class, PROP_SSL_USE_SYSTEM_CA_FILE, + g_param_spec_boolean ("ssl-use-system-ca-file", "Use System CA File", + "Use system CA file", DEFAULT_SSL_USE_SYSTEM_CA_FILE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * GstSoupHTTPSrc::tls-database: + * + * TLS database with anchor certificate authorities used to validate + * the server certificate. + * + * If this property is non-%NULL, #GstSoupHTTPSrc::ssl-use-system-ca-file + * and #GstSoupHTTPSrc::ssl-ca-file values will be ignored. + * + * Since: 1.6 + */ + g_object_class_install_property (gobject_class, PROP_TLS_DATABASE, + g_param_spec_object ("tls-database", "TLS database", + "TLS database with anchor certificate authorities used to validate the server certificate", + G_TYPE_TLS_DATABASE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * GstSoupHTTPSrc::tls-interaction: + * + * A #GTlsInteraction object to be used when the connection or certificate + * database need to interact with the user. This will be used to prompt the + * user for passwords or certificate where necessary. + * + * Since: 1.8 + */ + g_object_class_install_property (gobject_class, PROP_TLS_INTERACTION, + g_param_spec_object ("tls-interaction", "TLS interaction", + "A GTlsInteraction object to be used when the connection or certificate database need to interact with the user.", + G_TYPE_TLS_INTERACTION, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * GstSoupHTTPSrc::retries: + * + * Maximum number of retries until giving up. + * + * Since: 1.4 + */ + g_object_class_install_property (gobject_class, PROP_RETRIES, + g_param_spec_int ("retries", "Retries", + "Maximum number of retries until giving up (-1=infinite)", -1, + G_MAXINT, DEFAULT_RETRIES, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * GstSoupHTTPSrc::method + * + * The HTTP method to use when making a request + * + * Since: 1.6 + */ + g_object_class_install_property (gobject_class, PROP_METHOD, + g_param_spec_string ("method", "HTTP method", + "The HTTP method to use (GET, HEAD, OPTIONS, etc)", + DEFAULT_SOUP_METHOD, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + gst_element_class_add_static_pad_template (gstelement_class, &srctemplate); gst_element_class_set_static_metadata (gstelement_class, "HTTP client source", "Source/Network", "Receive data as a client over the network via HTTP using SOUP", "Wouter Cloetens "); + gstelement_class->change_state = + GST_DEBUG_FUNCPTR (gst_soup_http_src_change_state); gstbasesrc_class->start = GST_DEBUG_FUNCPTR (gst_soup_http_src_start); gstbasesrc_class->stop = GST_DEBUG_FUNCPTR (gst_soup_http_src_stop); @@ -305,8 +434,7 @@ gst_soup_http_src_class_init (GstSoupHTTPSrcClass * klass) static void gst_soup_http_src_reset (GstSoupHTTPSrc * src) { - src->interrupted = FALSE; - src->retry = FALSE; + src->retry_count = 0; src->have_size = FALSE; src->got_headers = FALSE; src->seekable = FALSE; @@ -316,6 +444,16 @@ gst_soup_http_src_reset (GstSoupHTTPSrc * src) src->content_size = 0; src->have_body = FALSE; + src->reduce_blocksize_count = 0; + src->increase_blocksize_count = 0; + + src->ret = GST_FLOW_OK; + g_cancellable_reset (src->cancellable); + if (src->input_stream) { + g_object_unref (src->input_stream); + src->input_stream = NULL; + } + gst_caps_replace (&src->src_caps, NULL); g_free (src->iradio_name); src->iradio_name = NULL; @@ -331,7 +469,8 @@ gst_soup_http_src_init (GstSoupHTTPSrc * src) const gchar *proxy; g_mutex_init (&src->mutex); - g_cond_init (&src->request_finished_cond); + g_cond_init (&src->have_headers_cond); + src->cancellable = g_cancellable_new (); src->location = NULL; src->redirection_uri = NULL; src->automatic_redirect = TRUE; @@ -342,22 +481,42 @@ gst_soup_http_src_init (GstSoupHTTPSrc * src) src->proxy_pw = NULL; src->cookies = NULL; src->iradio_mode = DEFAULT_IRADIO_MODE; - src->loop = NULL; - src->context = NULL; src->session = NULL; src->msg = NULL; + src->timeout = DEFAULT_TIMEOUT; src->log_level = DEFAULT_SOUP_LOG_LEVEL; + src->ssl_strict = DEFAULT_SSL_STRICT; + src->ssl_use_system_ca_file = DEFAULT_SSL_USE_SYSTEM_CA_FILE; + src->tls_database = DEFAULT_TLS_DATABASE; + src->tls_interaction = DEFAULT_TLS_INTERACTION; + src->max_retries = DEFAULT_RETRIES; + src->method = DEFAULT_SOUP_METHOD; + src->minimum_blocksize = gst_base_src_get_blocksize (GST_BASE_SRC_CAST (src)); proxy = g_getenv ("http_proxy"); - if (proxy && !gst_soup_http_src_set_proxy (src, proxy)) { + if (!gst_soup_http_src_set_proxy (src, proxy)) { GST_WARNING_OBJECT (src, "The proxy in the http_proxy env var (\"%s\") cannot be parsed.", proxy); } + gst_base_src_set_automatic_eos (GST_BASE_SRC (src), FALSE); + gst_soup_http_src_reset (src); } static void +gst_soup_http_src_dispose (GObject * gobject) +{ + GstSoupHTTPSrc *src = GST_SOUP_HTTP_SRC (gobject); + + GST_DEBUG_OBJECT (src, "dispose"); + + gst_soup_http_src_session_close (src); + + G_OBJECT_CLASS (parent_class)->dispose (gobject); +} + +static void gst_soup_http_src_finalize (GObject * gobject) { GstSoupHTTPSrc *src = GST_SOUP_HTTP_SRC (gobject); @@ -365,11 +524,10 @@ gst_soup_http_src_finalize (GObject * gobject) GST_DEBUG_OBJECT (src, "finalize"); g_mutex_clear (&src->mutex); - g_cond_clear (&src->request_finished_cond); + g_cond_clear (&src->have_headers_cond); + g_object_unref (src->cancellable); g_free (src->location); - if (src->redirection_uri) { - g_free (src->redirection_uri); - } + g_free (src->redirection_uri); g_free (src->user_agent); if (src->proxy != NULL) { soup_uri_free (src->proxy); @@ -380,6 +538,20 @@ gst_soup_http_src_finalize (GObject * gobject) g_free (src->proxy_pw); g_strfreev (src->cookies); + if (src->extra_headers) { + gst_structure_free (src->extra_headers); + src->extra_headers = NULL; + } + + g_free (src->ssl_ca_file); + + if (src->tls_database) + g_object_unref (src->tls_database); + g_free (src->method); + + if (src->tls_interaction) + g_object_unref (src->tls_interaction); + G_OBJECT_CLASS (parent_class)->finalize (gobject); } @@ -407,8 +579,7 @@ gst_soup_http_src_set_property (GObject * object, guint prop_id, break; } case PROP_USER_AGENT: - if (src->user_agent) - g_free (src->user_agent); + g_free (src->user_agent); src->user_agent = g_value_dup_string (value); break; case PROP_IRADIO_MODE: @@ -422,11 +593,6 @@ gst_soup_http_src_set_property (GObject * object, guint prop_id, const gchar *proxy; proxy = g_value_get_string (value); - - if (proxy == NULL) { - GST_WARNING ("proxy property cannot be NULL"); - goto done; - } if (!gst_soup_http_src_set_proxy (src, proxy)) { GST_WARNING ("badly formatted proxy URI"); goto done; @@ -441,23 +607,19 @@ gst_soup_http_src_set_property (GObject * object, guint prop_id, gst_base_src_set_live (GST_BASE_SRC (src), g_value_get_boolean (value)); break; case PROP_USER_ID: - if (src->user_id) - g_free (src->user_id); + g_free (src->user_id); src->user_id = g_value_dup_string (value); break; case PROP_USER_PW: - if (src->user_pw) - g_free (src->user_pw); + g_free (src->user_pw); src->user_pw = g_value_dup_string (value); break; case PROP_PROXY_ID: - if (src->proxy_id) - g_free (src->proxy_id); + g_free (src->proxy_id); src->proxy_id = g_value_dup_string (value); break; case PROP_PROXY_PW: - if (src->proxy_pw) - g_free (src->proxy_pw); + g_free (src->proxy_pw); src->proxy_pw = g_value_dup_string (value); break; case PROP_TIMEOUT: @@ -478,6 +640,34 @@ gst_soup_http_src_set_property (GObject * object, guint prop_id, case PROP_COMPRESS: src->compress = g_value_get_boolean (value); break; + case PROP_KEEP_ALIVE: + src->keep_alive = g_value_get_boolean (value); + break; + case PROP_SSL_STRICT: + src->ssl_strict = g_value_get_boolean (value); + break; + case PROP_SSL_CA_FILE: + g_free (src->ssl_ca_file); + src->ssl_ca_file = g_value_dup_string (value); + break; + case PROP_SSL_USE_SYSTEM_CA_FILE: + src->ssl_use_system_ca_file = g_value_get_boolean (value); + break; + case PROP_TLS_DATABASE: + g_clear_object (&src->tls_database); + src->tls_database = g_value_dup_object (value); + break; + case PROP_TLS_INTERACTION: + g_clear_object (&src->tls_interaction); + src->tls_interaction = g_value_dup_object (value); + break; + case PROP_RETRIES: + src->max_retries = g_value_get_int (value); + break; + case PROP_METHOD: + g_free (src->method); + src->method = g_value_dup_string (value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -545,6 +735,30 @@ gst_soup_http_src_get_property (GObject * object, guint prop_id, case PROP_COMPRESS: g_value_set_boolean (value, src->compress); break; + case PROP_KEEP_ALIVE: + g_value_set_boolean (value, src->keep_alive); + break; + case PROP_SSL_STRICT: + g_value_set_boolean (value, src->ssl_strict); + break; + case PROP_SSL_CA_FILE: + g_value_set_string (value, src->ssl_ca_file); + break; + case PROP_SSL_USE_SYSTEM_CA_FILE: + g_value_set_boolean (value, src->ssl_use_system_ca_file); + break; + case PROP_TLS_DATABASE: + g_value_set_object (value, src->tls_database); + break; + case PROP_TLS_INTERACTION: + g_value_set_object (value, src->tls_interaction); + break; + case PROP_RETRIES: + g_value_set_int (value, src->max_retries); + break; + case PROP_METHOD: + g_value_set_string (value, src->method); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -564,20 +778,8 @@ gst_soup_http_src_unicodify (const gchar * str) static void gst_soup_http_src_cancel_message (GstSoupHTTPSrc * src) { - if (src->msg != NULL) { - src->session_io_status = GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_CANCELLED; - soup_session_cancel_message (src->session, src->msg, SOUP_STATUS_CANCELLED); - } - src->session_io_status = GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_IDLE; - src->msg = NULL; -} - -static void -gst_soup_http_src_queue_message (GstSoupHTTPSrc * src) -{ - soup_session_queue_message (src->session, src->msg, - (SoupSessionCallback) gst_soup_http_src_response_cb, src); - src->session_io_status = GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_QUEUED; + g_cancellable_cancel (src->cancellable); + g_cond_signal (&src->have_headers_cond); } static gboolean @@ -585,14 +787,16 @@ gst_soup_http_src_add_range_header (GstSoupHTTPSrc * src, guint64 offset, guint64 stop_offset) { gchar buf[64]; - gint rc; soup_message_headers_remove (src->msg->request_headers, "Range"); if (offset || stop_offset != -1) { if (stop_offset != -1) { + g_assert (offset != stop_offset); + rc = g_snprintf (buf, sizeof (buf), "bytes=%" G_GUINT64_FORMAT "-%" - G_GUINT64_FORMAT, offset, stop_offset); + G_GUINT64_FORMAT, offset, (stop_offset > 0) ? stop_offset - 1 : + stop_offset); } else { rc = g_snprintf (buf, sizeof (buf), "bytes=%" G_GUINT64_FORMAT "-", offset); @@ -680,19 +884,6 @@ gst_soup_http_src_add_extra_headers (GstSoupHTTPSrc * src) return gst_structure_foreach (src->extra_headers, _append_extra_headers, src); } - -static void -gst_soup_http_src_session_unpause_message (GstSoupHTTPSrc * src) -{ - soup_session_unpause_message (src->session, src->msg); -} - -static void -gst_soup_http_src_session_pause_message (GstSoupHTTPSrc * src) -{ - soup_session_pause_message (src->session, src->msg); -} - static gboolean gst_soup_http_src_session_open (GstSoupHTTPSrc * src) { @@ -707,46 +898,50 @@ gst_soup_http_src_session_open (GstSoupHTTPSrc * src) return FALSE; } - src->context = g_main_context_new (); + if (!src->session) { + GST_DEBUG_OBJECT (src, "Creating session"); + if (src->proxy == NULL) { + src->session = + soup_session_new_with_options (SOUP_SESSION_USER_AGENT, + src->user_agent, SOUP_SESSION_TIMEOUT, src->timeout, + SOUP_SESSION_SSL_STRICT, src->ssl_strict, + SOUP_SESSION_TLS_INTERACTION, src->tls_interaction, NULL); + } else { + src->session = + soup_session_new_with_options (SOUP_SESSION_PROXY_URI, src->proxy, + SOUP_SESSION_TIMEOUT, src->timeout, + SOUP_SESSION_SSL_STRICT, src->ssl_strict, + SOUP_SESSION_USER_AGENT, src->user_agent, + SOUP_SESSION_TLS_INTERACTION, src->tls_interaction, NULL); + } - src->loop = g_main_loop_new (src->context, TRUE); - if (!src->loop) { - GST_ELEMENT_ERROR (src, LIBRARY, INIT, - (NULL), ("Failed to start GMainLoop")); - g_main_context_unref (src->context); - return FALSE; - } + if (!src->session) { + GST_ELEMENT_ERROR (src, LIBRARY, INIT, + (NULL), ("Failed to create async session")); + return FALSE; + } - GST_DEBUG_OBJECT (src, "Creating session"); - if (src->proxy == NULL) { - src->session = - soup_session_async_new_with_options (SOUP_SESSION_ASYNC_CONTEXT, - src->context, SOUP_SESSION_USER_AGENT, src->user_agent, - SOUP_SESSION_TIMEOUT, src->timeout, - SOUP_SESSION_ADD_FEATURE_BY_TYPE, SOUP_TYPE_PROXY_RESOLVER_DEFAULT, - NULL); + g_signal_connect (src->session, "authenticate", + G_CALLBACK (gst_soup_http_src_authenticate_cb), src); + + /* Set up logging */ + gst_soup_util_log_setup (src->session, src->log_level, GST_ELEMENT (src)); + if (src->tls_database) + g_object_set (src->session, "tls-database", src->tls_database, NULL); + else if (src->ssl_ca_file) + g_object_set (src->session, "ssl-ca-file", src->ssl_ca_file, NULL); + else + g_object_set (src->session, "ssl-use-system-ca-file", + src->ssl_use_system_ca_file, NULL); } else { - src->session = - soup_session_async_new_with_options (SOUP_SESSION_ASYNC_CONTEXT, - src->context, SOUP_SESSION_PROXY_URI, src->proxy, - SOUP_SESSION_TIMEOUT, src->timeout, - SOUP_SESSION_USER_AGENT, src->user_agent, NULL); + GST_DEBUG_OBJECT (src, "Re-using session"); } - if (!src->session) { - GST_ELEMENT_ERROR (src, LIBRARY, INIT, - (NULL), ("Failed to create async session")); - return FALSE; - } - - g_signal_connect (src->session, "authenticate", - G_CALLBACK (gst_soup_http_src_authenticate_cb), src); - - /* Set up logging */ - gst_soup_util_log_setup (src->session, src->log_level, GST_ELEMENT (src)); - if (src->compress) soup_session_add_feature_by_type (src->session, SOUP_TYPE_CONTENT_DECODER); + else + soup_session_remove_feature_by_type (src->session, + SOUP_TYPE_CONTENT_DECODER); return TRUE; } @@ -754,12 +949,21 @@ gst_soup_http_src_session_open (GstSoupHTTPSrc * src) static void gst_soup_http_src_session_close (GstSoupHTTPSrc * src) { + GST_DEBUG_OBJECT (src, "Closing session"); + + g_mutex_lock (&src->mutex); + if (src->msg) { + soup_session_cancel_message (src->session, src->msg, SOUP_STATUS_CANCELLED); + g_object_unref (src->msg); + src->msg = NULL; + } + if (src->session) { - soup_session_abort (src->session); /* This unrefs the message. */ + soup_session_abort (src->session); g_object_unref (src->session); src->session = NULL; - src->msg = NULL; } + g_mutex_unlock (&src->mutex); } static void @@ -779,13 +983,53 @@ gst_soup_http_src_authenticate_cb (SoupSession * session, SoupMessage * msg, } static void -gst_soup_http_src_got_headers_cb (SoupMessage * msg, GstSoupHTTPSrc * src) +insert_http_header (const gchar * name, const gchar * value, gpointer user_data) +{ + GstStructure *headers = user_data; + const GValue *gv; + + if (!g_utf8_validate (name, -1, NULL) || !g_utf8_validate (value, -1, NULL)) + return; + + gv = gst_structure_get_value (headers, name); + if (gv && GST_VALUE_HOLDS_ARRAY (gv)) { + GValue v = G_VALUE_INIT; + + g_value_init (&v, G_TYPE_STRING); + g_value_set_string (&v, value); + gst_value_array_append_value ((GValue *) gv, &v); + g_value_unset (&v); + } else if (gv && G_VALUE_HOLDS_STRING (gv)) { + GValue arr = G_VALUE_INIT; + GValue v = G_VALUE_INIT; + const gchar *old_value = g_value_get_string (gv); + + g_value_init (&arr, GST_TYPE_ARRAY); + g_value_init (&v, G_TYPE_STRING); + g_value_set_string (&v, old_value); + gst_value_array_append_value (&arr, &v); + g_value_set_string (&v, value); + gst_value_array_append_value (&arr, &v); + + gst_structure_set_value (headers, name, &arr); + g_value_unset (&v); + g_value_unset (&arr); + } else { + gst_structure_set (headers, name, G_TYPE_STRING, value, NULL); + } +} + +static void +gst_soup_http_src_got_headers (GstSoupHTTPSrc * src, SoupMessage * msg) { const char *value; GstTagList *tag_list; GstBaseSrc *basesrc; guint64 newsize; GHashTable *params = NULL; + GstEvent *http_headers_event; + GstStructure *http_headers, *headers; + const gchar *accept_ranges; GST_INFO_OBJECT (src, "got headers"); @@ -794,18 +1038,63 @@ gst_soup_http_src_got_headers_cb (SoupMessage * msg, GstSoupHTTPSrc * src) return; if (src->automatic_redirect && SOUP_STATUS_IS_REDIRECTION (msg->status_code)) { - src->redirection_uri = g_strdup (soup_message_headers_get_one - (msg->response_headers, "Location")); - GST_DEBUG_OBJECT (src, "%u redirect to \"%s\"", msg->status_code, - src->redirection_uri); - return; + const gchar *location; + + location = soup_message_headers_get_one (msg->response_headers, "Location"); + + if (location) { + if (!g_utf8_validate (location, -1, NULL)) { + GST_ELEMENT_ERROR_WITH_DETAILS (src, RESOURCE, SEEK, + (_("Corrupted HTTP response.")), + ("Location header is not valid UTF-8"), + ("http-status-code", G_TYPE_UINT, msg->status_code, + "http-redirection-uri", G_TYPE_STRING, + GST_STR_NULL (src->redirection_uri), NULL)); + src->ret = GST_FLOW_ERROR; + return; + } + + src->redirection_uri = g_strdup (location); + + src->redirection_permanent = + (msg->status_code == SOUP_STATUS_MOVED_PERMANENTLY); + GST_DEBUG_OBJECT (src, "%u redirect to \"%s\" (permanent %d)", + msg->status_code, src->redirection_uri, src->redirection_permanent); + return; + } } - if (msg->status_code == SOUP_STATUS_UNAUTHORIZED) + if (msg->status_code == SOUP_STATUS_UNAUTHORIZED) { + /* force an error */ + gst_soup_http_src_parse_status (msg, src); return; + } - src->session_io_status = GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_RUNNING; src->got_headers = TRUE; + g_cond_broadcast (&src->have_headers_cond); + + http_headers = gst_structure_new_empty ("http-headers"); + gst_structure_set (http_headers, "uri", G_TYPE_STRING, src->location, NULL); + if (src->redirection_uri) + gst_structure_set (http_headers, "redirection-uri", G_TYPE_STRING, + src->redirection_uri, NULL); + headers = gst_structure_new_empty ("request-headers"); + soup_message_headers_foreach (msg->request_headers, insert_http_header, + headers); + gst_structure_set (http_headers, "request-headers", GST_TYPE_STRUCTURE, + headers, NULL); + gst_structure_free (headers); + headers = gst_structure_new_empty ("response-headers"); + soup_message_headers_foreach (msg->response_headers, insert_http_header, + headers); + gst_structure_set (http_headers, "response-headers", GST_TYPE_STRUCTURE, + headers, NULL); + gst_structure_free (headers); + + http_headers_event = + gst_event_new_custom (GST_EVENT_CUSTOM_DOWNSTREAM_STICKY, http_headers); + gst_event_replace (&src->http_headers_event, http_headers_event); + gst_event_unref (http_headers_event); /* Parse Content-Length. */ if (soup_message_headers_get_encoding (msg->response_headers) == @@ -825,52 +1114,86 @@ gst_soup_http_src_got_headers_cb (SoupMessage * msg, GstSoupHTTPSrc * src) } } + /* If the server reports Accept-Ranges: none we don't have to try + * doing range requests at all + */ + if ((accept_ranges = + soup_message_headers_get_one (msg->response_headers, + "Accept-Ranges"))) { + if (g_ascii_strcasecmp (accept_ranges, "none") == 0) + src->seekable = FALSE; + } + /* Icecast stuff */ tag_list = gst_tag_list_new_empty (); if ((value = soup_message_headers_get_one (msg->response_headers, "icy-metaint")) != NULL) { - gint icy_metaint = atoi (value); + gint icy_metaint; - GST_DEBUG_OBJECT (src, "icy-metaint: %s (parsed: %d)", value, icy_metaint); - if (icy_metaint > 0) { - if (src->src_caps) - gst_caps_unref (src->src_caps); + if (g_utf8_validate (value, -1, NULL)) { + icy_metaint = atoi (value); + + GST_DEBUG_OBJECT (src, "icy-metaint: %s (parsed: %d)", value, + icy_metaint); + if (icy_metaint > 0) { + if (src->src_caps) + gst_caps_unref (src->src_caps); - src->src_caps = gst_caps_new_simple ("application/x-icy", - "metadata-interval", G_TYPE_INT, icy_metaint, NULL); + src->src_caps = gst_caps_new_simple ("application/x-icy", + "metadata-interval", G_TYPE_INT, icy_metaint, NULL); - gst_base_src_set_caps (GST_BASE_SRC (src), src->src_caps); + gst_base_src_set_caps (GST_BASE_SRC (src), src->src_caps); + } } } if ((value = soup_message_headers_get_content_type (msg->response_headers, ¶ms)) != NULL) { - GST_DEBUG_OBJECT (src, "Content-Type: %s", value); - if (g_ascii_strcasecmp (value, "audio/L16") == 0) { + if (!g_utf8_validate (value, -1, NULL)) { + GST_WARNING_OBJECT (src, "Content-Type is invalid UTF-8"); + } else if (g_ascii_strcasecmp (value, "audio/L16") == 0) { gint channels = 2; gint rate = 44100; char *param; - if (src->src_caps) + GST_DEBUG_OBJECT (src, "Content-Type: %s", value); + + if (src->src_caps) { gst_caps_unref (src->src_caps); + src->src_caps = NULL; + } param = g_hash_table_lookup (params, "channels"); - if (param != NULL) - channels = atol (param); + if (param != NULL) { + guint64 val = g_ascii_strtoull (param, NULL, 10); + if (val < 64) + channels = val; + else + channels = 0; + } param = g_hash_table_lookup (params, "rate"); - if (param != NULL) - rate = atol (param); + if (param != NULL) { + guint64 val = g_ascii_strtoull (param, NULL, 10); + if (val < G_MAXINT) + rate = val; + else + rate = 0; + } - src->src_caps = gst_caps_new_simple ("audio/x-raw", - "format", G_TYPE_STRING, "S16BE", - "layout", G_TYPE_STRING, "interleaved", - "channels", G_TYPE_INT, channels, "rate", G_TYPE_INT, rate, NULL); + if (rate > 0 && channels > 0) { + src->src_caps = gst_caps_new_simple ("audio/x-unaligned-raw", + "format", G_TYPE_STRING, "S16BE", + "layout", G_TYPE_STRING, "interleaved", + "channels", G_TYPE_INT, channels, "rate", G_TYPE_INT, rate, NULL); - gst_base_src_set_caps (GST_BASE_SRC (src), src->src_caps); + gst_base_src_set_caps (GST_BASE_SRC (src), src->src_caps); + } } else { + GST_DEBUG_OBJECT (src, "Content-Type: %s", value); + /* Set the Content-Type field on the caps */ if (src->src_caps) { src->src_caps = gst_caps_make_writable (src->src_caps); @@ -887,30 +1210,36 @@ gst_soup_http_src_got_headers_cb (SoupMessage * msg, GstSoupHTTPSrc * src) if ((value = soup_message_headers_get_one (msg->response_headers, "icy-name")) != NULL) { - g_free (src->iradio_name); - src->iradio_name = gst_soup_http_src_unicodify (value); - if (src->iradio_name) { - gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE, GST_TAG_ORGANIZATION, - src->iradio_name, NULL); + if (g_utf8_validate (value, -1, NULL)) { + g_free (src->iradio_name); + src->iradio_name = gst_soup_http_src_unicodify (value); + if (src->iradio_name) { + gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE, GST_TAG_ORGANIZATION, + src->iradio_name, NULL); + } } } if ((value = soup_message_headers_get_one (msg->response_headers, "icy-genre")) != NULL) { - g_free (src->iradio_genre); - src->iradio_genre = gst_soup_http_src_unicodify (value); - if (src->iradio_genre) { - gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE, GST_TAG_GENRE, - src->iradio_genre, NULL); + if (g_utf8_validate (value, -1, NULL)) { + g_free (src->iradio_genre); + src->iradio_genre = gst_soup_http_src_unicodify (value); + if (src->iradio_genre) { + gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE, GST_TAG_GENRE, + src->iradio_genre, NULL); + } } } if ((value = soup_message_headers_get_one (msg->response_headers, "icy-url")) != NULL) { - g_free (src->iradio_url); - src->iradio_url = gst_soup_http_src_unicodify (value); - if (src->iradio_url) { - gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE, GST_TAG_LOCATION, - src->iradio_url, NULL); + if (g_utf8_validate (value, -1, NULL)) { + g_free (src->iradio_url); + src->iradio_url = gst_soup_http_src_unicodify (value); + if (src->iradio_url) { + gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE, GST_TAG_LOCATION, + src->iradio_url, NULL); + } } } if (!gst_tag_list_is_empty (tag_list)) { @@ -928,9 +1257,13 @@ gst_soup_http_src_got_headers_cb (SoupMessage * msg, GstSoupHTTPSrc * src) if (src->ret == GST_FLOW_CUSTOM_ERROR && src->read_position && msg->status_code != SOUP_STATUS_PARTIAL_CONTENT) { src->seekable = FALSE; - GST_ELEMENT_ERROR (src, RESOURCE, SEEK, + GST_ELEMENT_ERROR_WITH_DETAILS (src, RESOURCE, SEEK, (_("Server does not support seeking.")), - ("Server does not accept Range HTTP header, URL: %s", src->location)); + ("Server does not accept Range HTTP header, URL: %s, Redirect to: %s", + src->location, GST_STR_NULL (src->redirection_uri)), + ("http-status-code", G_TYPE_UINT, msg->status_code, + "http-redirection-uri", G_TYPE_STRING, + GST_STR_NULL (src->redirection_uri), NULL)); src->ret = GST_FLOW_ERROR; } @@ -939,236 +1272,33 @@ gst_soup_http_src_got_headers_cb (SoupMessage * msg, GstSoupHTTPSrc * src) * GST_FLOW_ERROR from the create function instead of having * got_chunk_cb overwrite src->ret with FLOW_OK again. */ if (src->ret == GST_FLOW_ERROR || src->ret == GST_FLOW_EOS) { - gst_soup_http_src_session_pause_message (src); - - if (src->loop) - g_main_loop_quit (src->loop); - } - g_cond_signal (&src->request_finished_cond); -} - -/* Have body. Signal EOS. */ -static void -gst_soup_http_src_got_body_cb (SoupMessage * msg, GstSoupHTTPSrc * src) -{ - if (G_UNLIKELY (msg != src->msg)) { - GST_DEBUG_OBJECT (src, "got body, but not for current message"); - return; - } - if (G_UNLIKELY (src->session_io_status != - GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_RUNNING)) { - /* Probably a redirect. */ - return; } - GST_DEBUG_OBJECT (src, "got body"); - src->ret = GST_FLOW_EOS; - src->have_body = TRUE; - - /* no need to interrupt the message here, we do it on the - * finished_cb anyway if needed. And getting the body might mean - * that the connection was hang up before finished. This happens when - * the pipeline is stalled for too long (long pauses during playback). - * Best to let it continue from here and pause because it reached the - * final bytes based on content_size or received an out of range error */ } -/* Finished. Signal EOS. */ -static void -gst_soup_http_src_finished_cb (SoupMessage * msg, GstSoupHTTPSrc * src) +static GstBuffer * +gst_soup_http_src_alloc_buffer (GstSoupHTTPSrc * src) { - if (G_UNLIKELY (msg != src->msg)) { - GST_DEBUG_OBJECT (src, "finished, but not for current message"); - return; - } - GST_DEBUG_OBJECT (src, "finished"); - src->ret = GST_FLOW_EOS; - if (src->session_io_status == GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_CANCELLED) { - /* gst_soup_http_src_cancel_message() triggered this; probably a seek - * that occurred in the QUEUEING state; i.e. before the connection setup - * was complete. Do nothing */ - } else if (src->session_io_status == - GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_RUNNING && src->read_position > 0 && - (src->have_size && src->read_position < src->content_size)) { - /* The server disconnected while streaming. Reconnect and seeking to the - * last location. */ - src->retry = TRUE; - src->ret = GST_FLOW_CUSTOM_ERROR; - } else if (G_UNLIKELY (src->session_io_status != - GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_RUNNING)) { - if (msg->method == SOUP_METHOD_HEAD) { - GST_DEBUG_OBJECT (src, "Ignoring error %d:%s during HEAD request", - msg->status_code, msg->reason_phrase); - } else { - gst_soup_http_src_parse_status (msg, src); - } - } - if (src->loop) - g_main_loop_quit (src->loop); - g_cond_signal (&src->request_finished_cond); -} - -/* Buffer lifecycle management. - * - * gst_soup_http_src_create() runs the GMainLoop for this element, to let - * Soup take control. - * A GstBuffer is allocated in gst_soup_http_src_chunk_allocator() and - * associated with a SoupBuffer. - * Soup reads HTTP data in the GstBuffer's data buffer. - * The gst_soup_http_src_got_chunk_cb() is then called with the SoupBuffer. - * That sets gst_soup_http_src_create()'s return argument to the GstBuffer, - * increments its refcount (to 2), pauses the flow of data from the HTTP - * source to prevent gst_soup_http_src_got_chunk_cb() from being called - * again and breaks out of the GMainLoop. - * Because the SOUP_MESSAGE_OVERWRITE_CHUNKS flag is set, Soup frees the - * SoupBuffer and calls gst_soup_http_src_chunk_free(), which decrements the - * refcount (to 1). - * gst_soup_http_src_create() returns the GstBuffer. It will be freed by a - * downstream element. - * If Soup fails to read HTTP data, it does not call - * gst_soup_http_src_got_chunk_cb(), but still frees the SoupBuffer and - * calls gst_soup_http_src_chunk_free(), which decrements the GstBuffer's - * refcount to 0, freeing it. - */ - -typedef struct -{ - GstBuffer *buffer; - GstMapInfo map; -} SoupGstChunk; - -static void -gst_soup_http_src_chunk_free (gpointer user_data) -{ - SoupGstChunk *chunk = (SoupGstChunk *) user_data; - - gst_buffer_unmap (chunk->buffer, &chunk->map); - gst_buffer_unref (chunk->buffer); - g_slice_free (SoupGstChunk, chunk); -} - -static SoupBuffer * -gst_soup_http_src_chunk_allocator (SoupMessage * msg, gsize max_len, - gpointer user_data) -{ - GstSoupHTTPSrc *src = (GstSoupHTTPSrc *) user_data; GstBaseSrc *basesrc = GST_BASE_SRC_CAST (src); - GstBuffer *gstbuf; - SoupBuffer *soupbuf; - gsize length; GstFlowReturn rc; - SoupGstChunk *chunk; - - if (max_len) - length = MIN (basesrc->blocksize, max_len); - else - length = basesrc->blocksize; - GST_DEBUG_OBJECT (src, "alloc %" G_GSIZE_FORMAT " bytes <= %" G_GSIZE_FORMAT, - length, max_len); + GstBuffer *gstbuf; - rc = GST_BASE_SRC_CLASS (parent_class)->alloc (basesrc, -1, length, &gstbuf); + rc = GST_BASE_SRC_CLASS (parent_class)->alloc (basesrc, -1, + basesrc->blocksize, &gstbuf); if (G_UNLIKELY (rc != GST_FLOW_OK)) { - /* Failed to allocate buffer. Stall SoupSession and return error code - * to create(). */ - src->ret = rc; - g_main_loop_quit (src->loop); return NULL; } - chunk = g_slice_new0 (SoupGstChunk); - chunk->buffer = gstbuf; - gst_buffer_map (gstbuf, &chunk->map, GST_MAP_READWRITE); - - soupbuf = soup_buffer_new_with_owner (chunk->map.data, chunk->map.size, - chunk, gst_soup_http_src_chunk_free); - - return soupbuf; -} - -static void -gst_soup_http_src_got_chunk_cb (SoupMessage * msg, SoupBuffer * chunk, - GstSoupHTTPSrc * src) -{ - GstBaseSrc *basesrc; - guint64 new_position; - SoupGstChunk *gchunk; - - if (G_UNLIKELY (msg != src->msg)) { - GST_DEBUG_OBJECT (src, "got chunk, but not for current message"); - return; - } - src->have_body = FALSE; - if (G_UNLIKELY (src->session_io_status != - GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_RUNNING)) { - /* Probably a redirect. */ - return; - } - basesrc = GST_BASE_SRC_CAST (src); - GST_DEBUG_OBJECT (src, "got chunk of %" G_GSIZE_FORMAT " bytes", - chunk->length); - - /* Extract the GstBuffer from the SoupBuffer and set its fields. */ - gchunk = (SoupGstChunk *) soup_buffer_get_owner (chunk); - *src->outbuf = gchunk->buffer; - - gst_buffer_resize (*src->outbuf, 0, chunk->length); - GST_BUFFER_OFFSET (*src->outbuf) = basesrc->segment.position; - - gst_buffer_ref (*src->outbuf); - - new_position = src->read_position + chunk->length; - if (G_LIKELY (src->request_position == src->read_position)) - src->request_position = new_position; - src->read_position = new_position; - - if (src->content_size != 0 && new_position > src->content_size) { - GST_DEBUG_OBJECT (src, "Got position previous estimated content size " - "(%" G_GINT64_FORMAT " > %" G_GINT64_FORMAT ")", new_position, - src->content_size); - src->content_size = new_position; - basesrc->segment.duration = src->content_size; - gst_element_post_message (GST_ELEMENT (src), - gst_message_new_duration_changed (GST_OBJECT (src))); - } - - src->ret = GST_FLOW_OK; - g_main_loop_quit (src->loop); - gst_soup_http_src_session_pause_message (src); -} - -static void -gst_soup_http_src_response_cb (SoupSession * session, SoupMessage * msg, - GstSoupHTTPSrc * src) -{ - if (G_UNLIKELY (msg != src->msg)) { - GST_DEBUG_OBJECT (src, "got response %d: %s, but not for current message", - msg->status_code, msg->reason_phrase); - return; - } - if (G_UNLIKELY (src->session_io_status != - GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_RUNNING) - && SOUP_STATUS_IS_REDIRECTION (msg->status_code)) { - /* Ignore redirections. */ - return; - } - GST_DEBUG_OBJECT (src, "got response %d: %s", msg->status_code, - msg->reason_phrase); - if (src->session_io_status == GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_RUNNING && - src->read_position > 0 && (src->have_size - && src->read_position < src->content_size)) { - /* The server disconnected while streaming. Reconnect and seeking to the - * last location. */ - src->retry = TRUE; - } else - gst_soup_http_src_parse_status (msg, src); - /* The session's SoupMessage object expires after this callback returns. */ - src->msg = NULL; - g_main_loop_quit (src->loop); + return gstbuf; } #define SOUP_HTTP_SRC_ERROR(src,soup_msg,cat,code,error_message) \ - GST_ELEMENT_ERROR ((src), cat, code, ("%s", error_message), \ - ("%s (%d), URL: %s", (soup_msg)->reason_phrase, \ - (soup_msg)->status_code, (src)->location)); + do { \ + GST_ELEMENT_ERROR_WITH_DETAILS ((src), cat, code, ("%s", error_message), \ + ("%s (%d), URL: %s, Redirect to: %s", (soup_msg)->reason_phrase, \ + (soup_msg)->status_code, (src)->location, GST_STR_NULL ((src)->redirection_uri)), \ + ("http-status-code", G_TYPE_UINT, msg->status_code, \ + "http-redirect-uri", G_TYPE_STRING, GST_STR_NULL ((src)->redirection_uri), NULL)); \ + } while(0) static void gst_soup_http_src_parse_status (SoupMessage * msg, GstSoupHTTPSrc * src) @@ -1197,10 +1327,14 @@ gst_soup_http_src_parse_status (SoupMessage * msg, GstSoupHTTPSrc * src) src->ret = GST_FLOW_ERROR; break; case SOUP_STATUS_IO_ERROR: - SOUP_HTTP_SRC_ERROR (src, msg, RESOURCE, READ, - _("A network error occured, or the server closed the connection " - "unexpectedly.")); - src->ret = GST_FLOW_ERROR; + if (src->max_retries == -1 || src->retry_count < src->max_retries) { + src->ret = GST_FLOW_CUSTOM_ERROR; + } else { + SOUP_HTTP_SRC_ERROR (src, msg, RESOURCE, READ, + _("A network error occurred, or the server closed the connection " + "unexpectedly.")); + src->ret = GST_FLOW_ERROR; + } break; case SOUP_STATUS_MALFORMED: SOUP_HTTP_SRC_ERROR (src, msg, RESOURCE, READ, @@ -1214,13 +1348,21 @@ gst_soup_http_src_parse_status (SoupMessage * msg, GstSoupHTTPSrc * src) } else if (SOUP_STATUS_IS_CLIENT_ERROR (msg->status_code) || SOUP_STATUS_IS_REDIRECTION (msg->status_code) || SOUP_STATUS_IS_SERVER_ERROR (msg->status_code)) { + const gchar *reason_phrase; + + reason_phrase = msg->reason_phrase; + if (reason_phrase && !g_utf8_validate (reason_phrase, -1, NULL)) { + GST_ERROR_OBJECT (src, "Invalid UTF-8 in reason"); + reason_phrase = "(invalid)"; + } + /* Report HTTP error. */ /* when content_size is unknown and we have just finished receiving * a body message, requests that go beyond the content limits will result * in an error. Here we convert those to EOS */ if (msg->status_code == SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE && - src->have_body && src->have_size) { + src->have_body && !src->have_size) { GST_DEBUG_OBJECT (src, "Requested range out of limits and received full " "body, returning EOS"); src->ret = GST_FLOW_EOS; @@ -1231,23 +1373,33 @@ gst_soup_http_src_parse_status (SoupMessage * msg, GstSoupHTTPSrc * src) * error dialog according to libsoup documentation. */ if (msg->status_code == SOUP_STATUS_NOT_FOUND) { - GST_ELEMENT_ERROR (src, RESOURCE, NOT_FOUND, - ("%s", msg->reason_phrase), - ("%s (%d), URL: %s", msg->reason_phrase, msg->status_code, - src->location)); - } else if (msg->status_code == SOUP_STATUS_UNAUTHORIZED || - msg->status_code == SOUP_STATUS_PAYMENT_REQUIRED || - msg->status_code == SOUP_STATUS_FORBIDDEN || - msg->status_code == SOUP_STATUS_PROXY_AUTHENTICATION_REQUIRED) { - GST_ELEMENT_ERROR (src, RESOURCE, NOT_AUTHORIZED, - ("%s", msg->reason_phrase), - ("%s (%d), URL: %s", msg->reason_phrase, msg->status_code, - src->location)); + GST_ELEMENT_ERROR_WITH_DETAILS (src, RESOURCE, NOT_FOUND, + ("%s", reason_phrase), + ("%s (%d), URL: %s, Redirect to: %s", reason_phrase, + msg->status_code, src->location, + GST_STR_NULL (src->redirection_uri)), + ("http-status-code", G_TYPE_UINT, msg->status_code, + "http-redirect-uri", G_TYPE_STRING, + GST_STR_NULL (src->redirection_uri), NULL)); + } else if (msg->status_code == SOUP_STATUS_UNAUTHORIZED + || msg->status_code == SOUP_STATUS_PAYMENT_REQUIRED + || msg->status_code == SOUP_STATUS_FORBIDDEN + || msg->status_code == SOUP_STATUS_PROXY_AUTHENTICATION_REQUIRED) { + GST_ELEMENT_ERROR_WITH_DETAILS (src, RESOURCE, NOT_AUTHORIZED, ("%s", + reason_phrase), ("%s (%d), URL: %s, Redirect to: %s", + reason_phrase, msg->status_code, src->location, + GST_STR_NULL (src->redirection_uri)), ("http-status-code", + G_TYPE_UINT, msg->status_code, "http-redirect-uri", G_TYPE_STRING, + GST_STR_NULL (src->redirection_uri), NULL)); } else { - GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ, - ("%s", msg->reason_phrase), - ("%s (%d), URL: %s", msg->reason_phrase, msg->status_code, - src->location)); + GST_ELEMENT_ERROR_WITH_DETAILS (src, RESOURCE, OPEN_READ, + ("%s", reason_phrase), + ("%s (%d), URL: %s, Redirect to: %s", reason_phrase, + msg->status_code, src->location, + GST_STR_NULL (src->redirection_uri)), + ("http-status-code", G_TYPE_UINT, msg->status_code, + "http-redirect-uri", G_TYPE_STRING, + GST_STR_NULL (src->redirection_uri), NULL)); } src->ret = GST_FLOW_ERROR; } @@ -1256,15 +1408,18 @@ gst_soup_http_src_parse_status (SoupMessage * msg, GstSoupHTTPSrc * src) static gboolean gst_soup_http_src_build_message (GstSoupHTTPSrc * src, const gchar * method) { + g_return_val_if_fail (src->msg == NULL, FALSE); + src->msg = soup_message_new (method, src->location); if (!src->msg) { GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ, ("Error parsing URL."), ("URL: %s", src->location)); return FALSE; } - src->session_io_status = GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_IDLE; - soup_message_headers_append (src->msg->request_headers, "Connection", - "close"); + if (!src->keep_alive) { + soup_message_headers_append (src->msg->request_headers, "Connection", + "close"); + } if (src->iradio_mode) { soup_message_headers_append (src->msg->request_headers, "icy-metadata", "1"); @@ -1277,20 +1432,9 @@ gst_soup_http_src_build_message (GstSoupHTTPSrc * src, const gchar * method) *cookie); } } - src->retry = FALSE; - g_signal_connect (src->msg, "got_headers", - G_CALLBACK (gst_soup_http_src_got_headers_cb), src); - g_signal_connect (src->msg, "got_body", - G_CALLBACK (gst_soup_http_src_got_body_cb), src); - g_signal_connect (src->msg, "finished", - G_CALLBACK (gst_soup_http_src_finished_cb), src); - g_signal_connect (src->msg, "got_chunk", - G_CALLBACK (gst_soup_http_src_got_chunk_cb), src); soup_message_set_flags (src->msg, SOUP_MESSAGE_OVERWRITE_CHUNKS | (src->automatic_redirect ? 0 : SOUP_MESSAGE_NO_REDIRECT)); - soup_message_set_chunk_allocator (src->msg, - gst_soup_http_src_chunk_allocator, src, NULL); gst_soup_http_src_add_range_header (src, src->request_position, src->stop_position); @@ -1300,82 +1444,289 @@ gst_soup_http_src_build_message (GstSoupHTTPSrc * src, const gchar * method) } static GstFlowReturn -gst_soup_http_src_do_request (GstSoupHTTPSrc * src, const gchar * method, - GstBuffer ** outbuf) +gst_soup_http_src_send_message (GstSoupHTTPSrc * src) { + g_return_val_if_fail (src->msg != NULL, GST_FLOW_ERROR); + + /* FIXME We are ignoring the GError here, might be useful to debug */ + src->input_stream = + soup_session_send (src->session, src->msg, src->cancellable, NULL); + + if (g_cancellable_is_cancelled (src->cancellable)) + return GST_FLOW_FLUSHING; + + gst_soup_http_src_got_headers (src, src->msg); + if (src->ret != GST_FLOW_OK) { + return src->ret; + } + + if (!src->input_stream) { + GST_DEBUG_OBJECT (src, "Didn't get an input stream"); + return GST_FLOW_ERROR; + } + + if (SOUP_STATUS_IS_SUCCESSFUL (src->msg->status_code)) { + GST_DEBUG_OBJECT (src, "Successfully got a reply"); + } else { + /* FIXME - be more helpful to people debugging */ + return GST_FLOW_ERROR; + } + + return GST_FLOW_OK; +} + +static GstFlowReturn +gst_soup_http_src_do_request (GstSoupHTTPSrc * src, const gchar * method) +{ + if (src->max_retries != -1 && src->retry_count > src->max_retries) { + GST_DEBUG_OBJECT (src, "Max retries reached"); + src->ret = GST_FLOW_ERROR; + return src->ret; + } + + src->retry_count++; + /* EOS immediately if we have an empty segment */ + if (src->request_position == src->stop_position) + return GST_FLOW_EOS; + GST_LOG_OBJECT (src, "Running request for method: %s", method); + + /* Update the position if we are retrying */ if (src->msg && (src->request_position != src->read_position)) { - if (src->session_io_status == GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_IDLE) { - gst_soup_http_src_add_range_header (src, src->request_position, - src->stop_position); - } else { - GST_DEBUG_OBJECT (src, "Seek from position %" G_GUINT64_FORMAT - " to %" G_GUINT64_FORMAT ": requeueing connection request", - src->read_position, src->request_position); - gst_soup_http_src_cancel_message (src); - } + gst_soup_http_src_add_range_header (src, src->request_position, + src->stop_position); } - if (!src->msg) + + if (!src->msg) { if (!gst_soup_http_src_build_message (src, method)) { return GST_FLOW_ERROR; } + } - src->ret = GST_FLOW_CUSTOM_ERROR; - src->outbuf = outbuf; - do { - if (src->interrupted) { - GST_DEBUG_OBJECT (src, "interrupted"); - break; - } - if (src->retry) { - GST_DEBUG_OBJECT (src, "Reconnecting"); - if (!gst_soup_http_src_build_message (src, method)) { - return GST_FLOW_ERROR; - } - src->retry = FALSE; - continue; + if (g_cancellable_is_cancelled (src->cancellable)) { + GST_INFO_OBJECT (src, "interrupted"); + src->ret = GST_FLOW_FLUSHING; + goto done; + } + src->ret = gst_soup_http_src_send_message (src); + +done: + return src->ret; +} + +/* + * Check if the bytes_read is above a certain threshold of the blocksize, if + * that happens a few times in a row, increase the blocksize; Do the same in + * the opposite direction to reduce the blocksize. + */ +static void +gst_soup_http_src_check_update_blocksize (GstSoupHTTPSrc * src, + gint64 bytes_read) +{ + guint blocksize = gst_base_src_get_blocksize (GST_BASE_SRC_CAST (src)); + + GST_LOG_OBJECT (src, "Checking to update blocksize. Read:%" G_GINT64_FORMAT + " blocksize:%u", bytes_read, blocksize); + + if (bytes_read >= blocksize * GROW_BLOCKSIZE_LIMIT) { + src->reduce_blocksize_count = 0; + src->increase_blocksize_count++; + + if (src->increase_blocksize_count >= GROW_BLOCKSIZE_COUNT) { + blocksize *= GROW_BLOCKSIZE_FACTOR; + GST_DEBUG_OBJECT (src, "Increased blocksize to %u", blocksize); + gst_base_src_set_blocksize (GST_BASE_SRC_CAST (src), blocksize); + src->increase_blocksize_count = 0; } - if (!src->msg) { - GST_DEBUG_OBJECT (src, "EOS reached"); - break; + } else if (bytes_read < blocksize * REDUCE_BLOCKSIZE_LIMIT) { + src->reduce_blocksize_count++; + src->increase_blocksize_count = 0; + + if (src->reduce_blocksize_count >= REDUCE_BLOCKSIZE_COUNT) { + blocksize *= REDUCE_BLOCKSIZE_FACTOR; + blocksize = MAX (blocksize, src->minimum_blocksize); + GST_DEBUG_OBJECT (src, "Decreased blocksize to %u", blocksize); + gst_base_src_set_blocksize (GST_BASE_SRC_CAST (src), blocksize); + src->reduce_blocksize_count = 0; } + } else { + src->reduce_blocksize_count = src->increase_blocksize_count = 0; + } +} - switch (src->session_io_status) { - case GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_IDLE: - GST_DEBUG_OBJECT (src, "Queueing connection request"); - gst_soup_http_src_queue_message (src); - break; - case GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_QUEUED: - break; - case GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_RUNNING: - gst_soup_http_src_session_unpause_message (src); - break; - case GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_CANCELLED: - /* Impossible. */ - break; +static void +gst_soup_http_src_update_position (GstSoupHTTPSrc * src, gint64 bytes_read) +{ + GstBaseSrc *basesrc = GST_BASE_SRC_CAST (src); + guint64 new_position; + + new_position = src->read_position + bytes_read; + if (G_LIKELY (src->request_position == src->read_position)) + src->request_position = new_position; + src->read_position = new_position; + + if (src->have_size) { + if (new_position > src->content_size) { + GST_DEBUG_OBJECT (src, "Got position previous estimated content size " + "(%" G_GINT64_FORMAT " > %" G_GINT64_FORMAT ")", new_position, + src->content_size); + src->content_size = new_position; + basesrc->segment.duration = src->content_size; + gst_element_post_message (GST_ELEMENT (src), + gst_message_new_duration_changed (GST_OBJECT (src))); + } else if (new_position == src->content_size) { + GST_DEBUG_OBJECT (src, "We're EOS now"); } + } +} - if (src->ret == GST_FLOW_CUSTOM_ERROR) - g_main_loop_run (src->loop); - } while (src->ret == GST_FLOW_CUSTOM_ERROR); +static GstFlowReturn +gst_soup_http_src_read_buffer (GstSoupHTTPSrc * src, GstBuffer ** outbuf) +{ + gssize read_bytes; + GstMapInfo mapinfo; + GstBaseSrc *bsrc; + GstFlowReturn ret; - if (src->ret == GST_FLOW_CUSTOM_ERROR) - src->ret = GST_FLOW_EOS; - g_cond_signal (&src->request_finished_cond); - return src->ret; + bsrc = GST_BASE_SRC_CAST (src); + + *outbuf = gst_soup_http_src_alloc_buffer (src); + if (!*outbuf) { + GST_WARNING_OBJECT (src, "Failed to allocate buffer"); + return GST_FLOW_ERROR; + } + + if (!gst_buffer_map (*outbuf, &mapinfo, GST_MAP_WRITE)) { + GST_WARNING_OBJECT (src, "Failed to map buffer"); + return GST_FLOW_ERROR; + } + + read_bytes = + g_input_stream_read (src->input_stream, mapinfo.data, mapinfo.size, + src->cancellable, NULL); + GST_DEBUG_OBJECT (src, "Read %" G_GSSIZE_FORMAT " bytes from http input", + read_bytes); + + g_mutex_lock (&src->mutex); + if (g_cancellable_is_cancelled (src->cancellable)) { + gst_buffer_unmap (*outbuf, &mapinfo); + gst_buffer_unref (*outbuf); + g_mutex_unlock (&src->mutex); + return GST_FLOW_FLUSHING; + } + + gst_buffer_unmap (*outbuf, &mapinfo); + if (read_bytes > 0) { + gst_buffer_set_size (*outbuf, read_bytes); + GST_BUFFER_OFFSET (*outbuf) = bsrc->segment.position; + ret = GST_FLOW_OK; + gst_soup_http_src_update_position (src, read_bytes); + + /* Got some data, reset retry counter */ + src->retry_count = 0; + + gst_soup_http_src_check_update_blocksize (src, read_bytes); + + /* If we're at the end of a range request, read again to let libsoup + * finalize the request. This allows to reuse the connection again later, + * otherwise we would have to cancel the message and close the connection + */ + if (bsrc->segment.stop != -1 + && bsrc->segment.position + read_bytes >= bsrc->segment.stop) { + guint8 tmp[128]; + + g_object_unref (src->msg); + src->msg = NULL; + src->have_body = TRUE; + + /* This should return immediately as we're at the end of the range */ + read_bytes = + g_input_stream_read (src->input_stream, tmp, sizeof (tmp), + src->cancellable, NULL); + if (read_bytes > 0) + GST_ERROR_OBJECT (src, + "Read %" G_GSIZE_FORMAT " bytes after end of range", read_bytes); + } + } else { + gst_buffer_unref (*outbuf); + if (read_bytes < 0) { + /* Maybe the server disconnected, retry */ + ret = GST_FLOW_CUSTOM_ERROR; + } else { + g_object_unref (src->msg); + src->msg = NULL; + ret = GST_FLOW_EOS; + src->have_body = TRUE; + } + } + g_mutex_unlock (&src->mutex); + + return ret; } static GstFlowReturn gst_soup_http_src_create (GstPushSrc * psrc, GstBuffer ** outbuf) { GstSoupHTTPSrc *src; - GstFlowReturn ret; + GstFlowReturn ret = GST_FLOW_OK; + GstEvent *http_headers_event = NULL; src = GST_SOUP_HTTP_SRC (psrc); +retry: g_mutex_lock (&src->mutex); - ret = gst_soup_http_src_do_request (src, SOUP_METHOD_GET, outbuf); + + /* Check for pending position change */ + if (src->request_position != src->read_position) { + if (src->input_stream) { + g_input_stream_close (src->input_stream, src->cancellable, NULL); + g_object_unref (src->input_stream); + src->input_stream = NULL; + } + } + + if (g_cancellable_is_cancelled (src->cancellable)) { + ret = GST_FLOW_FLUSHING; + g_mutex_unlock (&src->mutex); + goto done; + } + + /* If we have no open connection to the server, start one */ + if (!src->input_stream) { + *outbuf = NULL; + ret = + gst_soup_http_src_do_request (src, + src->method ? src->method : SOUP_METHOD_GET); + http_headers_event = src->http_headers_event; + src->http_headers_event = NULL; + } g_mutex_unlock (&src->mutex); + + if (ret == GST_FLOW_OK || ret == GST_FLOW_CUSTOM_ERROR) { + if (http_headers_event) { + gst_pad_push_event (GST_BASE_SRC_PAD (src), http_headers_event); + http_headers_event = NULL; + } + } + + if (ret == GST_FLOW_OK) + ret = gst_soup_http_src_read_buffer (src, outbuf); + +done: + GST_DEBUG_OBJECT (src, "Returning %d %s", ret, gst_flow_get_name (ret)); + if (ret != GST_FLOW_OK) { + if (http_headers_event) + gst_event_unref (http_headers_event); + + g_mutex_lock (&src->mutex); + if (src->input_stream) { + g_object_unref (src->input_stream); + src->input_stream = NULL; + } + g_mutex_unlock (&src->mutex); + if (ret == GST_FLOW_CUSTOM_ERROR) + goto retry; + } return ret; } @@ -1396,22 +1747,36 @@ gst_soup_http_src_stop (GstBaseSrc * bsrc) src = GST_SOUP_HTTP_SRC (bsrc); GST_DEBUG_OBJECT (src, "stop()"); - gst_soup_http_src_session_close (src); - if (src->loop) { - g_main_loop_unref (src->loop); - g_main_context_unref (src->context); - src->loop = NULL; - src->context = NULL; - } - if (src->extra_headers) { - gst_structure_free (src->extra_headers); - src->extra_headers = NULL; - } + if (src->keep_alive && !src->msg) + gst_soup_http_src_cancel_message (src); + else + gst_soup_http_src_session_close (src); gst_soup_http_src_reset (src); return TRUE; } +static GstStateChangeReturn +gst_soup_http_src_change_state (GstElement * element, GstStateChange transition) +{ + GstStateChangeReturn ret; + GstSoupHTTPSrc *src; + + src = GST_SOUP_HTTP_SRC (element); + + switch (transition) { + case GST_STATE_CHANGE_READY_TO_NULL: + gst_soup_http_src_session_close (src); + break; + default: + break; + } + + ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition); + + return ret; +} + /* Interrupt a blocking request. */ static gboolean gst_soup_http_src_unlock (GstBaseSrc * bsrc) @@ -1421,10 +1786,8 @@ gst_soup_http_src_unlock (GstBaseSrc * bsrc) src = GST_SOUP_HTTP_SRC (bsrc); GST_DEBUG_OBJECT (src, "unlock()"); - src->interrupted = TRUE; - if (src->loop) - g_main_loop_quit (src->loop); - g_cond_signal (&src->request_finished_cond); + src->ret = GST_FLOW_FLUSHING; + gst_soup_http_src_cancel_message (src); return TRUE; } @@ -1437,7 +1800,8 @@ gst_soup_http_src_unlock_stop (GstBaseSrc * bsrc) src = GST_SOUP_HTTP_SRC (bsrc); GST_DEBUG_OBJECT (src, "unlock_stop()"); - src->interrupted = FALSE; + src->ret = GST_FLOW_OK; + g_cancellable_reset (src->cancellable); return TRUE; } @@ -1469,14 +1833,14 @@ gst_soup_http_src_check_seekable (GstSoupHTTPSrc * src) */ if (!src->got_headers && GST_STATE (src) >= GST_STATE_PAUSED) { g_mutex_lock (&src->mutex); - while (!src->got_headers && !src->interrupted && ret == GST_FLOW_OK) { - if ((src->msg && src->msg->method != SOUP_METHOD_HEAD) && - src->session_io_status != GST_SOUP_HTTP_SRC_SESSION_IO_STATUS_IDLE) { + while (!src->got_headers && !g_cancellable_is_cancelled (src->cancellable) + && ret == GST_FLOW_OK) { + if ((src->msg && src->msg->method != SOUP_METHOD_HEAD)) { /* wait for the current request to finish */ - g_cond_wait (&src->request_finished_cond, &src->mutex); + g_cond_wait (&src->have_headers_cond, &src->mutex); } else { if (gst_soup_http_src_session_open (src)) { - ret = gst_soup_http_src_do_request (src, SOUP_METHOD_HEAD, NULL); + ret = gst_soup_http_src_do_request (src, SOUP_METHOD_HEAD); } } } @@ -1484,11 +1848,8 @@ gst_soup_http_src_check_seekable (GstSoupHTTPSrc * src) /* A HEAD request shouldn't lead to EOS */ src->ret = GST_FLOW_OK; } - /* resets status to idle */ - gst_soup_http_src_cancel_message (src); g_mutex_unlock (&src->mutex); } - } static gboolean @@ -1530,7 +1891,7 @@ gst_soup_http_src_do_seek (GstBaseSrc * bsrc, GstSegment * segment) return FALSE; } - if (src->content_size != 0 && segment->start >= src->content_size) { + if (src->have_size && segment->start >= src->content_size) { GST_WARNING_OBJECT (src, "Potentially seeking behind end of file, might EOS immediately"); } @@ -1538,6 +1899,7 @@ gst_soup_http_src_do_seek (GstBaseSrc * bsrc, GstSegment * segment) /* Wait for create() to handle the jump in offset. */ src->request_position = segment->start; src->stop_position = segment->stop; + return TRUE; } @@ -1552,8 +1914,11 @@ gst_soup_http_src_query (GstBaseSrc * bsrc, GstQuery * query) switch (GST_QUERY_TYPE (query)) { case GST_QUERY_URI: gst_query_set_uri (query, src->location); - if (src->redirection_uri != NULL) + if (src->redirection_uri != NULL) { gst_query_set_uri_redirection (query, src->redirection_uri); + gst_query_set_uri_redirection_permanent (query, + src->redirection_permanent); + } ret = TRUE; break; default: @@ -1617,6 +1982,10 @@ gst_soup_http_src_set_proxy (GstSoupHTTPSrc * src, const gchar * uri) soup_uri_free (src->proxy); src->proxy = NULL; } + + if (uri == NULL || *uri == '\0') + return TRUE; + if (g_str_has_prefix (uri, "http://")) { src->proxy = soup_uri_new (uri); } else { @@ -1626,7 +1995,7 @@ gst_soup_http_src_set_proxy (GstSoupHTTPSrc * src, const gchar * uri) g_free (new_uri); } - return TRUE; + return (src->proxy != NULL); } static guint