From 08225d01262b638e1c4c86679a1375e02123fd4d Mon Sep 17 00:00:00 2001 From: Martin Storsjö Date: Sun, 30 Dec 2012 22:39:38 +0200 Subject: rtmp: Add support for adobe authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is mostly used to authenticate the client when publishing. Tested with wowza and akamai. Some but not all servers support resending a new connect invoke within the same connection, so always reconnect for sending a new connection attempt. This matches what other applications do as well. The authentication scheme is structurally pretty similar to http digest authentication, but uses base64 instead of hex strings. Signed-off-by: Martin Storsjö --- libavformat/rtmpproto.c | 163 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 159 insertions(+), 4 deletions(-) (limited to 'libavformat/rtmpproto.c') diff --git a/libavformat/rtmpproto.c b/libavformat/rtmpproto.c index 4e059d6674..1ffa748b25 100644 --- a/libavformat/rtmpproto.c +++ b/libavformat/rtmpproto.c @@ -26,8 +26,10 @@ #include "libavcodec/bytestream.h" #include "libavutil/avstring.h" +#include "libavutil/base64.h" #include "libavutil/intfloat.h" #include "libavutil/lfg.h" +#include "libavutil/md5.h" #include "libavutil/opt.h" #include "libavutil/random_seed.h" #include "libavutil/sha.h" @@ -116,6 +118,11 @@ typedef struct RTMPContext { int listen; ///< listen mode flag int listen_timeout; ///< listen timeout to wait for new connections int nb_streamid; ///< The next stream id to return on createStream calls + char username[50]; + char password[50]; + char auth_params[500]; + int do_reconnect; + int auth_tried; } RTMPContext; #define PLAYER_KEY_OPEN_PART_LEN 30 ///< length of partial key used for first client digest signing @@ -202,6 +209,9 @@ static void free_tracked_methods(RTMPContext *rt) for (i = 0; i < rt->nb_tracked_methods; i ++) av_free(rt->tracked_methods[i].name); av_free(rt->tracked_methods); + rt->tracked_methods = NULL; + rt->tracked_methods_size = 0; + rt->nb_tracked_methods = 0; } static int rtmp_send_packet(RTMPContext *rt, RTMPPacket *pkt, int track) @@ -314,7 +324,7 @@ static int gen_connect(URLContext *s, RTMPContext *rt) ff_amf_write_number(&p, ++rt->nb_invokes); ff_amf_write_object_start(&p); ff_amf_write_field_name(&p, "app"); - ff_amf_write_string(&p, rt->app); + ff_amf_write_string2(&p, rt->app, rt->auth_params); if (!rt->is_input) { ff_amf_write_field_name(&p, "type"); @@ -329,7 +339,7 @@ static int gen_connect(URLContext *s, RTMPContext *rt) } ff_amf_write_field_name(&p, "tcUrl"); - ff_amf_write_string(&p, rt->tcurl); + ff_amf_write_string2(&p, rt->tcurl, rt->auth_params); if (rt->is_input) { ff_amf_write_field_name(&p, "fpad"); ff_amf_write_bool(&p, 0); @@ -1512,8 +1522,122 @@ static int handle_server_bw(URLContext *s, RTMPPacket *pkt) return 0; } +static int do_adobe_auth(RTMPContext *rt, const char *user, const char *salt, + const char *opaque, const char *challenge) +{ + uint8_t hash[16]; + char hashstr[AV_BASE64_SIZE(sizeof(hash))], challenge2[10]; + struct AVMD5 *md5 = av_md5_alloc(); + if (!md5) + return AVERROR(ENOMEM); + + snprintf(challenge2, sizeof(challenge2), "%08x", av_get_random_seed()); + + av_md5_init(md5); + av_md5_update(md5, user, strlen(user)); + av_md5_update(md5, salt, strlen(salt)); + av_md5_update(md5, rt->password, strlen(rt->password)); + av_md5_final(md5, hash); + av_base64_encode(hashstr, sizeof(hashstr), hash, + sizeof(hash)); + av_md5_init(md5); + av_md5_update(md5, hashstr, strlen(hashstr)); + if (opaque) + av_md5_update(md5, opaque, strlen(opaque)); + else if (challenge) + av_md5_update(md5, challenge, strlen(challenge)); + av_md5_update(md5, challenge2, strlen(challenge2)); + av_md5_final(md5, hash); + av_base64_encode(hashstr, sizeof(hashstr), hash, + sizeof(hash)); + snprintf(rt->auth_params, sizeof(rt->auth_params), + "?authmod=%s&user=%s&challenge=%s&response=%s", + "adobe", user, challenge2, hashstr); + if (opaque) + av_strlcatf(rt->auth_params, sizeof(rt->auth_params), + "&opaque=%s", opaque); + + av_free(md5); + return 0; +} + +static int handle_connect_error(URLContext *s, const char *desc) +{ + RTMPContext *rt = s->priv_data; + char buf[300], *ptr; + int i = 0, ret = 0; + const char *user = "", *salt = "", *opaque = NULL, + *challenge = NULL, *cptr = NULL; + + if (!(cptr = strstr(desc, "authmod=adobe"))) { + av_log(s, AV_LOG_ERROR, + "Unknown connect error (unsupported authentication method?)\n"); + return AVERROR_UNKNOWN; + } + + if (!rt->username[0] || !rt->password[0]) { + av_log(s, AV_LOG_ERROR, "No credentials set\n"); + return AVERROR_UNKNOWN; + } + + if (strstr(desc, "?reason=authfailed")) { + av_log(s, AV_LOG_ERROR, "Incorrect username/password\n"); + return AVERROR_UNKNOWN; + } else if (strstr(desc, "?reason=nosuchuser")) { + av_log(s, AV_LOG_ERROR, "Incorrect username\n"); + return AVERROR_UNKNOWN; + } + + if (rt->auth_tried) { + av_log(s, AV_LOG_ERROR, "Authentication failed\n"); + return AVERROR_UNKNOWN; + } + + rt->auth_params[0] = '\0'; + + if (strstr(desc, "code=403 need auth")) { + snprintf(rt->auth_params, sizeof(rt->auth_params), + "?authmod=%s&user=%s", "adobe", rt->username); + return 0; + } + + if (!(cptr = strstr(desc, "?reason=needauth"))) { + av_log(s, AV_LOG_ERROR, "No auth parameters found\n"); + return AVERROR_UNKNOWN; + } + + av_strlcpy(buf, cptr + 1, sizeof(buf)); + ptr = buf; + + while (ptr) { + char *next = strchr(ptr, '&'); + char *value = strchr(ptr, '='); + if (next) + *next++ = '\0'; + if (value) + *value++ = '\0'; + if (!strcmp(ptr, "user")) { + user = value; + } else if (!strcmp(ptr, "salt")) { + salt = value; + } else if (!strcmp(ptr, "opaque")) { + opaque = value; + } else if (!strcmp(ptr, "challenge")) { + challenge = value; + } + ptr = next; + } + + if ((ret = do_adobe_auth(rt, user, salt, challenge, opaque)) < 0) + return ret; + + rt->auth_tried = 1; + return 0; +} + static int handle_invoke_error(URLContext *s, RTMPPacket *pkt) { + RTMPContext *rt = s->priv_data; const uint8_t *data_end = pkt->data + pkt->data_size; char *tracked_method = NULL; int level = AV_LOG_ERROR; @@ -1532,6 +1656,12 @@ static int handle_invoke_error(URLContext *s, RTMPPacket *pkt) /* Gracefully ignore Adobe-specific historical artifact errors. */ level = AV_LOG_WARNING; ret = 0; + } else if (tracked_method && !strcmp(tracked_method, "connect")) { + ret = handle_connect_error(s, tmpstr); + if (!ret) { + rt->do_reconnect = 1; + level = AV_LOG_VERBOSE; + } } else ret = AVERROR_UNKNOWN; av_log(s, level, "Server error: %s\n", tmpstr); @@ -1958,6 +2088,10 @@ static int get_packet(URLContext *s, int for_header) ff_rtmp_packet_destroy(&rpkt); return ret; } + if (rt->do_reconnect && for_header) { + ff_rtmp_packet_destroy(&rpkt); + return 0; + } if (rt->state == STATE_STOPPED) { ff_rtmp_packet_destroy(&rpkt); return AVERROR_EOF; @@ -2060,7 +2194,7 @@ static int rtmp_close(URLContext *h) static int rtmp_open(URLContext *s, const char *uri, int flags) { RTMPContext *rt = s->priv_data; - char proto[8], hostname[256], path[1024], *fname; + char proto[8], hostname[256], path[1024], auth[100], *fname; char *old_app; uint8_t buf[2048]; int port; @@ -2072,9 +2206,19 @@ static int rtmp_open(URLContext *s, const char *uri, int flags) rt->is_input = !(flags & AVIO_FLAG_WRITE); - av_url_split(proto, sizeof(proto), NULL, 0, hostname, sizeof(hostname), &port, + av_url_split(proto, sizeof(proto), auth, sizeof(auth), + hostname, sizeof(hostname), &port, path, sizeof(path), s->filename); + if (auth[0]) { + char *ptr = strchr(auth, ':'); + if (ptr) { + *ptr = '\0'; + av_strlcpy(rt->username, auth, sizeof(rt->username)); + av_strlcpy(rt->password, ptr + 1, sizeof(rt->password)); + } + } + if (rt->listen && strcmp(proto, "rtmp")) { av_log(s, AV_LOG_ERROR, "rtmp_listen not available for %s\n", proto); @@ -2110,6 +2254,7 @@ static int rtmp_open(URLContext *s, const char *uri, int flags) ff_url_join(buf, sizeof(buf), "tcp", NULL, hostname, port, NULL); } +reconnect: if ((ret = ffurl_open(&rt->stream, buf, AVIO_FLAG_READ_WRITE, &s->interrupt_callback, &opts)) < 0) { av_log(s , AV_LOG_ERROR, "Cannot open connection %s\n", buf); @@ -2239,6 +2384,16 @@ static int rtmp_open(URLContext *s, const char *uri, int flags) if (ret < 0) goto fail; + if (rt->do_reconnect) { + ffurl_close(rt->stream); + rt->stream = NULL; + rt->do_reconnect = 0; + rt->nb_invokes = 0; + memset(rt->prev_pkt, 0, sizeof(rt->prev_pkt)); + free_tracked_methods(rt); + goto reconnect; + } + if (rt->is_input) { // generate FLV header for demuxer rt->flv_size = 13; -- cgit v1.2.3 From c1ea44c54d837a69d8285601cfba7aa8df16e053 Mon Sep 17 00:00:00 2001 From: Martin Storsjö Date: Mon, 31 Dec 2012 00:46:14 +0200 Subject: rtmp: Add support for limelight authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Limelight is a not too uncommon CDN. The authentication scheme is pretty similar to the adobe authentication, but is even closer to normal http digest authentication (but not close enough to warrant sharing code) than the adobe version. Signed-off-by: Martin Storsjö --- Changelog | 2 +- libavformat/rtmpproto.c | 81 +++++++++++++++++++++++++++++++++++++++++++++---- libavformat/version.h | 2 +- 3 files changed, 77 insertions(+), 8 deletions(-) (limited to 'libavformat/rtmpproto.c') diff --git a/Changelog b/Changelog index fa8cd48955..f9f0f7ed4d 100644 --- a/Changelog +++ b/Changelog @@ -3,7 +3,7 @@ releases are sorted from youngest to oldest. version : - av_basename and av_dirname -- adobe publisher authentication in RTMP +- adobe and limelight publisher authentication in RTMP version 9_beta3: - ashowinfo audio filter diff --git a/libavformat/rtmpproto.c b/libavformat/rtmpproto.c index 1ffa748b25..f25a79b0e3 100644 --- a/libavformat/rtmpproto.c +++ b/libavformat/rtmpproto.c @@ -1561,19 +1561,81 @@ static int do_adobe_auth(RTMPContext *rt, const char *user, const char *salt, return 0; } +static int do_llnw_auth(RTMPContext *rt, const char *user, const char *nonce) +{ + uint8_t hash[16]; + char hashstr1[33], hashstr2[33]; + const char *realm = "live"; + const char *method = "publish"; + const char *qop = "auth"; + const char *nc = "00000001"; + char cnonce[10]; + struct AVMD5 *md5 = av_md5_alloc(); + if (!md5) + return AVERROR(ENOMEM); + + snprintf(cnonce, sizeof(cnonce), "%08x", av_get_random_seed()); + + av_md5_init(md5); + av_md5_update(md5, user, strlen(user)); + av_md5_update(md5, ":", 1); + av_md5_update(md5, realm, strlen(realm)); + av_md5_update(md5, ":", 1); + av_md5_update(md5, rt->password, strlen(rt->password)); + av_md5_final(md5, hash); + ff_data_to_hex(hashstr1, hash, 16, 1); + hashstr1[32] = '\0'; + + av_md5_init(md5); + av_md5_update(md5, method, strlen(method)); + av_md5_update(md5, ":/", 2); + av_md5_update(md5, rt->app, strlen(rt->app)); + av_md5_final(md5, hash); + ff_data_to_hex(hashstr2, hash, 16, 1); + hashstr2[32] = '\0'; + + av_md5_init(md5); + av_md5_update(md5, hashstr1, strlen(hashstr1)); + av_md5_update(md5, ":", 1); + if (nonce) + av_md5_update(md5, nonce, strlen(nonce)); + av_md5_update(md5, ":", 1); + av_md5_update(md5, nc, strlen(nc)); + av_md5_update(md5, ":", 1); + av_md5_update(md5, cnonce, strlen(cnonce)); + av_md5_update(md5, ":", 1); + av_md5_update(md5, qop, strlen(qop)); + av_md5_update(md5, ":", 1); + av_md5_update(md5, hashstr2, strlen(hashstr2)); + av_md5_final(md5, hash); + ff_data_to_hex(hashstr1, hash, 16, 1); + + snprintf(rt->auth_params, sizeof(rt->auth_params), + "?authmod=%s&user=%s&nonce=%s&cnonce=%s&nc=%s&response=%s", + "llnw", user, nonce, cnonce, nc, hashstr1); + + av_free(md5); + return 0; +} + static int handle_connect_error(URLContext *s, const char *desc) { RTMPContext *rt = s->priv_data; - char buf[300], *ptr; + char buf[300], *ptr, authmod[15]; int i = 0, ret = 0; const char *user = "", *salt = "", *opaque = NULL, - *challenge = NULL, *cptr = NULL; + *challenge = NULL, *cptr = NULL, *nonce = NULL; - if (!(cptr = strstr(desc, "authmod=adobe"))) { + if (!(cptr = strstr(desc, "authmod=adobe")) && + !(cptr = strstr(desc, "authmod=llnw"))) { av_log(s, AV_LOG_ERROR, "Unknown connect error (unsupported authentication method?)\n"); return AVERROR_UNKNOWN; } + cptr += strlen("authmod="); + while (*cptr && *cptr != ' ' && i < sizeof(authmod) - 1) + authmod[i++] = *cptr++; + authmod[i] = '\0'; if (!rt->username[0] || !rt->password[0]) { av_log(s, AV_LOG_ERROR, "No credentials set\n"); @@ -1597,7 +1659,7 @@ static int handle_connect_error(URLContext *s, const char *desc) if (strstr(desc, "code=403 need auth")) { snprintf(rt->auth_params, sizeof(rt->auth_params), - "?authmod=%s&user=%s", "adobe", rt->username); + "?authmod=%s&user=%s", authmod, rt->username); return 0; } @@ -1624,12 +1686,19 @@ static int handle_connect_error(URLContext *s, const char *desc) opaque = value; } else if (!strcmp(ptr, "challenge")) { challenge = value; + } else if (!strcmp(ptr, "nonce")) { + nonce = value; } ptr = next; } - if ((ret = do_adobe_auth(rt, user, salt, challenge, opaque)) < 0) - return ret; + if (!strcmp(authmod, "adobe")) { + if ((ret = do_adobe_auth(rt, user, salt, challenge, opaque)) < 0) + return ret; + } else { + if ((ret = do_llnw_auth(rt, user, nonce)) < 0) + return ret; + } rt->auth_tried = 1; return 0; diff --git a/libavformat/version.h b/libavformat/version.h index 51309797e4..89af0bac6e 100644 --- a/libavformat/version.h +++ b/libavformat/version.h @@ -31,7 +31,7 @@ #define LIBAVFORMAT_VERSION_MAJOR 54 #define LIBAVFORMAT_VERSION_MINOR 20 -#define LIBAVFORMAT_VERSION_MICRO 1 +#define LIBAVFORMAT_VERSION_MICRO 2 #define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \ LIBAVFORMAT_VERSION_MINOR, \ -- cgit v1.2.3