gstreamer/subprojects/gst-plugins-good/tests/check/elements/souphttpsrc.c
Philippe Normand c3455def2e soup: Runtime compatibility support for libsoup2 and libsoup3
The src and sink elements no longer link against libsoup. It is now loaded at
runtime. If any version is resident already, it is used. Otherwise we first try
to load libsoup3 and if it's not found we fallback to libsoup2.

For the unit-tests, we now build one version of the test unit file per libsoup
version found. So if both libsoup2 and libsoup3 are available on the host, the
CI will cover them both.

Based on initial patch by Daniel Kolesa <dkolesa@igalia.com> and
Patrick Griffis <pgriffis@igalia.com>.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/1044>
2021-10-13 08:32:25 +00:00

735 lines
20 KiB
C

/* GStreamer unit tests for the souphttpsrc element
* Copyright (C) 2006-2007 Tim-Philipp Müller <tim centricular net>
* Copyright (C) 2008 Wouter Cloetens <wouter@mind.be>
* Copyright (C) 2001-2003, Ximian, Inc.
* Copyright (C) 2021 Igalia S.L.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <stdlib.h>
#include <glib.h>
#include <glib/gprintf.h>
#include <libsoup/soup.h>
#include <gst/check/gstcheck.h>
#if ! SOUP_CHECK_VERSION(3, 0, 0)
#if !defined(SOUP_MINOR_VERSION) || SOUP_MINOR_VERSION < 44
#define SoupStatus SoupKnownStatusCode
#endif
#endif
#if SOUP_CHECK_VERSION(3, 0, 0)
#define SOUP_AUTH_DOMAIN_BASIC_AUTH_CALLBACK "auth-callback"
#define SOUP_AUTH_DOMAIN_DIGEST_AUTH_CALLBACK "auth-callback"
#define gst_soup_uri_free g_uri_unref
#define gst_soup_uri_to_string(x) g_uri_to_string_partial(x, G_URI_HIDE_PASSWORD)
#define gst_soup_uri_get_port(x) g_uri_get_port(x)
#else
#define gst_soup_uri_free soup_uri_free
#define gst_soup_uri_to_string(x) soup_uri_to_string(x, FALSE)
#define gst_soup_uri_get_port(x) soup_uri_get_port(x)
#define SoupServerMessage SoupMessage
#define soup_server_message_get_method(x) (x->method)
#define soup_server_message_get_http_version(x) soup_message_get_http_version(x)
#define soup_server_message_get_status(x) (x->status_code)
#define soup_server_message_set_status(x, s, r) soup_message_set_status(x, s)
#define soup_server_message_get_reason_phrase(x) (x->reason_phrase)
#define soup_server_message_get_uri(x) soup_message_get_uri(x)
#define soup_server_message_get_request_headers(x) (x->request_headers)
#define soup_server_message_get_response_headers(x) (x->response_headers)
#define soup_server_message_get_request_body(x) (x->request_body)
#define soup_server_message_get_response_body(x) (x->response_body)
#endif
gboolean redirect = TRUE;
static const char **cookies = NULL;
/* Variables for authentication tests */
static const char *user_id = NULL;
static const char *user_pw = NULL;
static const char *good_user = "good_user";
static const char *bad_user = "bad_user";
static const char *good_pw = "good_pw";
static const char *bad_pw = "bad_pw";
static const char *realm = "SOUPHTTPSRC_REALM";
static const char *basic_auth_path = "/basic_auth";
static const char *digest_auth_path = "/digest_auth";
static const char *ssl_cert_file = GST_TEST_FILES_PATH "/test-cert.pem";
static const char *ssl_key_file = GST_TEST_FILES_PATH "/test-key.pem";
static guint get_port_from_server (SoupServer * server);
static SoupServer *run_server (gboolean use_https);
static void
handoff_cb (GstElement * fakesink, GstBuffer * buf, GstPad * pad,
GstBuffer ** p_outbuf)
{
GST_LOG ("handoff, buf = %p", buf);
if (*p_outbuf == NULL)
*p_outbuf = gst_buffer_ref (buf);
}
static gboolean
basic_auth_cb (SoupAuthDomain * domain, SoupMessage * msg,
const char *username, const char *password, gpointer user_data)
{
/* There is only one good login for testing */
return (strcmp (username, good_user) == 0)
&& (strcmp (password, good_pw) == 0);
}
static char *
digest_auth_cb (SoupAuthDomain * domain, SoupMessage * msg,
const char *username, gpointer user_data)
{
/* There is only one good login for testing */
if (strcmp (username, good_user) == 0)
return soup_auth_domain_digest_encode_password (good_user, realm, good_pw);
return NULL;
}
static gboolean
run_test (gboolean use_https, const gchar * path, gint expected)
{
GstStateChangeReturn ret;
GstElement *pipe, *src, *sink;
GstBuffer *buf = NULL;
GstMessage *msg;
gchar *url;
gboolean res = FALSE;
SoupServer *server;
guint port;
server = run_server (use_https);
if (server == NULL) {
g_print ("Failed to start up %s server", use_https ? "HTTPS" : "HTTP");
/* skip this test */
return TRUE;
}
pipe = gst_pipeline_new (NULL);
src = gst_element_factory_make ("souphttpsrc", NULL);
fail_unless (src != NULL);
sink = gst_element_factory_make ("fakesink", NULL);
fail_unless (sink != NULL);
gst_bin_add (GST_BIN (pipe), src);
gst_bin_add (GST_BIN (pipe), sink);
fail_unless (gst_element_link (src, sink));
port = get_port_from_server (server);
url = g_strdup_printf ("%s://127.0.0.1:%u%s",
use_https ? "https" : "http", port, path);
fail_unless (url != NULL);
g_object_set (src, "location", url, NULL);
g_free (url);
if (use_https) {
GTlsDatabase *tlsdb;
GError *error = NULL;
gchar *path;
/* GTlsFileDatabase needs an absolute path. Using a relative one
* causes a warning from GLib-Net followed by a segfault in GnuTLS */
if (g_path_is_absolute (ssl_cert_file)) {
path = g_strdup (ssl_cert_file);
} else {
gchar *cwd = g_get_current_dir ();
path = g_build_filename (cwd, ssl_cert_file, NULL);
g_free (cwd);
}
tlsdb = g_tls_file_database_new (path, &error);
fail_unless (tlsdb, "Failed to load certificate: %s", error->message);
g_object_set (src, "tls-database", tlsdb, NULL);
g_object_unref (tlsdb);
g_free (path);
}
g_object_set (src, "automatic-redirect", redirect, NULL);
if (cookies != NULL)
g_object_set (src, "cookies", cookies, NULL);
g_object_set (sink, "signal-handoffs", TRUE, NULL);
g_signal_connect (sink, "preroll-handoff", G_CALLBACK (handoff_cb), &buf);
if (user_id != NULL)
g_object_set (src, "user-id", user_id, NULL);
if (user_pw != NULL)
g_object_set (src, "user-pw", user_pw, NULL);
ret = gst_element_set_state (pipe, GST_STATE_PAUSED);
if (ret != GST_STATE_CHANGE_ASYNC) {
GST_DEBUG ("failed to start up soup http src, ret = %d", ret);
goto done;
}
gst_element_set_state (pipe, GST_STATE_PLAYING);
msg = gst_bus_poll (GST_ELEMENT_BUS (pipe),
GST_MESSAGE_EOS | GST_MESSAGE_ERROR, -1);
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) {
gchar *debug = NULL;
GError *err = NULL;
gint rc = -1;
gst_message_parse_error (msg, &err, &debug);
GST_INFO ("error: %s", err->message);
if (g_str_has_suffix (err->message, "Not Found"))
rc = 404;
else if (g_str_has_suffix (err->message, "Forbidden"))
rc = 403;
else if (g_str_has_suffix (err->message, "Unauthorized"))
rc = 401;
else if (g_str_has_suffix (err->message, "Found"))
rc = 302;
GST_INFO ("debug: %s", debug);
/* should not've gotten any output in case of a 40x error. Wait a bit
* to give the streaming thread a chance to push out a buffer and trigger
* our callback before shutting down the pipeline */
g_usleep (G_USEC_PER_SEC / 2);
fail_unless (buf == NULL);
g_error_free (err);
g_free (debug);
gst_message_unref (msg);
GST_DEBUG ("Got HTTP error %u, expected %u", rc, expected);
res = (rc == expected);
goto done;
}
gst_message_unref (msg);
/* don't wait for more than 10 seconds */
ret = gst_element_get_state (pipe, NULL, NULL, 10 * GST_SECOND);
GST_LOG ("ret = %u", ret);
if (buf == NULL) {
/* we want to test the buffer offset, nothing else; if there's a failure
* it might be for lots of reasons (no network connection, whatever), we're
* not interested in those */
GST_DEBUG ("didn't manage to get data within 10 seconds, skipping test");
res = TRUE;
goto done;
}
GST_DEBUG ("buffer offset = %" G_GUINT64_FORMAT, GST_BUFFER_OFFSET (buf));
/* first buffer should have a 0 offset */
fail_unless (GST_BUFFER_OFFSET (buf) == 0);
gst_buffer_unref (buf);
res = (expected == 0);
done:
gst_element_set_state (pipe, GST_STATE_NULL);
gst_object_unref (pipe);
gst_object_unref (server);
return res;
}
GST_START_TEST (test_first_buffer_has_offset)
{
fail_unless (run_test (FALSE, "/", 0));
}
GST_END_TEST;
GST_START_TEST (test_not_found)
{
fail_unless (run_test (FALSE, "/404", 404));
fail_unless (run_test (FALSE, "/404-with-data", 404));
}
GST_END_TEST;
GST_START_TEST (test_forbidden)
{
fail_unless (run_test (FALSE, "/403", 403));
}
GST_END_TEST;
GST_START_TEST (test_redirect_no)
{
redirect = FALSE;
fail_unless (run_test (FALSE, "/302", 302));
}
GST_END_TEST;
GST_START_TEST (test_redirect_yes)
{
redirect = TRUE;
fail_unless (run_test (FALSE, "/302", 0));
}
GST_END_TEST;
GST_START_TEST (test_https)
{
fail_unless (run_test (TRUE, "/", 0));
}
GST_END_TEST;
GST_START_TEST (test_cookies)
{
static const char *biscotti[] = { "delacre=yummie", "koekje=lu", NULL };
gboolean res;
cookies = biscotti;
res = run_test (FALSE, "/", 0);
cookies = NULL;
fail_unless (res);
}
GST_END_TEST;
GST_START_TEST (test_good_user_basic_auth)
{
gboolean res;
user_id = good_user;
user_pw = good_pw;
res = run_test (FALSE, basic_auth_path, 0);
GST_DEBUG ("Basic Auth user %s password %s res = %d", user_id, user_pw, res);
user_id = user_pw = NULL;
fail_unless (res);
}
GST_END_TEST;
GST_START_TEST (test_bad_user_basic_auth)
{
gboolean res;
user_id = bad_user;
user_pw = good_pw;
res = run_test (FALSE, basic_auth_path, 401);
GST_DEBUG ("Basic Auth user %s password %s res = %d", user_id, user_pw, res);
user_id = user_pw = NULL;
fail_unless (res);
}
GST_END_TEST;
GST_START_TEST (test_bad_password_basic_auth)
{
gboolean res;
user_id = good_user;
user_pw = bad_pw;
res = run_test (FALSE, basic_auth_path, 401);
GST_DEBUG ("Basic Auth user %s password %s res = %d", user_id, user_pw, res);
user_id = user_pw = NULL;
fail_unless (res);
}
GST_END_TEST;
GST_START_TEST (test_good_user_digest_auth)
{
gboolean res;
user_id = good_user;
user_pw = good_pw;
res = run_test (FALSE, digest_auth_path, 0);
GST_DEBUG ("Digest Auth user %s password %s res = %d", user_id, user_pw, res);
user_id = user_pw = NULL;
fail_unless (res);
}
GST_END_TEST;
GST_START_TEST (test_bad_user_digest_auth)
{
gboolean res;
user_id = bad_user;
user_pw = good_pw;
res = run_test (FALSE, digest_auth_path, 401);
GST_DEBUG ("Digest Auth user %s password %s res = %d", user_id, user_pw, res);
user_id = user_pw = NULL;
fail_unless (res);
}
GST_END_TEST;
GST_START_TEST (test_bad_password_digest_auth)
{
gboolean res;
user_id = good_user;
user_pw = bad_pw;
res = run_test (FALSE, digest_auth_path, 401);
GST_DEBUG ("Digest Auth user %s password %s res = %d", user_id, user_pw, res);
user_id = user_pw = NULL;
fail_unless (res);
}
GST_END_TEST;
static gboolean icy_caps = FALSE;
static void
got_buffer (GstElement * fakesink, GstBuffer * buf, GstPad * pad,
gpointer user_data)
{
GstStructure *s;
GstCaps *caps;
/* Caps can be anything if we don't except icy caps */
if (!icy_caps)
return;
/* Otherwise they _must_ be "application/x-icy" */
caps = gst_pad_get_current_caps (pad);
fail_unless (caps != NULL);
s = gst_caps_get_structure (caps, 0);
fail_unless_equals_string (gst_structure_get_name (s), "application/x-icy");
gst_caps_unref (caps);
}
GST_START_TEST (test_icy_stream)
{
GstElement *pipe, *src, *sink;
GstMessage *msg;
pipe = gst_pipeline_new (NULL);
src = gst_element_factory_make ("souphttpsrc", NULL);
fail_unless (src != NULL);
sink = gst_element_factory_make ("fakesink", NULL);
fail_unless (sink != NULL);
g_object_set (sink, "signal-handoffs", TRUE, NULL);
g_signal_connect (sink, "handoff", G_CALLBACK (got_buffer), NULL);
gst_bin_add (GST_BIN (pipe), src);
gst_bin_add (GST_BIN (pipe), sink);
fail_unless (gst_element_link (src, sink));
/* Radionomy Hot40Music shoutcast stream */
g_object_set (src, "location",
"http://streaming.radionomy.com:80/Hot40Music", NULL);
/* EOS after the first buffer */
g_object_set (src, "num-buffers", 1, NULL);
icy_caps = TRUE;
gst_element_set_state (pipe, GST_STATE_PLAYING);
msg = gst_bus_poll (GST_ELEMENT_BUS (pipe),
GST_MESSAGE_EOS | GST_MESSAGE_ERROR, -1);
switch (GST_MESSAGE_TYPE (msg)) {
case GST_MESSAGE_EOS:
GST_DEBUG ("success, we're done here");
gst_message_unref (msg);
break;
case GST_MESSAGE_ERROR:{
GError *err = NULL;
gst_message_parse_error (msg, &err, NULL);
GST_INFO ("Error with ICY mp3 shoutcast stream: %s", err->message);
gst_message_unref (msg);
g_clear_error (&err);
break;
}
default:
break;
}
icy_caps = FALSE;
gst_element_set_state (pipe, GST_STATE_NULL);
gst_object_unref (pipe);
}
GST_END_TEST;
static Suite *
souphttpsrc_suite (void)
{
TCase *tc_chain, *tc_internet;
Suite *s;
/* we don't support exceptions from the proxy, so just unset the environment
* variable - in case it's set in the test environment it would otherwise
* prevent us from connecting to localhost (like jenkins.qa.ubuntu.com) */
g_unsetenv ("http_proxy");
s = suite_create ("souphttpsrc");
tc_chain = tcase_create ("general");
tc_internet = tcase_create ("internet");
suite_add_tcase (s, tc_chain);
tcase_add_test (tc_chain, test_first_buffer_has_offset);
tcase_add_test (tc_chain, test_redirect_yes);
tcase_add_test (tc_chain, test_redirect_no);
tcase_add_test (tc_chain, test_not_found);
tcase_add_test (tc_chain, test_forbidden);
tcase_add_test (tc_chain, test_cookies);
tcase_add_test (tc_chain, test_good_user_basic_auth);
tcase_add_test (tc_chain, test_bad_user_basic_auth);
tcase_add_test (tc_chain, test_bad_password_basic_auth);
tcase_add_test (tc_chain, test_good_user_digest_auth);
tcase_add_test (tc_chain, test_bad_user_digest_auth);
tcase_add_test (tc_chain, test_bad_password_digest_auth);
tcase_add_test (tc_chain, test_https);
suite_add_tcase (s, tc_internet);
tcase_set_timeout (tc_internet, 250);
tcase_add_test (tc_internet, test_icy_stream);
return s;
}
GST_CHECK_MAIN (souphttpsrc);
static void
do_get (SoupServerMessage * msg, const char *path)
{
gboolean send_error_doc = FALSE;
char *uri;
int buflen = 4096;
SoupStatus status = SOUP_STATUS_OK;
uri = gst_soup_uri_to_string (soup_server_message_get_uri (msg));
GST_DEBUG ("request: \"%s\"", uri);
if (!strcmp (path, "/301"))
status = SOUP_STATUS_MOVED_PERMANENTLY;
else if (!strcmp (path, "/302"))
status = SOUP_STATUS_MOVED_TEMPORARILY;
else if (!strcmp (path, "/307"))
status = SOUP_STATUS_TEMPORARY_REDIRECT;
else if (!strcmp (path, "/403"))
status = SOUP_STATUS_FORBIDDEN;
else if (!strcmp (path, "/404"))
status = SOUP_STATUS_NOT_FOUND;
else if (!strcmp (path, "/404-with-data")) {
status = SOUP_STATUS_NOT_FOUND;
send_error_doc = TRUE;
}
if (SOUP_STATUS_IS_REDIRECTION (status)) {
char *redir_uri;
redir_uri = g_strdup_printf ("%s-redirected", uri);
soup_message_headers_append (soup_server_message_get_response_headers (msg),
"Location", redir_uri);
g_free (redir_uri);
}
if (status != (SoupStatus) SOUP_STATUS_OK && !send_error_doc)
goto leave;
if (soup_server_message_get_method (msg) == SOUP_METHOD_GET) {
char *buf;
buf = g_malloc (buflen);
memset (buf, 0, buflen);
soup_message_body_append (soup_server_message_get_response_body (msg),
SOUP_MEMORY_TAKE, buf, buflen);
} else { /* method == SOUP_METHOD_HEAD */
char *length;
/* We could just use the same code for both GET and
* HEAD. But we'll optimize and avoid the extra
* malloc.
*/
length = g_strdup_printf ("%lu", (gulong) buflen);
soup_message_headers_append (soup_server_message_get_response_headers (msg),
"Content-Length", length);
g_free (length);
}
leave:
soup_server_message_set_status (msg, status, NULL);
g_free (uri);
}
static void
print_header (const char *name, const char *value, gpointer data)
{
GST_DEBUG ("header: %s: %s", name, value);
}
static void
server_callback (SoupServer * server, SoupServerMessage * msg,
const char *path, GHashTable * query,
#if !SOUP_CHECK_VERSION(3, 0, 0)
SoupClientContext * context,
#endif
gpointer data)
{
const char *method = soup_server_message_get_method (msg);
GST_DEBUG ("%s %s HTTP/1.%d", method, path,
soup_server_message_get_http_version (msg));
soup_message_headers_foreach (soup_server_message_get_request_headers (msg),
print_header, NULL);
if (soup_server_message_get_request_body (msg)->length)
GST_DEBUG ("%s", soup_server_message_get_request_body (msg)->data);
if (method == SOUP_METHOD_GET || method == SOUP_METHOD_HEAD)
do_get (msg, path);
else
soup_server_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED, NULL);
GST_DEBUG (" -> %d %s", soup_server_message_get_status (msg),
soup_server_message_get_reason_phrase (msg));
}
static guint
get_port_from_server (SoupServer * server)
{
GSList *uris;
guint port;
uris = soup_server_get_uris (server);
g_assert (g_slist_length (uris) == 1);
port = gst_soup_uri_get_port (uris->data);
g_slist_free_full (uris, (GDestroyNotify) gst_soup_uri_free);
return port;
}
static SoupServer *
run_server (gboolean use_https)
{
SoupServer *server = soup_server_new (NULL, NULL);
SoupServerListenOptions listen_flags = 0;
guint port;
if (use_https) {
GTlsBackend *backend = g_tls_backend_get_default ();
GError *err = NULL;
if (backend == NULL || !g_tls_backend_supports_tls (backend)) {
GST_INFO ("No TLS support");
g_object_unref (server);
return NULL;
}
#if SOUP_CHECK_VERSION(3, 0, 0)
{
GTlsCertificate *cert =
g_tls_certificate_new_from_files (ssl_cert_file, ssl_key_file,
&err);
if (!cert) {
GST_INFO ("Failed to load certificate: %s", err->message);
g_error_free (err);
return NULL;
}
soup_server_set_tls_certificate (server, cert);
g_object_unref (cert);
}
#else
if (!soup_server_set_ssl_cert_file (server, ssl_cert_file, ssl_key_file,
&err)) {
GST_INFO ("Failed to load certificate: %s", err->message);
g_object_unref (server);
g_error_free (err);
return NULL;
}
#endif
listen_flags |= SOUP_SERVER_LISTEN_HTTPS;
}
soup_server_add_handler (server, NULL, server_callback, NULL, NULL);
{
SoupAuthDomain *domain;
domain = soup_auth_domain_basic_new ("realm", realm,
SOUP_AUTH_DOMAIN_BASIC_AUTH_CALLBACK, basic_auth_cb, NULL);
soup_auth_domain_add_path (domain, basic_auth_path);
soup_server_add_auth_domain (server, domain);
g_object_unref (domain);
domain = soup_auth_domain_digest_new ("realm", realm,
SOUP_AUTH_DOMAIN_DIGEST_AUTH_CALLBACK, digest_auth_cb, NULL);
soup_auth_domain_add_path (domain, digest_auth_path);
soup_server_add_auth_domain (server, domain);
g_object_unref (domain);
}
{
GSocketAddress *address;
GError *err = NULL;
address = g_inet_socket_address_new_from_string ("0.0.0.0", 0);
soup_server_listen (server, address, listen_flags, &err);
g_object_unref (address);
if (err) {
GST_ERROR ("Failed to start %s server: %s",
use_https ? "HTTPS" : "HTTP", err->message);
g_object_unref (server);
g_error_free (err);
return NULL;
}
}
port = get_port_from_server (server);
GST_DEBUG ("%s server listening on port %u", use_https ? "HTTPS" : "HTTP",
port);
/* check if we can connect to our local http server */
{
GSocketConnection *conn;
GSocketClient *client;
client = g_socket_client_new ();
g_socket_client_set_timeout (client, 2);
conn =
g_socket_client_connect_to_host (client, "127.0.0.1", port, NULL, NULL);
if (conn == NULL) {
GST_INFO ("Couldn't connect to 127.0.0.1:%u", port);
g_object_unref (client);
g_object_unref (server);
return NULL;
}
g_object_unref (conn);
g_object_unref (client);
}
return server;
}