Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RTSP: RECEIVE operation still returns CURLE_OK when the connection is broken #15624

Closed
dengjfzh opened this issue Nov 22, 2024 · 7 comments
Closed
Assignees

Comments

@dengjfzh
Copy link
Contributor

dengjfzh commented Nov 22, 2024

I did this

I use libcurl to download a RTSP stream from a server, then restart the server, libcurl cannot report an error (the connection was broken).

This is my test code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>

size_t curl_write_cb(void *data, size_t size, size_t nmemb, void *userp);
size_t curl_interleave_cb(void *data, size_t size, size_t nmemb, void *userp);

char resp_body[1024*4];
size_t resp_len = 0;

int main(int argc, char *argv[])
{
    const char * const url = (argc>1) ? argv[1] : "rtsp://localhost/test/test";
    char errbuf[CURL_ERROR_SIZE] = {0};
    
    CURL *curl = curl_easy_init();
    if ( curl == NULL ) {
        fprintf(stderr, "Error: call curl_easy_init failed!\n");
        return 1;
    }
    curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
    curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb);
    curl_easy_setopt(curl, CURLOPT_INTERLEAVEFUNCTION, curl_interleave_cb);
    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_RTSP_STREAM_URI, url);
    curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_RTSP);
    curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);

    // options
    curl_easy_setopt(curl, CURLOPT_RTSP_REQUEST, CURL_RTSPREQ_OPTIONS);
    CURLcode res = curl_easy_perform(curl);
    if ( res != CURLE_OK ) {
        fprintf(stderr, "Error: call curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        return 1;
    }

    // describe
    curl_easy_setopt(curl, CURLOPT_RTSP_REQUEST, CURL_RTSPREQ_DESCRIBE);
    resp_len = 0;
    res = curl_easy_perform(curl);
    if ( res != CURLE_OK ) {
        fprintf(stderr, "Error: call curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        return 1;
    }
    resp_body[resp_len] = '\0';
    printf("SDP:\n%s\n", resp_body);

    // setup
    char buf[1024];
    snprintf(buf, sizeof(buf), "%s/streamid=0", url);
    curl_easy_setopt(curl, CURLOPT_RTSP_STREAM_URI, buf);
    curl_easy_setopt(curl, CURLOPT_RTSP_TRANSPORT, "RTP/AVP/TCP;unicast;interleaved=0-1");
    curl_easy_setopt(curl, CURLOPT_RTSP_REQUEST, CURL_RTSPREQ_SETUP);
    res = curl_easy_perform(curl);
    if ( res != CURLE_OK ) {
        fprintf(stderr, "Error: call curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        return 1;
    }
    curl_easy_setopt(curl, CURLOPT_RTSP_STREAM_URI, url);
    curl_easy_setopt(curl, CURLOPT_RTSP_TRANSPORT, "");

    // play
    curl_easy_setopt(curl, CURLOPT_RTSP_REQUEST, CURL_RTSPREQ_PLAY);
    res = curl_easy_perform(curl);
    if ( res != CURLE_OK ) {
        fprintf(stderr, "Error: call curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        return 1;
    }

    // receive loop
    while ( true ) {
        curl_easy_setopt(curl, CURLOPT_RTSP_REQUEST, CURL_RTSPREQ_RECEIVE);
        res = curl_easy_perform(curl);
        if ( res != CURLE_OK ) { // <== When the connection is broken, it still returns CURLE_OK!
            fprintf(stderr, "Error: call curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
            break;
        }
    }

    // teardown
    curl_easy_setopt(curl, CURLOPT_RTSP_REQUEST, CURL_RTSPREQ_TEARDOWN);
    res = curl_easy_perform(curl);
    if ( res != CURLE_OK ) {
        fprintf(stderr, "Error: call curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        return 1;
    }
    
    printf("done.\n");
    return 0;
}

size_t curl_write_cb(void *data, size_t size, size_t nmemb, void *userp)
{
    size_t len = size * nmemb;
    if ( (resp_len + len + 1) > sizeof(resp_body) ) {
        fprintf(stderr, "Warning: response too long! %lu+%lu\n", resp_len, len);
    } else {
        memcpy(resp_body+resp_len, data, len);
        resp_len += len;
    }
    return len;
}

size_t curl_interleave_cb(void *data, size_t size, size_t nmemb, void *userp)
{
    size_t len = size * nmemb;
    unsigned char *p = (unsigned char*)data;
    if ( p[0] == '$' ) {
        int channel = p[1];
        int framelen = p[2]*256 + p[3];
        printf("Recv %lu bytes of interleave frame, channel:%d, framelen:%d\n", len, channel, framelen);
    } else {
        fprintf(stderr, "Warning: not an interleave frame! p[0]=0x%x\n", p[0]);
    }
    return len;
}

Note that in this test code, when the connection is broken, the curl_easy_perform() call will immediately return CURLE_OK, resulting in 100% CPU usage.

Attached is the log.
log.txt

I expected the following

When the connection is broken, curl_easy_perform() should return an error code so that the application can handle it accordingly.

curl/libcurl version

curl 8.11.0

operating system

Ubuntu 24.04

@dengjfzh
Copy link
Contributor Author

dengjfzh commented Nov 22, 2024

I tried to add a check in rtsp_done() in curl/lib/rtsp.c.
Through gdb, I found that eos_written, upload_done, upload_aborted under the data->req structure are set after the connection is broken. So I added the following code:

static CURLcode rtsp_done(struct Curl_easy *data,
                          CURLcode status, bool premature)
{
  struct RTSP *rtsp = data->req.p.rtsp;
  CURLcode httpStatus;

  /* Bypass HTTP empty-reply checks on receive */
  if(data->set.rtspreq == RTSPREQ_RECEIVE)
    premature = TRUE;

  httpStatus = Curl_http_done(data, status, premature);

  if(rtsp && !status && !httpStatus) {
    /* Check the sequence numbers */
    long CSeq_sent = rtsp->CSeq_sent;
    long CSeq_recv = rtsp->CSeq_recv;
    if((data->set.rtspreq != RTSPREQ_RECEIVE) && (CSeq_sent != CSeq_recv)) {
      failf(data,
            "The CSeq of this request %ld did not match the response %ld",
            CSeq_sent, CSeq_recv);
      return CURLE_RTSP_CSEQ_ERROR;
    }
    if(data->set.rtspreq == RTSPREQ_RECEIVE &&
       (data->conn->proto.rtspc.rtp_channel == -1)) {
      infof(data, "Got an RTP Receive with a CSeq of %ld", CSeq_recv);
    }

    /* Here is the added code */
    if(data->set.rtspreq == RTSPREQ_RECEIVE &&
       data->req.eos_written) {
      failf(data, "The req.eos_written  bit is set in the Receive request");
      return CURLE_RECV_ERROR;
    }
  }

  return httpStatus;
}

This change works. Is it OK to check via data->req.eos_written? Can it be merged into this project?

@icing
Copy link
Contributor

icing commented Nov 22, 2024

Thanks for your report. If I understand the issue correctly, the transfer ends prematurely. I wonder if data->req.eos_written is the best test here. What is the value of the premature parameter in such an invocation of rtsp_done()?

@icing icing self-assigned this Nov 22, 2024
@dengjfzh
Copy link
Contributor Author

The value of the premature is FALSE.
It is called by multi_runsingle()->multi_done(data, result, FALSE)->conn->handler->done(data, status, premature)
截图 2024-11-22 20-47-36

@icing
Copy link
Contributor

icing commented Nov 22, 2024

Thanks @dengjfzh. Lacking a real RTSP server, I am not sure what I am looking at. In RTSPREQ_RECEIVE mode, there should be a Content-Length set, right? If the server closes the connection early, this should be detected. The bug might lurk in another place than your proposed fix.

Could you run this with a curl_global_trace("all") after the global curl init? May that trace will help us to see where things go wrong. Thanks!

@dengjfzh
Copy link
Contributor Author

RTSPREQ_RECEIVE is a special request because it does not send any data to the server. The application may call this function in order to receive interleaved RTP data. https://curl.se/libcurl/c/CURLOPT_RTSP_REQUEST.html
I think it may be because of this reason that the server disconnection is not detected when calling RTSPREQ_RECEIVE.

@icing
Copy link
Contributor

icing commented Nov 22, 2024

Ok, learning about this protocol a bit. As I understand now, RTSPREQ_RECEIVE is to do a receive of intermediary data the server might send. When the server sends, it returns that data. However, when the connection closes, it curl considers the transfer done, without error. That is what you want to change.

Reading the RTSP RFC, this protocol looks like HTTP but is not. Especially when it comes to RTSP sessions. Opening one introduces a state in the connection. If the server closes the connection before the client issues a TEARDOWN, the RTSP session is broken and that should error, as you say. Did I get that right?

If so, I understand your proposed fix. It implies that a client never does RTSPREQ_RECEIVE out of the blue, but while things are ongoing and should continue to do so. The PR is fine then.

@dengjfzh
Copy link
Contributor Author

dengjfzh commented Nov 22, 2024

Ok, learning about this protocol a bit. As I understand now, RTSPREQ_RECEIVE is to do a receive of intermediary data the server might send. When the server sends, it returns that data. However, when the connection closes, it curl considers the transfer done, without error. That is what you want to change.

Reading the RTSP RFC, this protocol looks like HTTP but is not. Especially when it comes to RTSP sessions. Opening one introduces a state in the connection. If the server closes the connection before the client issues a TEARDOWN, the RTSP session is broken and that should error, as you say. Did I get that right?

If so, I understand your proposed fix. It implies that a client never does RTSPREQ_RECEIVE out of the blue, but while things are ongoing and should continue to do so. The PR is fine then.

That's right.

This is my test data:
(I've added the curl_global_trace("all") call before the program starts. Also, I've recompiled curl with ENABLE_CURLDEBUG and ENABLE_DEBUG turned on.)

log file:
log.txt
The connection is broken here:
The connection is broken here.

Network capture file:
Network capture file rtsp.pcap.tar.gz
截图 2024-11-22 23-02-40

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging a pull request may close this issue.

2 participants