PR #25713 opened by Kilian (lusoris)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25713
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25713.patch

This adds a FATE test for RTSP command serialization, as suggested in the 
automated review of PR 25618. It is stacked on PR 25618 and should land after 
it; the first two commits here are that PR's commits.

The test sends commands through `ff_rtsp_send_cmd_with_content_async()` into an 
in-memory URLProtocol and prints the result, the bytes written and the CSeq 
before and after each call. It covers:

- a normal request, and one that exactly fills the 4096-byte command buffer;
- a request one byte over the buffer, which must be rejected with nothing 
written and CSeq unchanged;
- header and body write failures, which must be returned to the caller;
- a tunneled request with a body, which must be rejected before anything is 
written;
- an overflow with Digest authentication, where the nonce count must not 
advance.

**Tests**

- `fate-rtsp-cmd`, `fate-rtsp`, `fate-rtpdec` and `fate-srtp` pass on top of PR 
25618, and `fate-source` passes.
- With `libavformat/rtsp.c` from current master, `fate-rtsp-cmd` fails on five 
of the seven lines: the overflow cases write 4095 bytes and return 0, the write 
failures return 0, and the tunneled body sends 124 bytes.



>From 28a39f28ceb1efe2ac2e8d4e137396788d9cbaa1 Mon Sep 17 00:00:00 2001
From: lusoris <[email protected]>
Date: Fri, 30 Oct 2026 15:22:03 +0100
Subject: [PATCH 1/3] avformat/rtsp: check command write errors before using
 replies

A failed command write returns before reading the reply. Ignoring that
error can make callers inspect an uninitialised reply, including when a
request is rejected before it is sent.

Return header and body write errors and check command results before
using replies in setup, playback, subscription and publishing paths.
Keep keepalive failures nonfatal: warn and advance the attempt time so
the next packet does not immediately retry the failed request.

Map non-OK RECORD replies to invalid data rather than passing an
internal -1 error to the caller. Propagate failures when resuming after
a subscription and while falling back from UDP to TCP.

Injected ENOSPC and EPIPE now propagate without a reply read. Ten reply
caller cases and both keepalive methods fail without the caller changes
and pass with them. A controlled clock verifies five packet reads, one
warning and no retry before timeout/2, with the reply queue unchanged.
The loopback RTP session and RTSP, RTP and SRTP FATE tests pass.

Signed-off-by: lusoris <[email protected]>
---
 libavformat/rtsp.c    | 15 +++++++++----
 libavformat/rtspdec.c | 51 +++++++++++++++++++++++++++++--------------
 libavformat/rtspenc.c | 20 ++++++++++-------
 3 files changed, 58 insertions(+), 28 deletions(-)

