]> git.sesse.net Git - ffmpeg/blobdiff - libavformat/rtmpproto.c
lavf: Add functions for SRTP decryption/encryption
[ffmpeg] / libavformat / rtmpproto.c
index 6f41213925c03b715e0778ed3e80041434003bba..78ebbf1c101c175f9222050b695ecce1d7dd3f97 100644 (file)
 
 #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);
@@ -859,7 +869,7 @@ int ff_rtmp_calc_digest(const uint8_t *src, int len, int gap,
     uint8_t hmac_buf[64+32] = {0};
     int i;
 
-    sha = av_mallocz(av_sha_size);
+    sha = av_sha_alloc();
     if (!sha)
         return AVERROR(ENOMEM);
 
@@ -1371,9 +1381,9 @@ static int rtmp_server_handshake(URLContext *s, RTMPContext *rt)
     /* By now same epoch will be sent */
     hs_my_epoch = hs_epoch;
     /* Generate random */
-    for (randomidx = 0; randomidx < (RTMP_HANDSHAKE_PACKET_SIZE);
+    for (randomidx = 8; randomidx < (RTMP_HANDSHAKE_PACKET_SIZE);
          randomidx += 4)
-        AV_WB32(hs_s1 + 8 + randomidx, av_get_random_seed());
+        AV_WB32(hs_s1 + randomidx, av_get_random_seed());
 
     ret = rtmp_send_hs_packet(rt, hs_my_epoch, 0, hs_s1,
                               RTMP_HANDSHAKE_PACKET_SIZE);
@@ -1512,8 +1522,191 @@ 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 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, authmod[15];
+    int i = 0, ret = 0;
+    const char *user = "", *salt = "", *opaque = NULL,
+               *challenge = NULL, *cptr = NULL, *nonce = NULL;
+
+    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");
+        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", authmod, 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;
+        } else if (!strcmp(ptr, "nonce")) {
+            nonce = value;
+        }
+        ptr = next;
+    }
+
+    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;
+}
+
 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,8 +1725,14 @@ 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 = -1;
+            ret = AVERROR_UNKNOWN;
         av_log(s, level, "Server error: %s\n", tmpstr);
     }
 
@@ -1793,7 +1992,7 @@ static int handle_invoke(URLContext *s, RTMPPacket *pkt)
                !memcmp(pkt->data, "\002\000\007publish", 10)       ||
                !memcmp(pkt->data, "\002\000\010_checkbw", 11)      ||
                !memcmp(pkt->data, "\002\000\014createStream", 15)) {
-        if (ret = send_invoke_response(s, pkt) < 0)
+        if ((ret = send_invoke_response(s, pkt)) < 0)
             return ret;
     }
 
@@ -1958,6 +2157,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 +2263,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 +2275,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 +2323,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);
@@ -2188,7 +2402,7 @@ static int rtmp_open(URLContext *s, const char *uri, int flags)
         } else {
             rt->playpath[0] = 0;
         }
