rtmp2: Add llnw auth support to rtmp client

Add support for Limelight CDN (llnw) authentication. Inspired
by the ffmpeg implementation of llnw auth.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/7410>
This commit is contained in:
Daniel Pendse 2024-08-09 09:41:07 +00:00 committed by GStreamer Marge Bot
parent 5ca52ea026
commit e4fbf9d180
3 changed files with 137 additions and 23 deletions

View file

@ -236545,19 +236545,24 @@
"kind": "enum",
"values": [
{
"desc": "GST_RTMP_AUTHMOD_NONE",
"desc": "Attempt no authentication",
"name": "none",
"value": "0"
},
{
"desc": "GST_RTMP_AUTHMOD_AUTO",
"desc": "Automatically switch to server-suggested method",
"name": "auto",
"value": "1"
},
{
"desc": "GST_RTMP_AUTHMOD_ADOBE",
"desc": "Adobe-style authentication",
"name": "adobe",
"value": "2"
},
{
"desc": "Limelight Networks authentication",
"name": "llnw",
"value": "3"
}
]
},

View file

@ -153,9 +153,16 @@ gst_rtmp_authmod_get_type (void)
{
static gsize authmod_type = 0;
static const GEnumValue authmod[] = {
{GST_RTMP_AUTHMOD_NONE, "GST_RTMP_AUTHMOD_NONE", "none"},
{GST_RTMP_AUTHMOD_AUTO, "GST_RTMP_AUTHMOD_AUTO", "auto"},
{GST_RTMP_AUTHMOD_ADOBE, "GST_RTMP_AUTHMOD_ADOBE", "adobe"},
{GST_RTMP_AUTHMOD_NONE, "Attempt no authentication", "none"},
{GST_RTMP_AUTHMOD_AUTO, "Automatically switch to server-suggested method",
"auto"},
{GST_RTMP_AUTHMOD_ADOBE, "Adobe-style authentication", "adobe"},
/**
* GstRtmpAuthmod::llnw:
*
* Since: 1.26
*/
{GST_RTMP_AUTHMOD_LLNW, "Limelight Networks authentication", "llnw"},
{0, NULL, NULL},
};
@ -591,6 +598,68 @@ do_adobe_auth (const gchar * username, const gchar * password,
return auth_query;
}
static gchar *
do_llnw_auth (const gchar * username, const gchar * password,
const gchar * nonce, const gchar * app)
{
const gchar *nc = "00000001";
gchar cnonce[10];
gchar *auth_query;
GChecksum *md5_1, *md5_2, *md5_3;
/* FIXME: Handle case were nonce is NULL */
g_return_val_if_fail (username, NULL);
g_return_val_if_fail (password, NULL);
g_return_val_if_fail (app, NULL);
md5_1 = g_checksum_new (G_CHECKSUM_MD5);
g_checksum_update (md5_1, (guchar *) username, -1);
g_checksum_update (md5_1, (guchar *) ":live:", -1); /* realm */
g_checksum_update (md5_1, (guchar *) password, -1);
md5_2 = g_checksum_new (G_CHECKSUM_MD5);
/* FIXME: Might be possible to use play method in rtmp2src */
g_checksum_update (md5_2, (guchar *) "publish:/", -1); /* method */
g_checksum_update (md5_2, (guchar *) app, -1);
/*
* When streaming to limelight, the app name is either a full
* "appname/subaccount" or "appname/_definst_". In the latter case, the app
* name can be simplified into simply "appname", but the authentication
* hashing assumes the /_definst_ still to be present.
*/
if (!strchr (app, '/'))
g_checksum_update (md5_2, (guchar *) "/_definst_", -1);
md5_3 = g_checksum_new (G_CHECKSUM_MD5);
g_checksum_update (md5_3, (guchar *) g_checksum_get_string (md5_1), -1);
g_checksum_update (md5_3, (guchar *) ":", 1);
if (nonce)
g_checksum_update (md5_3, (guchar *) nonce, -1);
g_checksum_update (md5_3, (guchar *) ":", -1);
g_checksum_update (md5_3, (guchar *) nc, -1);
g_checksum_update (md5_3, (guchar *) ":", -1);
g_snprintf (cnonce, sizeof (cnonce), "%08x", g_random_int ());
g_checksum_update (md5_3, (guchar *) cnonce, -1);
g_checksum_update (md5_3, (guchar *) ":auth:", -1);
g_checksum_update (md5_3, (guchar *) g_checksum_get_string (md5_2), -1);
auth_query =
g_strdup_printf
("authmod=llnw&user=%s&nonce=%s&cnonce=%s&nc=%s&response=%s",
username, nonce ? nonce : "(null)", cnonce, nc,
g_checksum_get_string (md5_3));
g_checksum_free (md5_1);
g_checksum_free (md5_2);
g_checksum_free (md5_3);
return auth_query;
}
static GstAmfNode *
parse_conn_token (gchar type, const gchar * value, GError ** error)
{
@ -758,20 +827,20 @@ send_connect (GTask * task)
const gchar *query = data->auth_query;
appstr = g_strdup_printf ("%s?%s", app, query);
uristr = g_strdup_printf ("%s?%s", uri, query);
} else if (data->location.authmod == GST_RTMP_AUTHMOD_ADOBE) {
} else if (data->location.authmod > GST_RTMP_AUTHMOD_AUTO) {
const gchar *user = data->location.username;
const gchar *authmod = "adobe";
const gchar *authmod = gst_rtmp_authmod_get_nick (data->location.authmod);
if (!user) {
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED,
"no username for adobe authentication");
"no username for %s authentication", authmod);
g_object_unref (task);
goto out;
}
if (!data->location.password) {
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED,
"no password for adobe authentication");
"no password for %s authentication", authmod);
g_object_unref (task);
goto out;
}
@ -933,6 +1002,13 @@ send_connect_done (const gchar * command_name, GPtrArray * args,
return;
}
if (strstr (desc, "authmod=llnw")) {
GST_INFO ("Reconnecting with authmod=llnw");
data->location.authmod = GST_RTMP_AUTHMOD_LLNW;
socket_connect (task);
return;
}
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED,
"'connect' cmd returned unhandled authmod: %s", desc);
g_object_unref (task);
@ -960,6 +1036,10 @@ send_connect_done (const gchar * command_name, GPtrArray * args,
matches = g_str_equal (authmod_str, "adobe");
break;
case GST_RTMP_AUTHMOD_LLNW:
matches = g_str_equal (authmod_str, "llnw");
break;
default:
matches = FALSE;
break;
@ -1016,12 +1096,13 @@ send_connect_done (const gchar * command_name, GPtrArray * args,
}
}
{
if (authmod == GST_RTMP_AUTHMOD_ADOBE) {
const gchar *salt, *opaque, *challenge;
salt = gst_uri_get_query_value (query, "salt");
if (!salt) {
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED,
g_task_return_new_error (task, G_IO_ERROR,
G_IO_ERROR_PERMISSION_DENIED,
"salt missing from auth request: %s", desc);
g_object_unref (task);
gst_uri_unref (query);
@ -1034,19 +1115,39 @@ send_connect_done (const gchar * command_name, GPtrArray * args,
g_warn_if_fail (!data->auth_query);
data->auth_query = do_adobe_auth (data->location.username,
data->location.password, salt, opaque, challenge);
}
gst_uri_unref (query);
gst_uri_unref (query);
if (!data->auth_query) {
/* do_adobe_auth should not fail; send_connect tests if username
* and password are provided */
g_warn_if_reached ();
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
"internal error: failed to generate adobe auth query");
g_object_unref (task);
return;
}
if (!data->auth_query) {
/* do_adobe_auth should not fail; send_connect tests if username
* and password are provided */
g_warn_if_reached ();
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
"internal error: failed to generate adobe auth query");
g_object_unref (task);
return;
}
} else if (authmod == GST_RTMP_AUTHMOD_LLNW) {
const gchar *nonce;
nonce = gst_uri_get_query_value (query, "nonce");
g_warn_if_fail (!data->auth_query);
data->auth_query = do_llnw_auth (data->location.username,
data->location.password, nonce, data->location.application);
gst_uri_unref (query);
if (!data->auth_query) {
/* do_llnw_auth should not fail; send_connect tests if username
* and password are provided */
g_warn_if_reached ();
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
"internal error: failed to generate llnw auth query");
g_object_unref (task);
return;
}
} else
g_return_if_reached ();
socket_connect (task);
return;

View file

@ -45,11 +45,19 @@ guint gst_rtmp_scheme_get_default_port (GstRtmpScheme scheme);
#define GST_TYPE_RTMP_AUTHMOD (gst_rtmp_authmod_get_type ())
/**
* GstRtmpAuthmod:
* @GST_RTMP_AUTHMOD_NONE: Attempt no authentication.
* @GST_RTMP_AUTHMOD_AUTO: Automatically switch to server-suggested method.
* @GST_RTMP_AUTHMOD_ADOBE: Adobe-style authentication.
* @GST_RTMP_AUTHMOD_LLNW: Limelight Networks authentication. (Since 1.26)
*/
typedef enum
{
GST_RTMP_AUTHMOD_NONE = 0,
GST_RTMP_AUTHMOD_AUTO,
GST_RTMP_AUTHMOD_ADOBE,
GST_RTMP_AUTHMOD_LLNW,
} GstRtmpAuthmod;
GType gst_rtmp_authmod_get_type (void);