diff --git a/libavformat/rtsp.c b/libavformat/rtsp.c
index 29f219c366a..d912eedb7be 100644
--- a/libavformat/rtsp.c
+++ b/libavformat/rtsp.c
@@ -1478,6 +1478,7 @@ int ff_rtsp_send_cmd_with_content_async(AVFormatContext 
*s,
     RTSPState *rt = s->priv_data;
     char buf[MAX_URL_SIZE], *out_buf;
     char base64buf[AV_BASE64_SIZE(sizeof(buf))];
+    int ret;
 
     if (!rt->rtsp_hd_out)
         return AVERROR(ENOTCONN);
@@ -1513,13 +1514,15 @@ int ff_rtsp_send_cmd_with_content_async(AVFormatContext 
*s,
 
     av_log(s, AV_LOG_TRACE, "Sending:\n%s--\n", buf);
 
-    ffurl_write(rt->rtsp_hd_out, out_buf, strlen(out_buf));
+    if ((ret = ffurl_write(rt->rtsp_hd_out, out_buf, strlen(out_buf))) < 0)
+        return ret;
     if (send_content_length > 0 && send_content) {
         if (rt->control_transport == RTSP_MODE_TUNNEL) {
             avpriv_report_missing_feature(s, "Tunneling of RTSP requests with 
content data");
             return AVERROR_PATCHWELCOME;
         }
-        ffurl_write(rt->rtsp_hd_out, send_content, send_content_length);
+        if ((ret = ffurl_write(rt->rtsp_hd_out, send_content, 
send_content_length)) < 0)
+            return ret;
     }
     rt->last_cmd_time = av_gettime_relative();
 
@@ -1774,7 +1777,9 @@ int ff_rtsp_make_setup_request(AVFormatContext *s, const 
char *host, int port,
                         "RealChallenge2: %s, sd=%s\r\n",
                         rt->session_id, real_res, real_csum);
         }
-        ff_rtsp_send_cmd(s, "SETUP", rtsp_st->control_url, cmd, reply, NULL);
+        err = ff_rtsp_send_cmd(s, "SETUP", rtsp_st->control_url, cmd, reply, 
NULL);
+        if (err < 0)
+            goto fail;
         if (reply->status_code == 461 /* Unsupported protocol */ && i == 0) {
             err = 1;
             goto fail;
@@ -2190,7 +2195,9 @@ redirect:
                        "CompanyID: KnKV4M4I/B2FjJ1TToLycw==\r\n"
                        "GUID: 00000000-0000-0000-0000-000000000000\r\n",
                        sizeof(cmd));
-        ff_rtsp_send_cmd(s, "OPTIONS", rt->control_uri, cmd, reply, NULL);
+        err = ff_rtsp_send_cmd(s, "OPTIONS", rt->control_uri, cmd, reply, 
NULL);
+        if (err < 0)
+            goto fail;
         if (reply->status_code != RTSP_STATUS_OK) {
             err = ff_rtsp_averror(reply->status_code, AVERROR_INVALIDDATA);
             goto fail;
diff --git a/libavformat/rtspdec.c b/libavformat/rtspdec.c
index 0e592eebaed..0c7e9098b39 100644
--- a/libavformat/rtspdec.c
+++ b/libavformat/rtspdec.c
@@ -542,7 +542,7 @@ static int rtsp_read_play(AVFormatContext *s)
 {
     RTSPState *rt = s->priv_data;
     RTSPMessageHeader reply1, *reply = &reply1;
-    int i;
+    int i, ret;
     char cmd[MAX_URL_SIZE];
 
     av_log(s, AV_LOG_DEBUG, "hello state=%d\n", rt->state);
@@ -584,7 +584,8 @@ static int rtsp_read_play(AVFormatContext *s)
                      rt->seek_timestamp / AV_TIME_BASE,
                      rt->seek_timestamp / (AV_TIME_BASE / 1000) % 1000);
         }
-        ff_rtsp_send_cmd(s, "PLAY", rt->control_uri, cmd, reply, NULL);
+        if ((ret = ff_rtsp_send_cmd(s, "PLAY", rt->control_uri, cmd, reply, 
NULL)) < 0)
+            return ret;
         if (reply->status_code != RTSP_STATUS_OK) {
             return ff_rtsp_averror(reply->status_code, -1);
         }
@@ -613,11 +614,13 @@ static int rtsp_read_pause(AVFormatContext *s)
 {
     RTSPState *rt = s->priv_data;
     RTSPMessageHeader reply1, *reply = &reply1;
+    int ret;
 
     if (rt->state != RTSP_STATE_STREAMING)
         return 0;
     else if (!(rt->server_type == RTSP_SERVER_REAL && rt->need_subscription)) {
-        ff_rtsp_send_cmd(s, "PAUSE", rt->control_uri, NULL, reply, NULL);
+        if ((ret = ff_rtsp_send_cmd(s, "PAUSE", rt->control_uri, NULL, reply, 
NULL)) < 0)
+            return ret;
         if (reply->status_code != RTSP_STATUS_OK) {
             return ff_rtsp_averror(reply->status_code, -1);
         }
@@ -751,7 +754,11 @@ int ff_rtsp_setup_input_streams(AVFormatContext *s, 
RTSPMessageHeader *reply)
                    "Require: com.real.retain-entity-for-setup\r\n",
                    sizeof(cmd));
     }
-    ff_rtsp_send_cmd(s, "DESCRIBE", rt->control_uri, cmd, reply, &content);
+    ret = ff_rtsp_send_cmd(s, "DESCRIBE", rt->control_uri, cmd, reply, 
&content);
+    if (ret < 0) {
+        av_freep(&content);
+        return ret;
+    }
     if (reply->status_code != RTSP_STATUS_OK) {
         av_freep(&content);
         return ff_rtsp_averror(reply->status_code, AVERROR_INVALIDDATA);
@@ -987,8 +994,10 @@ retry:
                 snprintf(cmd, sizeof(cmd),
                          "Unsubscribe: %s\r\n",
                          rt->last_subscription);
-                ff_rtsp_send_cmd(s, "SET_PARAMETER", rt->control_uri,
-                                 cmd, reply, NULL);
+                ret = ff_rtsp_send_cmd(s, "SET_PARAMETER", rt->control_uri,
+                                       cmd, reply, NULL);
+                if (ret < 0)
+                    return ret;
                 if (reply->status_code != RTSP_STATUS_OK)
                     return ff_rtsp_averror(reply->status_code, 
AVERROR_INVALIDDATA);
                 rt->need_subscription = 1;
@@ -1022,14 +1031,19 @@ retry:
                 }
             }
             av_strlcatf(cmd, sizeof(cmd), "%s\r\n", rt->last_subscription);
-            ff_rtsp_send_cmd(s, "SET_PARAMETER", rt->control_uri,
-                             cmd, reply, NULL);
+            ret = ff_rtsp_send_cmd(s, "SET_PARAMETER", rt->control_uri,
+                                   cmd, reply, NULL);
+            if (ret < 0)
+                return ret;
             if (reply->status_code != RTSP_STATUS_OK)
                 return ff_rtsp_averror(reply->status_code, 
AVERROR_INVALIDDATA);
             rt->need_subscription = 0;
 
-            if (rt->state == RTSP_STATE_STREAMING)
-                rtsp_read_play (s);
+            if (rt->state == RTSP_STATE_STREAMING) {
+                ret = rtsp_read_play(s);
+                if (ret < 0)
+                    return ret;
+            }
         }
     }
 