-        strncat(rt->playpath, fname, PLAYPATH_MAX_LENGTH - 5);
+        av_strlcat(rt->playpath, fname, PLAYPATH_MAX_LENGTH);
     }
 
     if (!rt->tcurl) {
@@ -2239,6 +2453,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;
@@ -2407,11 +2631,11 @@ static int rtmp_write(URLContext *s, const uint8_t *buf, int size)
 
 static const AVOption rtmp_options[] = {
     {"rtmp_app", "Name of application to connect to on the RTMP server", OFFSET(app), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC|ENC},
-    {"rtmp_buffer", "Set buffer time in milliseconds. The default is 3000.", OFFSET(client_buffer_time), AV_OPT_TYPE_INT, {3000}, 0, INT_MAX, DEC|ENC},
+    {"rtmp_buffer", "Set buffer time in milliseconds. The default is 3000.", OFFSET(client_buffer_time), AV_OPT_TYPE_INT, {.i64 = 3000}, 0, INT_MAX, DEC|ENC},
     {"rtmp_conn", "Append arbitrary AMF data to the Connect message", OFFSET(conn), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC|ENC},
     {"rtmp_flashver", "Version of the Flash plugin used to run the SWF player.", OFFSET(flashver), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC|ENC},
-    {"rtmp_flush_interval", "Number of packets flushed in the same request (RTMPT only).", OFFSET(flush_interval), AV_OPT_TYPE_INT, {10}, 0, INT_MAX, ENC},
-    {"rtmp_live", "Specify that the media is a live stream.", OFFSET(live), AV_OPT_TYPE_INT, {-2}, INT_MIN, INT_MAX, DEC, "rtmp_live"},
+    {"rtmp_flush_interval", "Number of packets flushed in the same request (RTMPT only).", OFFSET(flush_interval), AV_OPT_TYPE_INT, {.i64 = 10}, 0, INT_MAX, ENC},
+    {"rtmp_live", "Specify that the media is a live stream.", OFFSET(live), AV_OPT_TYPE_INT, {.i64 = -2}, INT_MIN, INT_MAX, DEC, "rtmp_live"},
     {"any", "both", 0, AV_OPT_TYPE_CONST, {.i64 = -2}, 0, 0, DEC, "rtmp_live"},
     {"live", "live stream", 0, AV_OPT_TYPE_CONST, {.i64 = -1}, 0, 0, DEC, "rtmp_live"},
     {"recorded", "recorded stream", 0, AV_OPT_TYPE_CONST, {.i64 = 0}, 0, 0, DEC, "rtmp_live"},
@@ -2419,12 +2643,12 @@ static const AVOption rtmp_options[] = {
     {"rtmp_playpath", "Stream identifier to play or to publish", OFFSET(playpath), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC|ENC},
     {"rtmp_subscribe", "Name of live stream to subscribe to. Defaults to rtmp_playpath.", OFFSET(subscribe), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC},
     {"rtmp_swfhash", "SHA256 hash of the decompressed SWF file (32 bytes).", OFFSET(swfhash), AV_OPT_TYPE_BINARY, .flags = DEC},
-    {"rtmp_swfsize", "Size of the decompressed SWF file, required for SWFVerification.", OFFSET(swfsize), AV_OPT_TYPE_INT, {0}, 0, INT_MAX, DEC},
+    {"rtmp_swfsize", "Size of the decompressed SWF file, required for SWFVerification.", OFFSET(swfsize), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, DEC},
     {"rtmp_swfurl", "URL of the SWF player. By default no value will be sent", OFFSET(swfurl), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC|ENC},
     {"rtmp_swfverify", "URL to player swf file, compute hash/size automatically.", OFFSET(swfverify), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC},
     {"rtmp_tcurl", "URL of the target stream. Defaults to proto://host[:port]/app.", OFFSET(tcurl), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC|ENC},
-    {"rtmp_listen", "Listen for incoming rtmp connections", OFFSET(listen), AV_OPT_TYPE_INT, {0}, INT_MIN, INT_MAX, DEC, "rtmp_listen" },
-    {"timeout", "Maximum timeout (in seconds) to wait for incoming connections. -1 is infinite. Implies -rtmp_listen 1",  OFFSET(listen_timeout), AV_OPT_TYPE_INT, {-1}, INT_MIN, INT_MAX, DEC, "rtmp_listen" },
+    {"rtmp_listen", "Listen for incoming rtmp connections", OFFSET(listen), AV_OPT_TYPE_INT, {.i64 = 0}, INT_MIN, INT_MAX, DEC, "rtmp_listen" },
+    {"timeout", "Maximum timeout (in seconds) to wait for incoming connections. -1 is infinite. Implies -rtmp_listen 1",  OFFSET(listen_timeout), AV_OPT_TYPE_INT, {.i64 = -1}, INT_MIN, INT_MAX, DEC, "rtmp_listen" },
     { NULL },
 };