@@ -1040,8 +1054,8 @@ retry:
                 rt->lower_transport_mask & (1 << RTSP_LOWER_TRANSPORT_TCP)) {
                 RTSPMessageHeader reply1, *reply = &reply1;
                 av_log(s, AV_LOG_WARNING, "UDP timeout, retrying with TCP\n");
-                if (rtsp_read_pause(s) != 0)
-                    return -1;
+                if ((ret = rtsp_read_pause(s)) != 0)
+                    return ret;
                 // TEARDOWN is required on Real-RTSP, but might make
                 // other servers close the connection.
                 if (rt->server_type == RTSP_SERVER_REAL)
@@ -1051,8 +1065,8 @@ retry:
                 if (resetup_tcp(s) == 0) {
                     rt->state = RTSP_STATE_IDLE;
                     rt->need_subscription = 1;
-                    if (rtsp_read_play(s) != 0)
-                        return -1;
+                    if ((ret = rtsp_read_play(s)) != 0)
+                        return ret;
                     goto retry;
                 }
             }
@@ -1068,9 +1082,14 @@ retry:
             if (rt->server_type == RTSP_SERVER_WMS ||
                 (rt->server_type != RTSP_SERVER_REAL &&
                  rt->get_parameter_supported)) {
-                ff_rtsp_send_cmd_async(s, "GET_PARAMETER", rt->control_uri, 
NULL);
+                ret = ff_rtsp_send_cmd_async(s, "GET_PARAMETER", 
rt->control_uri, NULL);
             } else {
-                ff_rtsp_send_cmd_async(s, "OPTIONS", rt->control_uri, NULL);
+                ret = ff_rtsp_send_cmd_async(s, "OPTIONS", rt->control_uri, 
NULL);
+            }
+            if (ret < 0) {
+                av_log(s, AV_LOG_WARNING, "Failed to send RTSP keepalive: 
%s\n",
+                       av_err2str(ret));
+                rt->last_cmd_time = av_gettime_relative();
             }
             /* The stale flag should be reset when creating the auth response 
in
              * ff_rtsp_send_cmd_async, but reset it here just in case we never
diff --git a/libavformat/rtspenc.c b/libavformat/rtspenc.c
index f2f4b6d5258..370260a0eb8 100644
--- a/libavformat/rtspenc.c
+++ b/libavformat/rtspenc.c
@@ -48,7 +48,7 @@ int ff_rtsp_setup_output_streams(AVFormatContext *s, const 
char *addr)
 {
     RTSPState *rt = s->priv_data;
     RTSPMessageHeader reply1, *reply = &reply1;
-    int i;
+    int i, ret;
     char *sdp;
     AVFormatContext sdp_ctx, *ctx_array[1];
     char url[MAX_URL_SIZE];
@@ -82,10 +82,12 @@ int ff_rtsp_setup_output_streams(AVFormatContext *s, const 
char *addr)
         return AVERROR_INVALIDDATA;
     }
     av_log(s, AV_LOG_VERBOSE, "SDP:\n%s\n", sdp);
-    ff_rtsp_send_cmd_with_content(s, "ANNOUNCE", rt->control_uri,
-                                  "Content-Type: application/sdp\r\n",
-                                  reply, NULL, sdp, strlen(sdp));
+    ret = ff_rtsp_send_cmd_with_content(s, "ANNOUNCE", rt->control_uri,
+                                        "Content-Type: application/sdp\r\n",
+                                        reply, NULL, sdp, strlen(sdp));
     av_free(sdp);
+    if (ret < 0)
+        return ret;
     if (reply->status_code != RTSP_STATUS_OK)
         return ff_rtsp_averror(reply->status_code, AVERROR_INVALIDDATA);
 
@@ -114,12 +116,14 @@ static int rtsp_write_record(AVFormatContext *s)
     RTSPState *rt = s->priv_data;
     RTSPMessageHeader reply1, *reply = &reply1;
     char cmd[MAX_URL_SIZE];
+    int ret;
 
     snprintf(cmd, sizeof(cmd),
              "Range: npt=0.000-\r\n");
-    ff_rtsp_send_cmd(s, "RECORD", rt->control_uri, cmd, reply, NULL);
+    if ((ret = ff_rtsp_send_cmd(s, "RECORD", rt->control_uri, cmd, reply, 
NULL)) < 0)
+        return ret;
     if (reply->status_code != RTSP_STATUS_OK)
-        return ff_rtsp_averror(reply->status_code, -1);
+        return ff_rtsp_averror(reply->status_code, AVERROR_INVALIDDATA);
     rt->state = RTSP_STATE_STREAMING;
     return 0;
 }
@@ -132,10 +136,10 @@ static int rtsp_write_header(AVFormatContext *s)
     if (ret)
         return ret;
 
-    if (rtsp_write_record(s) < 0) {
+    if ((ret = rtsp_write_record(s)) < 0) {
         ff_rtsp_close_streams(s);
         ff_rtsp_close_connections(s);
-        return AVERROR_INVALIDDATA;
+        return ret;
     }
     return 0;
 }
-- 
2.54.0


>From b7419f19531d5bedd17cb8cae5dd24f834b4064d Mon Sep 17 00:00:00 2001
From: lusoris <[email protected]>
Date: Fri, 30 Oct 2026 15:23:53 +0100
Subject: [PATCH 2/3] avformat/rtsp: reject truncated command requests

Unchecked appends can fill the fixed request buffer before the header
terminator is added, sending an incomplete request to the server.

Build the request with a bounded AVBPrint and reject truncation before
writing anything or advancing CSeq. The preceding caller checks handle
this rejection before inspecting a reply that has not been read.

Restore Digest authentication state when a too-long request is rejected,
so the request has no authentication-side effects. Reject tunneled
request bodies before building or writing a header, advancing CSeq, or
changing authentication state.

A loopback ffprobe test with a 4040-byte SDP control URL sent a
4095-byte SETUP without CRLFCRLF before this change. It now logs an
error and sends no SETUP bytes. Direct checks cover overflowing headers,
authentication, content length, tunneling and the exact-fit boundary.
The loopback RTP session and RTSP, RTP and SRTP FATE tests pass.

Signed-off-by: lusoris <[email protected]>
---
 libavformat/rtsp.c | 39 ++++++++++++++++++++++++++-------------
 1 file changed, 26 insertions(+), 13 deletions(-)

diff --git a/libavformat/rtsp.c b/libavformat/rtsp.c
index d912eedb7be..aa85b68cf87 100644
--- a/libavformat/rtsp.c
+++ b/libavformat/rtsp.c
@@ -1478,33 +1478,50 @@ int ff_rtsp_send_cmd_with_content_async(AVFormatContext 
*s,
     RTSPState *rt = s->priv_data;
     char buf[MAX_URL_SIZE], *out_buf;
     char base64buf[AV_BASE64_SIZE(sizeof(buf))];
+    AVBPrint request;
+    HTTPAuthState auth_saved;
     int ret;
 
     if (!rt->rtsp_hd_out)
         return AVERROR(ENOTCONN);
+    if (rt->control_transport == RTSP_MODE_TUNNEL && send_content_length > 0 &&
+        send_content) {
+        avpriv_report_missing_feature(s, "Tunneling of RTSP requests with 
content data");
+        return AVERROR_PATCHWELCOME;
+    }
 
     /* Add in RTSP headers */
     out_buf = buf;
-    rt->seq++;
-    snprintf(buf, sizeof(buf), "%s %s RTSP/1.0\r\n", method, url);
+    if (rt->auth[0])
+        auth_saved = rt->auth_state;
+    av_bprint_init_for_buffer(&request, buf, sizeof(buf));
+    av_bprintf(&request, "%s %s RTSP/1.0\r\n", method, url);
     if (headers)
-        av_strlcat(buf, headers, sizeof(buf));
-    av_strlcatf(buf, sizeof(buf), "CSeq: %d\r\n", rt->seq);
-    av_strlcatf(buf, sizeof(buf), "User-Agent: %s\r\n",  rt->user_agent);
+        av_bprintf(&request, "%s", headers);
+    av_bprintf(&request, "CSeq: %d\r\n", rt->seq + 1);
+    av_bprintf(&request, "User-Agent: %s\r\n", rt->user_agent);
     if (rt->session_id[0] != '\0' && (!headers ||
         !strstr(headers, "\nIf-Match:"))) {
-        av_strlcatf(buf, sizeof(buf), "Session: %s\r\n", rt->session_id);
+        av_bprintf(&request, "Session: %s\r\n", rt->session_id);
     }
     if (rt->auth[0]) {
         char *str = ff_http_auth_create_response(&rt->auth_state,
                                                  rt->auth, url, method);
         if (str)
-            av_strlcat(buf, str, sizeof(buf));
+            av_bprintf(&request, "%s", str);
         av_free(str);
     }
     if (send_content_length > 0 && send_content)
-        av_strlcatf(buf, sizeof(buf), "Content-Length: %d\r\n", 
send_content_length);
-    av_strlcat(buf, "\r\n", sizeof(buf));
+        av_bprintf(&request, "Content-Length: %d\r\n", send_content_length);
+    av_bprintf(&request, "\r\n");
+
+    if (!av_bprint_is_complete(&request)) {
+        if (rt->auth[0])
+            rt->auth_state = auth_saved;
+        av_log(s, AV_LOG_ERROR, "RTSP request too long\n");
+        return AVERROR(EINVAL);
+    }
+    rt->seq++;
 
     /* base64 encode rtsp if tunneling */
     if (rt->control_transport == RTSP_MODE_TUNNEL) {
@@ -1517,10 +1534,6 @@ int ff_rtsp_send_cmd_with_content_async(AVFormatContext 
*s,
     if ((ret = ffurl_write(rt->rtsp_hd_out, out_buf, strlen(out_buf))) < 0)
         return ret;
     if (send_content_length > 0 && send_content) {
-        if (rt->control_transport == RTSP_MODE_TUNNEL) {
-            avpriv_report_missing_feature(s, "Tunneling of RTSP requests with 
content data");
-            return AVERROR_PATCHWELCOME;
-        }
         if ((ret = ffurl_write(rt->rtsp_hd_out, send_content, 
send_content_length)) < 0)
             return ret;
     }
-- 
2.54.0


>From 2690fbddced5d8c697676674c9031ea1cf2fd588 Mon Sep 17 00:00:00 2001
From: lusoris <[email protected]>
Date: Tue, 3 Nov 2026 13:47:42 +0100
Subject: [PATCH 3/3] avformat/tests: add an RTSP command serialization test

The RTSP FATE tests only cover SDP sample-rate parsing. Add a test
that sends commands through ff_rtsp_send_cmd_with_content_async() into
an in-memory URLProtocol and prints the result, the bytes written and
the CSeq before and after each call.

It covers a normal request, a request that exactly fills the command
buffer, one byte over it, header and body write failures, a tunneled
request with a body, and an overflow with Digest authentication, where
the nonce count must not advance.

The control handle is heap-allocated, so the test also holds if a
failed write closes the connection.

Signed-off-by: lusoris <[email protected]>
---
 libavformat/Makefile       |   1 +
 libavformat/tests/rtsp.c   | 206 +++++++++++++++++++++++++++++++++++++
 tests/fate/libavformat.mak |   4 +
 tests/ref/fate/rtsp-cmd    |   7 ++
 4 files changed, 218 insertions(+)
 create mode 100644 libavformat/tests/rtsp.c
 create mode 100644 tests/ref/fate/rtsp-cmd

diff --git a/libavformat/Makefile b/libavformat/Makefile
index e479c5c67cf..53b649c4e88 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -795,6 +795,7 @@ TESTPROGS = id3v2                                           
            \
             url                                                         \
             seek_utils
 TESTPROGS-$(CONFIG_RTPDEC)               += rtpdec
+TESTPROGS-$(CONFIG_RTSP_MUXER)            += rtsp
 #           async                                                       \
 
 FIFO-MUXER-TESTPROGS-$(CONFIG_NETWORK)   += fifo_muxer
diff --git a/libavformat/tests/rtsp.c b/libavformat/tests/rtsp.c
new file mode 100644
index 00000000000..d8d70b9cca7
--- /dev/null
+++ b/libavformat/tests/rtsp.c
@@ -0,0 +1,206 @@
+/*
+ * Copyright (c) 2026 lusoris
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "libavformat/avformat.h"
+#include "libavformat/httpauth.h"
+#include "libavformat/internal.h"
+#include "libavformat/rtsp.h"
+#include "libavformat/url.h"
+#include "libavutil/error.h"
+#include "libavutil/avstring.h"
+#include "libavutil/mem.h"
+
+typedef struct TestContext {
+    unsigned char buf[MAX_URL_SIZE];
+    int len;
+    int fail_write;
+    int error;
+    int writes;
+} TestContext;
+
+static int test_write(URLContext *h, const unsigned char *buf, int size)
+{
+    TestContext *ctx = h->priv_data;
+
+    if (ctx->writes++ == ctx->fail_write)
+        return ctx->error;
+    if (size > MAX_URL_SIZE - ctx->len)
+        return AVERROR(ENOSPC);
+    memcpy(ctx->buf + ctx->len, buf, size);
+    ctx->len += size;
+    return size;
+}
+
+static const URLProtocol test_protocol = {
+    .name      = "test",
+    .url_write = test_write,
+};
+
+static char user_agent[] = "rtsp-test";
+
+/**
+ * The control handle is heap-allocated, as ff_rtsp_close_connections() may
+ * free it after a failed write.
+ */
+static void init_context(AVFormatContext *s, RTSPState *rt, TestContext *ctx)
+{
+    URLContext *url;
+
+    ffurl_closep(&rt->rtsp_hd_out);
+    url = av_mallocz(sizeof(*url));
+    if (!url)
+        exit(1);
+    *url = (URLContext) { .prot = &test_protocol, .priv_data = ctx,
+                          .flags = AVIO_FLAG_WRITE };
+    *s = (AVFormatContext) { .priv_data = rt };
+    *rt = (RTSPState) { .rtsp_hd_out = url, .seq = 41,
+                         .user_agent = user_agent };
+    *ctx = (TestContext) { .fail_write = -1 };
+}
+
+static int ends_with_crlfcrlf(const TestContext *ctx)
+{
+    return ctx->len >= 4 && !memcmp(ctx->buf + ctx->len - 4, "\r\n\r\n", 4);
+}
+
+static const char *err_name(int ret)
+{
+    if (ret >= 0)
+        return "0";
+    if (ret == AVERROR(EINVAL))
+        return "EINVAL";
+    if (ret == AVERROR(ENOSPC))
+        return "ENOSPC";
+    if (ret == AVERROR(EPIPE))
+        return "EPIPE";
+    if (ret == AVERROR_PATCHWELCOME)
+        return "PATCHWELCOME";
+    return "other";
+}
+
+static void print_result(const char *name, const char *method, int ret,
+                         const TestContext *ctx, int seq_before, int seq_after,
+                         int ends_with_crlfcrlf)
+{
+    printf("%s method=%s rc=%s bytes=%d cseq=%d/%d crlf=%d\n", name, method,
+           err_name(ret), ctx->len, seq_before, seq_after, ends_with_crlfcrlf);
+}
+
+static int send_rtsp(AVFormatContext *s, const char *method, const char 
*headers,
+                     const unsigned char *content, int content_length)
+{
+    return ff_rtsp_send_cmd_with_content_async(s, method, 
"rtsp://example/test",
+                                               headers, content, 
content_length);
+}
+
+static void set_digest_auth(RTSPState *rt)
+{
+    av_strlcpy(rt->auth, "user:pass", sizeof(rt->auth));
+    rt->auth_state.auth_type = HTTP_AUTH_DIGEST;
+    av_strlcpy(rt->auth_state.realm, "realm", sizeof(rt->auth_state.realm));
+    av_strlcpy(rt->auth_state.digest_params.nonce, "nonce",
+               sizeof(rt->auth_state.digest_params.nonce));
+    av_strlcpy(rt->auth_state.digest_params.qop, "auth",
+               sizeof(rt->auth_state.digest_params.qop));
+    rt->auth_state.digest_params.nc = 7;
+}
+
+int main(void)
+{
+    AVFormatContext s;
+    RTSPState rt = { 0 };
+    TestContext ctx;
+    unsigned char content[] = "body";
+    char headers[MAX_URL_SIZE + 1];
+    int bare_length, digest_length, fit_length, ret, seq_before, nc_before;
+
+    init_context(&s, &rt, &ctx);
+    seq_before = rt.seq;
+    ret = send_rtsp(&s, "OPTIONS", NULL, NULL, 0);
+    bare_length = ctx.len;
+    print_result("normal", "OPTIONS", ret, &ctx, seq_before, rt.seq,
+                 ends_with_crlfcrlf(&ctx));
+
+    fit_length = MAX_URL_SIZE - 1 - bare_length;
+    memset(headers, 'a', fit_length + 1);
+    headers[0] = 'X';
+    headers[1] = ':';
+    headers[2] = ' ';
+    headers[fit_length - 2] = '\r';
+    headers[fit_length - 1] = '\n';
+    headers[fit_length] = 0;
+
+    init_context(&s, &rt, &ctx);
+    seq_before = rt.seq;
+    ret = send_rtsp(&s, "OPTIONS", headers, NULL, 0);
+    print_result("exact", "OPTIONS", ret, &ctx, seq_before, rt.seq,
+                 ends_with_crlfcrlf(&ctx));
+
+    headers[fit_length] = 'a';
+    headers[fit_length + 1] = 0;
+    init_context(&s, &rt, &ctx);
+    seq_before = rt.seq;
+    ret = send_rtsp(&s, "OPTIONS", headers, NULL, 0);
+    print_result("overflow", "OPTIONS", ret, &ctx, seq_before, rt.seq, 0);
+
+    init_context(&s, &rt, &ctx);
+    ctx.fail_write = 0;
+    ctx.error = AVERROR(ENOSPC);
+    seq_before = rt.seq;
+    ret = send_rtsp(&s, "OPTIONS", NULL, NULL, 0);
+    print_result("header-fail", "OPTIONS", ret, &ctx, seq_before, rt.seq, 0);
+
+    init_context(&s, &rt, &ctx);
+    ctx.fail_write = 1;
+    ctx.error = AVERROR(EPIPE);
+    seq_before = rt.seq;
+    ret = send_rtsp(&s, "ANNOUNCE", NULL, content, sizeof(content) - 1);
+    print_result("body-fail", "ANNOUNCE", ret, &ctx, seq_before, rt.seq, 0);
+
+    init_context(&s, &rt, &ctx);
+    rt.control_transport = RTSP_MODE_TUNNEL;
+    seq_before = rt.seq;
+    ret = send_rtsp(&s, "ANNOUNCE", NULL, content, sizeof(content) - 1);
+    print_result("tunnel-body", "ANNOUNCE", ret, &ctx, seq_before, rt.seq, 0);
+
+    init_context(&s, &rt, &ctx);
+    set_digest_auth(&rt);
+    ret = send_rtsp(&s, "OPTIONS", NULL, NULL, 0);
+    digest_length = ctx.len;
+    fit_length = MAX_URL_SIZE - digest_length;
+    memset(headers, 'a', fit_length + 1);
+    headers[fit_length] = 0;
+
+    init_context(&s, &rt, &ctx);
+    set_digest_auth(&rt);
+    seq_before = rt.seq;
+    nc_before = rt.auth_state.digest_params.nc;
+    ret = send_rtsp(&s, "OPTIONS", headers, NULL, 0);
+    printf("digest-overflow method=OPTIONS rc=%s bytes=%d cseq=%d/%d 
nc=%d/%d\n",
+           err_name(ret), ctx.len, seq_before, rt.seq, nc_before,
+           rt.auth_state.digest_params.nc);
+
+    ffurl_closep(&rt.rtsp_hd_out);
+    return 0;
+}
diff --git a/tests/fate/libavformat.mak b/tests/fate/libavformat.mak
index e7854626c33..89b744d3478 100644
--- a/tests/fate/libavformat.mak
+++ b/tests/fate/libavformat.mak
@@ -64,6 +64,10 @@ FATE_LIBAVFORMAT-$(CONFIG_RTPDEC) += fate-rtpdec
 fate-rtpdec: libavformat/tests/rtpdec$(EXESUF)
 fate-rtpdec: CMD = run libavformat/tests/rtpdec$(EXESUF)
 
+FATE_LIBAVFORMAT-$(CONFIG_RTSP_MUXER) += fate-rtsp-cmd
+fate-rtsp-cmd: libavformat/tests/rtsp$(EXESUF)
+fate-rtsp-cmd: CMD = run libavformat/tests/rtsp$(EXESUF)
+
 FATE_LIBAVFORMAT-$(CONFIG_SRTP) += fate-srtp
 fate-srtp: libavformat/tests/srtp$(EXESUF)
 fate-srtp: CMD = run libavformat/tests/srtp$(EXESUF)
diff --git a/tests/ref/fate/rtsp-cmd b/tests/ref/fate/rtsp-cmd
new file mode 100644
index 00000000000..156d7b14e92
--- /dev/null
+++ b/tests/ref/fate/rtsp-cmd
@@ -0,0 +1,7 @@
+normal method=OPTIONS rc=0 bytes=73 cseq=41/42 crlf=1
+exact method=OPTIONS rc=0 bytes=4095 cseq=41/42 crlf=1
+overflow method=OPTIONS rc=EINVAL bytes=0 cseq=41/41 crlf=0
+header-fail method=OPTIONS rc=ENOSPC bytes=0 cseq=41/42 crlf=0
+body-fail method=ANNOUNCE rc=EPIPE bytes=93 cseq=41/42 crlf=0
+tunnel-body method=ANNOUNCE rc=PATCHWELCOME bytes=0 cseq=41/41 crlf=0
+digest-overflow method=OPTIONS rc=EINVAL bytes=0 cseq=41/41 nc=7/7
-- 
2.54.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to