/***************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at https://curl.se/docs/copyright.html.
 *
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
 * copies of the Software, and permit persons to whom the Software is
 * furnished to do so, under the terms of the COPYING file.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 * SPDX-License-Identifier: curl
 *
 ***************************************************************************/
#include "curl_setup.h"
#include "urldata.h"
#include "rtsp.h"

#ifndef CURL_DISABLE_RTSP

#include "transfer.h"
#include "sendf.h"
#include "curl_trc.h"
#include "multiif.h"
#include "http.h"
#include "url.h"
#include "progress.h"
#include "strcase.h"
#include "select.h"
#include "connect.h"
#include "cfilters.h"
#include "curlx/strdup.h"
#include "bufref.h"
#include "curlx/strparse.h"

/* meta key for storing protocol meta at easy handle */
#define CURL_META_RTSP_EASY   "meta:proto:rtsp:easy"
/* meta key for storing protocol meta at connection */
#define CURL_META_RTSP_CONN   "meta:proto:rtsp:conn"

typedef enum {
  RTP_PARSE_SKIP,
  RTP_PARSE_CHANNEL,
  RTP_PARSE_LEN,
  RTP_PARSE_DATA
} rtp_parse_st;

/* RTSP Connection data
 * Currently, only used for tracking incomplete RTP data reads */
struct rtsp_conn {
  struct dynbuf buf;
  int rtp_channel;
  size_t rtp_len;
  rtp_parse_st state;
  BIT(in_header);
};

/* RTSP transfer data */
struct RTSP {
  uint32_t CSeq_sent; /* CSeq of this request */
  uint32_t CSeq_recv; /* CSeq received */
};

#define RTP_PKT_LENGTH(p) ((((unsigned int)((unsigned char)((p)[2]))) << 8) | \
                            ((unsigned int)((unsigned char)((p)[3]))))

/* this returns the socket to wait for in the DO and DOING state for the multi
   interface and then we are always _sending_ a request and thus we wait for
   the single socket to become writable only */
static CURLcode rtsp_do_pollset(struct Curl_easy *data,
                                struct easy_pollset *ps)
{
  /* write mode */
  return Curl_pollset_add_out(data, ps, data->conn->sock[FIRSTSOCKET]);
}

#define MAX_RTP_BUFFERSIZE 1000000 /* arbitrary */

static void rtsp_easy_dtor(void *key, size_t klen, void *entry)
{
  struct RTSP *rtsp = entry;
  (void)key;
  (void)klen;
  curlx_free(rtsp);
}

static void rtsp_conn_dtor(void *key, size_t klen, void *entry)
{
  struct rtsp_conn *rtspc = entry;
  (void)key;
  (void)klen;
  curlx_dyn_free(&rtspc->buf);
  curlx_free(rtspc);
}

static CURLcode rtsp_setup_connection(struct Curl_easy *data,
                                      struct connectdata *conn)
{
  struct rtsp_conn *rtspc;
  struct RTSP *rtsp;

  rtspc = curlx_calloc(1, sizeof(*rtspc));
  if(!rtspc)
    return CURLE_OUT_OF_MEMORY;
  curlx_dyn_init(&rtspc->buf, MAX_RTP_BUFFERSIZE);
  if(Curl_conn_meta_set(conn, CURL_META_RTSP_CONN, rtspc, rtsp_conn_dtor))
    return CURLE_OUT_OF_MEMORY;

  rtsp = curlx_calloc(1, sizeof(struct RTSP));
  if(!rtsp ||
     Curl_meta_set(data, CURL_META_RTSP_EASY, rtsp, rtsp_easy_dtor))
    return CURLE_OUT_OF_MEMORY;

  return CURLE_OK;
}

/*
 * Function to check on various aspects of a connection.
 */
static bool rtsp_conn_is_dead(struct Curl_easy *data,
                              struct connectdata *conn)
{
  bool input_pending;
  /* Contrary to default handling, this protocol allows pending
   * input on an unused connection. */
  return !Curl_conn_is_alive(data, conn, &input_pending);
}

static CURLcode rtsp_connect(struct Curl_easy *data, bool *done)
{
  struct rtsp_conn *rtspc =
    Curl_conn_meta_get(data->conn, CURL_META_RTSP_CONN);

  if(!rtspc)
    return CURLE_FAILED_INIT;

  /* Initialize the CSeq if not already done */
  if(data->state.rtsp_next_client_CSeq == 0)
    data->state.rtsp_next_client_CSeq = 1;
  if(data->state.rtsp_next_server_CSeq == 0)
    data->state.rtsp_next_server_CSeq = 1;

  rtspc->rtp_channel = -1;
  *done = TRUE;
  return CURLE_OK;
}

static CURLcode rtsp_done(struct Curl_easy *data,
                          CURLcode status, bool premature)
{
  struct rtsp_conn *rtspc =
    Curl_conn_meta_get(data->conn, CURL_META_RTSP_CONN);
  struct RTSP *rtsp = Curl_meta_get(data, CURL_META_RTSP_EASY);
  CURLcode result;

  if(!rtspc || !rtsp)
    return CURLE_FAILED_INIT;

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

  result = Curl_http_done(data, status, premature);

  if(!status && !result) {
    /* Check the sequence numbers */
    uint32_t CSeq_sent = rtsp->CSeq_sent;
    uint32_t CSeq_recv = rtsp->CSeq_recv;
    if((data->set.rtspreq != RTSPREQ_RECEIVE) && (CSeq_sent != CSeq_recv)) {
      failf(data,
            "The CSeq of this request %u did not match the response %u",
            CSeq_sent, CSeq_recv);
      return CURLE_RTSP_CSEQ_ERROR;
    }
    if(data->set.rtspreq == RTSPREQ_RECEIVE && (rtspc->rtp_channel == -1)) {
      infof(data, "Got an RTP Receive with a CSeq of %u", CSeq_recv);
    }
    if(data->set.rtspreq == RTSPREQ_RECEIVE &&
       data->req.eos_written) {
      failf(data, "Server prematurely closed the RTSP connection.");
      return CURLE_RECV_ERROR;
    }
  }

  return result;
}

static CURLcode rtsp_setup_body(struct Curl_easy *data,
                                Curl_RtspReq rtspreq,
                                struct dynbuf *reqp)
{
  CURLcode result;
  if(rtspreq == RTSPREQ_ANNOUNCE ||
     rtspreq == RTSPREQ_SET_PARAMETER ||
     rtspreq == RTSPREQ_GET_PARAMETER) {
    curl_off_t req_clen; /* request content length */

    if(data->state.upload) {
      req_clen = data->state.infilesize;
      data->state.httpreq = HTTPREQ_PUT;
      result = Curl_creader_set_fread(data, req_clen);
      if(result)
        return result;
    }
    else {
      if(data->set.postfields) {
        size_t plen = (data->set.postfieldsize >= 0) ?
          (size_t)data->set.postfieldsize : strlen(data->set.postfields);
        req_clen = (curl_off_t)plen;
        result = Curl_creader_set_buf(data, data->set.postfields, plen);
      }
      else if(data->state.infilesize >= 0) {
        req_clen = data->state.infilesize;
        result = Curl_creader_set_fread(data, req_clen);
      }
      else {
        req_clen = 0;
        result = Curl_creader_set_null(data);
      }
      if(result)
        return result;
    }

    if(req_clen > 0) {
      /* As stated in the http comments, it is probably not wise to
       * actually set a custom Content-Length in the headers */
      if(!Curl_checkheaders(data, STRCONST("Content-Length"))) {
        result = curlx_dyn_addf(reqp, "Content-Length: %" FMT_OFF_T "\r\n",
                                req_clen);
        if(result)
          return result;
      }

      if(rtspreq == RTSPREQ_SET_PARAMETER ||
         rtspreq == RTSPREQ_GET_PARAMETER) {
        if(!Curl_checkheaders(data, STRCONST("Content-Type"))) {
          result = curlx_dyn_addn(reqp, STRCONST("Content-Type: "
                                                 "text/parameters\r\n"));
          if(result)
            return result;
        }
      }

      if(rtspreq == RTSPREQ_ANNOUNCE) {
        if(!Curl_checkheaders(data, STRCONST("Content-Type"))) {
          result = curlx_dyn_addn(reqp, STRCONST("Content-Type: "
                                                 "application/sdp\r\n"));
          if(result)
            return result;
        }
      }
    }
    else if(rtspreq == RTSPREQ_GET_PARAMETER) {
      /* Check for an empty GET_PARAMETER (heartbeat) request */
      data->state.httpreq = HTTPREQ_HEAD;
      data->req.no_body = TRUE;
    }
  }
  else
    result = Curl_creader_set_null(data);
  return result;
}

static CURLcode rtsp_do(struct Curl_easy *data, bool *done)
{
  struct connectdata *conn = data->conn;
  CURLcode result = CURLE_OK;
  const Curl_RtspReq rtspreq = data->set.rtspreq;
  struct RTSP *rtsp = Curl_meta_get(data, CURL_META_RTSP_EASY);
  struct dynbuf req_buffer;
  const unsigned char httpversion = 11; /* RTSP is close to HTTP/1.1, sort
                                           of... */
  const char *p_request = NULL;
  const char *p_session_id = NULL;
  const char *p_accept = NULL;
  const char *p_accept_encoding = NULL;
  const char *p_range = NULL;
  const char *p_referrer = NULL;
  const char *p_stream_uri = NULL;
  const char *p_transport = NULL;
  const char *p_uagent = NULL;
  const char *p_hd_proxy_auth = NULL;
  const char *p_hd_auth = NULL;

  *done = TRUE;
  if(!rtsp)
    return CURLE_FAILED_INIT;

  /* Initialize a dynamic send buffer */
  curlx_dyn_init(&req_buffer, DYN_RTSP_REQ_HEADER);

  rtsp->CSeq_sent = data->state.rtsp_next_client_CSeq;
  rtsp->CSeq_recv = 0;

  /* Setup the first_* fields to allow auth details get sent
     to this origin */

  if(!data->state.first_origin)
    Curl_peer_link(&data->state.first_origin, conn->origin);

  /* Setup the 'p_request' pointer to the proper p_request string
   * Since all RTSP requests are included here, there is no need to
   * support custom requests like HTTP.
   **/
  data->req.no_body = TRUE; /* most requests do not contain a body */
  switch(rtspreq) {
  default:
    failf(data, "Got invalid RTSP request");
    return CURLE_BAD_FUNCTION_ARGUMENT;
  case RTSPREQ_OPTIONS:
    p_request = "OPTIONS";
    break;
  case RTSPREQ_DESCRIBE:
    p_request = "DESCRIBE";
    data->req.no_body = FALSE;
    break;
  case RTSPREQ_ANNOUNCE:
    p_request = "ANNOUNCE";
    break;
  case RTSPREQ_SETUP:
    p_request = "SETUP";
    break;
  case RTSPREQ_PLAY:
    p_request = "PLAY";
    break;
  case RTSPREQ_PAUSE:
    p_request = "PAUSE";
    break;
  case RTSPREQ_TEARDOWN:
    p_request = "TEARDOWN";
    break;
  case RTSPREQ_GET_PARAMETER:
    /* GET_PARAMETER's no_body status is determined later */
    p_request = "GET_PARAMETER";
    data->req.no_body = FALSE;
    break;
  case RTSPREQ_SET_PARAMETER:
    p_request = "SET_PARAMETER";
    break;
  case RTSPREQ_RECORD:
    p_request = "RECORD";
    break;
  case RTSPREQ_RECEIVE:
    p_request = "";
    /* Treat interleaved RTP as body */
    data->req.no_body = FALSE;
    break;
  case RTSPREQ_LAST:
    failf(data, "Got invalid RTSP request: RTSPREQ_LAST");
    return CURLE_BAD_FUNCTION_ARGUMENT;
  }

  if(rtspreq == RTSPREQ_RECEIVE) {
    Curl_xfer_setup_recv(data, FIRSTSOCKET, -1);
    goto out;
  }

  p_session_id = data->set.str[STRING_RTSP_SESSION_ID];
  if(!p_session_id &&
     (rtspreq & ~(Curl_RtspReq)(RTSPREQ_OPTIONS |
                                RTSPREQ_DESCRIBE |
                                RTSPREQ_SETUP))) {
    failf(data, "Refusing to issue an RTSP request [%s] without a session ID.",
          p_request);
    result = CURLE_BAD_FUNCTION_ARGUMENT;
    goto out;
  }

  /* Stream URI. Default to server '*' if not specified */
  if(data->set.str[STRING_RTSP_STREAM_URI]) {
    p_stream_uri = data->set.str[STRING_RTSP_STREAM_URI];
  }
  else {
    p_stream_uri = "*";
  }

  /* Transport Header for SETUP requests */
  p_transport = Curl_checkheaders(data, STRCONST("Transport"));
  if(rtspreq == RTSPREQ_SETUP && !p_transport) {
    /* New Transport: setting? */
    if(data->set.str[STRING_RTSP_TRANSPORT]) {
      curlx_free(data->state.aptr.rtsp_transport);
      data->state.aptr.rtsp_transport =
        curl_maprintf("Transport: %s\r\n",
                      data->set.str[STRING_RTSP_TRANSPORT]);
      if(!data->state.aptr.rtsp_transport)
        return CURLE_OUT_OF_MEMORY;
    }
    else {
      failf(data,
            "Refusing to issue an RTSP SETUP without a Transport: header.");
      result = CURLE_BAD_FUNCTION_ARGUMENT;
      goto out;
    }

    p_transport = data->state.aptr.rtsp_transport;
  }

  /* Accept Headers for DESCRIBE requests */
  if(rtspreq == RTSPREQ_DESCRIBE) {
    /* Accept Header */
    p_accept = Curl_checkheaders(data, STRCONST("Accept")) ?
      NULL : "Accept: application/sdp\r\n";

    /* Accept-Encoding header */
    if(!Curl_checkheaders(data, STRCONST("Accept-Encoding")) &&
       data->set.str[STRING_ENCODING]) {
      curlx_free(data->state.aptr.accept_encoding);
      data->state.aptr.accept_encoding =
        curl_maprintf("Accept-Encoding: %s\r\n",
                      data->set.str[STRING_ENCODING]);

      if(!data->state.aptr.accept_encoding) {
        result = CURLE_OUT_OF_MEMORY;
        goto out;
      }
      p_accept_encoding = data->state.aptr.accept_encoding;
    }
  }

  /* The User-Agent string might have been allocated already, because
     it might have been used in the proxy connect, but if we have got a header
     with the user-agent string specified, we erase the previously made string
     here. */
  if(Curl_checkheaders(data, STRCONST("User-Agent")) &&
     data->state.aptr.uagent) {
    curlx_safefree(data->state.aptr.uagent);
  }
  else if(!Curl_checkheaders(data, STRCONST("User-Agent")) &&
          data->set.str[STRING_USERAGENT]) {
    p_uagent = data->state.aptr.uagent;
  }

  /* setup the authentication headers */
  result = Curl_http_output_auth(data, conn, p_request, HTTPREQ_GET,
                                 p_stream_uri, NULL, FALSE);
  if(result)
    goto out;

#ifndef CURL_DISABLE_PROXY
  p_hd_proxy_auth = data->req.hd_proxy_auth;
#endif
  p_hd_auth = data->req.hd_auth;

  /* Referrer */
  curlx_safefree(data->state.aptr.ref);
  if(Curl_bufref_ptr(&data->state.referer) &&
     !Curl_checkheaders(data, STRCONST("Referer")))
    data->state.aptr.ref =
      curl_maprintf("Referer: %s\r\n", Curl_bufref_ptr(&data->state.referer));

  p_referrer = data->state.aptr.ref;

  /*
   * Range Header
   * Only applies to PLAY, PAUSE, RECORD
   *
   * Go ahead and use the Range stuff supplied for HTTP
   */
  if(data->state.use_range &&
     (rtspreq & (RTSPREQ_PLAY | RTSPREQ_PAUSE | RTSPREQ_RECORD))) {

    /* Check to see if there is a range set in the custom headers */
    if(!Curl_checkheaders(data, STRCONST("Range")) && data->state.range) {
      curlx_free(data->state.aptr.rangeline);
      data->state.aptr.rangeline = curl_maprintf("Range: %s\r\n",
                                                 data->state.range);
      p_range = data->state.aptr.rangeline;
    }
  }

  /*
   * Sanity check the custom headers
   */
  if(Curl_checkheaders(data, STRCONST("CSeq"))) {
    failf(data, "CSeq cannot be set as a custom header.");
    result = CURLE_RTSP_CSEQ_ERROR;
    goto out;
  }
  if(Curl_checkheaders(data, STRCONST("Session"))) {
    failf(data, "Session ID cannot be set as a custom header.");
    result = CURLE_BAD_FUNCTION_ARGUMENT;
    goto out;
  }

  result =
    curlx_dyn_addf(&req_buffer,
                   "%s %s RTSP/1.0\r\n" /* Request Stream-URI RTSP/1.0 */
                   "CSeq: %u\r\n", /* CSeq */
                   p_request, p_stream_uri, rtsp->CSeq_sent);
  if(result)
    goto out;

  /*
   * Rather than do a normal alloc line, keep the session_id unformatted
   * to make comparison easier
   */
  if(p_session_id) {
    result = curlx_dyn_addf(&req_buffer, "Session: %s\r\n", p_session_id);
    if(result)
      goto out;
  }

  /*
   * Shared HTTP-like options
   */
  result = curlx_dyn_addf(&req_buffer,
                          "%s" /* transport */
                          "%s" /* accept */
                          "%s" /* accept-encoding */
                          "%s" /* range */
                          "%s" /* referrer */
                          "%s" /* user-agent */
                          "%s" /* hd_proxy_auth */
                          "%s" /* hd_auth */
                          ,
                          p_transport ? p_transport : "",
                          p_accept ? p_accept : "",
                          p_accept_encoding ? p_accept_encoding : "",
                          p_range ? p_range : "",
                          p_referrer ? p_referrer : "",
                          p_uagent ? p_uagent : "",
                          p_hd_proxy_auth ? p_hd_proxy_auth : "",
                          p_hd_auth ? p_hd_auth : "");

  if(result)
    goto out;

  if((rtspreq == RTSPREQ_SETUP) || (rtspreq == RTSPREQ_DESCRIBE)) {
    result = Curl_add_timecondition(data, &req_buffer);
    if(result)
      goto out;
  }

  result = Curl_add_custom_headers(data, FALSE, httpversion, &req_buffer);
  if(result)
    goto out;

  result = rtsp_setup_body(data, rtspreq, &req_buffer);
  if(result)
    goto out;

  /* Finish the request buffer */
  result = curlx_dyn_addn(&req_buffer, STRCONST("\r\n"));
  if(result)
    goto out;

  Curl_xfer_setup_sendrecv(data, FIRSTSOCKET, -1);

  /* issue the request */
  result = Curl_req_send(data, &req_buffer, httpversion);
  if(result) {
    failf(data, "Failed sending RTSP request");
    goto out;
  }

  /* Increment the CSeq on success */
  data->state.rtsp_next_client_CSeq++;

  if(data->req.writebytecount) {
    /* if a request-body has been sent off, we make sure this progress is
       noted properly */
    Curl_pgrsSetUploadCounter(data, data->req.writebytecount);
    result = Curl_pgrsUpdate(data);
  }
out:
  curlx_dyn_free(&req_buffer);
  return result;
}

/**
 * write any BODY bytes missing to the client, ignore the rest.
 */
static CURLcode rtp_write_body_junk(struct Curl_easy *data,
                                    struct rtsp_conn *rtspc,
                                    const char *buf,
                                    size_t blen)
{
  curl_off_t body_remain;
  bool in_body;

  in_body = (data->req.headerline && !rtspc->in_header) &&
            (data->req.size >= 0) &&
            (data->req.bytecount < data->req.size);
  body_remain = in_body ? (data->req.size - data->req.bytecount) : 0;
  DEBUGASSERT(body_remain >= 0);
  if(body_remain) {
    if((curl_off_t)blen > body_remain)
      blen = (size_t)body_remain;
    return Curl_client_write(data, CLIENTWRITE_BODY, buf, blen);
  }
  return CURLE_OK;
}

static CURLcode rtp_client_write(struct Curl_easy *data, const char *ptr,
                                 size_t len)
{
  size_t wrote;
  curl_write_callback writeit;
  void *user_ptr;

  if(len == 0) {
    failf(data, "Cannot write a 0 size RTP packet.");
    return CURLE_WRITE_ERROR;
  }

  /* If the user has configured CURLOPT_INTERLEAVEFUNCTION then use that
     function and any configured CURLOPT_INTERLEAVEDATA to write out the RTP
     data. Otherwise, use the CURLOPT_WRITEFUNCTION with the CURLOPT_WRITEDATA
     pointer to write out the RTP data. */
  if(data->set.fwrite_rtp) {
    writeit = data->set.fwrite_rtp;
    user_ptr = data->set.rtp_out;
  }
  else {
    writeit = data->set.fwrite_func;
    user_ptr = data->set.out;
  }

  Curl_set_in_callback(data, TRUE);
  wrote = writeit((char *)CURL_UNCONST(ptr), 1, len, user_ptr);
  Curl_set_in_callback(data, FALSE);

  if(wrote == CURL_WRITEFUNC_PAUSE) {
    failf(data, "Cannot pause RTP");
    return CURLE_WRITE_ERROR;
  }

  if(wrote != len) {
    failf(data, "Failed writing RTP data");
    return CURLE_WRITE_ERROR;
  }

  return CURLE_OK;
}

static CURLcode rtsp_filter_rtp(struct Curl_easy *data,
                                struct rtsp_conn *rtspc,
                                const char *buf,
                                size_t blen,
                                size_t *pconsumed)
{
  CURLcode result = CURLE_OK;
  size_t skip_len = 0;

  *pconsumed = 0;
  while(blen) {
    bool in_body = (data->req.headerline && !rtspc->in_header) &&
                   (data->req.size >= 0) &&
                   (data->req.bytecount < data->req.size);
    switch(rtspc->state) {

    case RTP_PARSE_SKIP: {
      DEBUGASSERT(curlx_dyn_len(&rtspc->buf) == 0);
      while(blen && buf[0] != '$') {
        if(!in_body && buf[0] == 'R' &&
           data->set.rtspreq != RTSPREQ_RECEIVE) {
          if(strncmp(buf, "RTSP/", (blen < 5) ? blen : 5) == 0) {
            /* This could be the next response, no consume and return */
            if(*pconsumed) {
              DEBUGF(infof(data, "RTP rtsp_filter_rtp[SKIP] RTSP/ prefix, "
                           "skipping %zu bytes of junk", *pconsumed));
            }
            rtspc->state = RTP_PARSE_SKIP;
            rtspc->in_header = TRUE;
            goto out;
          }
        }
        /* junk/BODY, consume without buffering */
        *pconsumed += 1;
        ++buf;
        --blen;
        ++skip_len;
      }
      if(blen && buf[0] == '$') {
        /* possible start of an RTP message, buffer */
        if(skip_len) {
          /* end of junk/BODY bytes, flush */
          result = rtp_write_body_junk(data, rtspc, buf - skip_len, skip_len);
          skip_len = 0;
          if(result)
            goto out;
        }
        if(curlx_dyn_addn(&rtspc->buf, buf, 1)) {
          result = CURLE_OUT_OF_MEMORY;
          goto out;
        }
        *pconsumed += 1;
        ++buf;
        --blen;
        rtspc->state = RTP_PARSE_CHANNEL;
      }
      break;
    }

    case RTP_PARSE_CHANNEL: {
      int idx = ((unsigned char)buf[0]) / 8;
      int off = ((unsigned char)buf[0]) % 8;
      DEBUGASSERT(curlx_dyn_len(&rtspc->buf) == 1);
      if(!(data->state.rtp_channel_mask[idx] & (1 << off))) {
        /* invalid channel number, junk or BODY data */
        rtspc->state = RTP_PARSE_SKIP;
        DEBUGASSERT(skip_len == 0);
        /* we do not consume this byte, it is BODY data */
        DEBUGF(infof(data, "RTSP: invalid RTP channel %d, skipping", idx));
        if(*pconsumed == 0) {
          /* We did not consume the initial '$' in our buffer, but had
           * it from an earlier call. We cannot un-consume it and have
           * to write it directly as BODY data */
          result = rtp_write_body_junk(data, rtspc,
                                       curlx_dyn_ptr(&rtspc->buf), 1);
          if(result)
            goto out;
        }
        else {
          /* count the '$' as skip and continue */
          skip_len = 1;
        }
        curlx_dyn_free(&rtspc->buf);
        break;
      }
      /* a valid channel, so we expect this to be a real RTP message */
      rtspc->rtp_channel = (unsigned char)buf[0];
      if(curlx_dyn_addn(&rtspc->buf, buf, 1)) {
        result = CURLE_OUT_OF_MEMORY;
        goto out;
      }
      *pconsumed += 1;
      ++buf;
      --blen;
      rtspc->state = RTP_PARSE_LEN;
      break;
    }

    case RTP_PARSE_LEN: {
      size_t rtp_len = curlx_dyn_len(&rtspc->buf);
      const char *rtp_buf;
      DEBUGASSERT(rtp_len >= 2 && rtp_len < 4);
      if(curlx_dyn_addn(&rtspc->buf, buf, 1)) {
        result = CURLE_OUT_OF_MEMORY;
        goto out;
      }
      *pconsumed += 1;
      ++buf;
      --blen;
      if(rtp_len == 2)
        break;
      rtp_buf = curlx_dyn_ptr(&rtspc->buf);
      rtspc->rtp_len = RTP_PKT_LENGTH(rtp_buf) + 4;
      if(rtspc->rtp_len == 4) {
        /* zero-length payload, the 4-byte header is the complete RTP
           message. Dispatch immediately without entering RTP_PARSE_DATA. */
        DEBUGF(infof(data, "RTP write channel %d rtp_len %zu (no payload)",
                     rtspc->rtp_channel, rtspc->rtp_len));
        result = rtp_client_write(data, rtp_buf, rtspc->rtp_len);
        curlx_dyn_free(&rtspc->buf);
        rtspc->state = RTP_PARSE_SKIP;
        if(result)
          goto out;
        break;
      }
      rtspc->state = RTP_PARSE_DATA;
      break;
    }

    case RTP_PARSE_DATA: {
      size_t rtp_len = curlx_dyn_len(&rtspc->buf);
      size_t needed;
      DEBUGASSERT(rtp_len < rtspc->rtp_len);
      needed = rtspc->rtp_len - rtp_len;
      if(needed <= blen) {
        if(curlx_dyn_addn(&rtspc->buf, buf, needed)) {
          result = CURLE_OUT_OF_MEMORY;
          goto out;
        }
        *pconsumed += needed;
        buf += needed;
        blen -= needed;
        /* complete RTP message in buffer */
        DEBUGF(infof(data, "RTP write channel %d rtp_len %zu",
                     rtspc->rtp_channel, rtspc->rtp_len));
        result = rtp_client_write(data, curlx_dyn_ptr(&rtspc->buf),
                                  rtspc->rtp_len);
        curlx_dyn_free(&rtspc->buf);
        rtspc->state = RTP_PARSE_SKIP;
        if(result)
          goto out;
      }
      else {
        if(curlx_dyn_addn(&rtspc->buf, buf, blen)) {
          result = CURLE_OUT_OF_MEMORY;
          goto out;
        }
        *pconsumed += blen;
        buf += blen;
        blen = 0;
      }
      break;
    }

    default:
      DEBUGASSERT(0);
      return CURLE_RECV_ERROR;
    }
  }
out:
  if(!result && skip_len)
    result = rtp_write_body_junk(data, rtspc, buf - skip_len, skip_len);
  return result;
}

/*
 * Parse and write out an RTSP response.
 * @param data     the transfer
 * @param conn     the connection
 * @param buf      data read from connection
 * @param blen     amount of data in buf
 * @param is_eos   TRUE iff this is the last write
 * @param readmore out, TRUE iff complete buf was consumed and more data
 *                 is needed
 */
static CURLcode rtsp_rtp_write_resp(struct Curl_easy *data,
                                    const char *buf,
                                    size_t blen,
                                    bool is_eos)
{
  struct rtsp_conn *rtspc =
    Curl_conn_meta_get(data->conn, CURL_META_RTSP_CONN);
  CURLcode result = CURLE_OK;
  size_t consumed = 0;

  if(!rtspc)
    return CURLE_FAILED_INIT;

  if(!data->req.header)
    rtspc->in_header = FALSE;
  if(!blen) {
    goto out;
  }

  DEBUGF(infof(data, "rtsp_rtp_write_resp(len=%zu, in_header=%d, eos=%d)",
               blen, rtspc->in_header, is_eos));

  /* If header parsing is not ongoing, extract RTP messages */
  if(!rtspc->in_header) {
    result = rtsp_filter_rtp(data, rtspc, buf, blen, &consumed);
    if(result)
      goto out;
    buf += consumed;
    blen -= consumed;
    /* either we consumed all or are at the start of header parsing */
    if(blen && !data->req.header)
      DEBUGF(infof(data, "RTSP: %zu bytes, possibly excess in response body",
                   blen));
  }

  /* we want to parse headers, do so */
  if(data->req.header && blen) {
    rtspc->in_header = TRUE;
    result = Curl_http_write_resp_hds(data, buf, blen, &consumed);
    if(result)
      goto out;

    buf += consumed;
    blen -= consumed;

    if(!data->req.header)
      rtspc->in_header = FALSE;

    if(!rtspc->in_header) {
      /* If header parsing is done, extract interleaved RTP messages */
      if(data->req.size <= -1) {
        /* Respect section 4.4 of rfc2326: If the Content-Length header is
           absent, a length 0 must be assumed. */
        data->req.size = 0;
        data->req.download_done = TRUE;
      }
      result = rtsp_filter_rtp(data, rtspc, buf, blen, &consumed);
      if(result)
        goto out;
      blen -= consumed;
    }
  }

  if(rtspc->state != RTP_PARSE_SKIP)
    data->req.done = FALSE;
  /* we SHOULD have consumed all bytes, unless the response is borked.
   * In which case we write out the left over bytes, letting the client
   * writer deal with it (it will report EXCESS and fail the transfer). */
  DEBUGF(infof(data, "rtsp_rtp_write_resp(len=%zu, in_header=%d, done=%d, "
               "rtspc->state=%d, req.size=%" FMT_OFF_T ")",
               blen, rtspc->in_header, data->req.done, rtspc->state,
               data->req.size));
  if(!result && (is_eos || blen)) {
    result = Curl_client_write(data, CLIENTWRITE_BODY |
                               (is_eos ? CLIENTWRITE_EOS : 0), buf, blen);
  }

out:
  if((data->set.rtspreq == RTSPREQ_RECEIVE) &&
     (rtspc->state == RTP_PARSE_SKIP)) {
    /* In special mode RECEIVE, we process one chunk of network
     * data, so we stop the transfer here, if we have no incomplete
     * RTP message pending. */
    data->req.download_done = TRUE;
  }
  return result;
}

static CURLcode rtsp_rtp_write_resp_hd(struct Curl_easy *data,
                                       const char *buf,
                                       size_t blen,
                                       bool is_eos)
{
  return rtsp_rtp_write_resp(data, buf, blen, is_eos);
}

static CURLcode rtsp_parse_transport(struct Curl_easy *data,
                                     const char *transport)
{
  /* If we receive multiple Transport response-headers, the interleaved
     channels of each response header is recorded and used together for
     subsequent data validity checks.*/
  /* e.g.: ' RTP/AVP/TCP;unicast;interleaved=5-6' */
  const char *start, *end;
  start = transport;
  while(start && *start) {
    curlx_str_passblanks(&start);
    end = strchr(start, ';');
    if(checkprefix("interleaved=", start)) {
      curl_off_t chan1, chan2, chan;
      const char *p = start + 12;
      if(!curlx_str_number(&p, &chan1, 255)) {
        unsigned char *rtp_channel_mask = data->state.rtp_channel_mask;
        chan2 = chan1;
        if(!curlx_str_single(&p, '-')) {
          if(curlx_str_number(&p, &chan2, 255)) {
            infof(data, "Unable to read the interleaved parameter from "
                  "Transport header: [%s]", transport);
            chan2 = chan1;
          }
        }
        for(chan = chan1; chan <= chan2; chan++) {
          int idx = (int)chan / 8;
          int off = (int)chan % 8;
          rtp_channel_mask[idx] |= (unsigned char)(1 << off);
        }
      }
      else {
        infof(data, "Unable to read the interleaved parameter from "
              "Transport header: [%s]", transport);
      }
      break;
    }
    /* skip to next parameter */
    start = (!end) ? end : (end + 1);
  }
  return CURLE_OK;
}

CURLcode Curl_rtsp_parseheader(struct Curl_easy *data, const char *header)
{
  if(checkprefix("CSeq:", header)) {
    curl_off_t CSeq = 0;
    struct RTSP *rtsp = Curl_meta_get(data, CURL_META_RTSP_EASY);
    const char *p = &header[5];
    if(!rtsp)
      return CURLE_FAILED_INIT;
    curlx_str_passblanks(&p);
    if(curlx_str_number(&p, &CSeq, UINT_MAX)) {
      failf(data, "Unable to read the CSeq header: [%s]", header);
      return CURLE_RTSP_CSEQ_ERROR;
    }
    data->state.rtsp_CSeq_recv = rtsp->CSeq_recv = (uint32_t)CSeq;
  }
  else if(checkprefix("Session:", header)) {
    const char *start, *end;
    size_t idlen;

    /* Find the first non-space letter */
    start = header + 8;
    curlx_str_passblanks(&start);

    if(!*start) {
      failf(data, "Got a blank Session ID");
      return CURLE_RTSP_SESSION_ERROR;
    }

    /* Find the end of Session ID
     *
     * Allow any non whitespace content, up to the field separator or end of
     * line. RFC 2326 is not 100% clear on the session ID and for example
     * gstreamer does URL-encoded session ID's not covered by the standard.
     */
    end = start;
    while((*end > ' ') && (*end != ';'))
      end++;
    idlen = end - start;

    if(data->set.str[STRING_RTSP_SESSION_ID]) {

      /* If the Session ID is set, then compare */
      if(strlen(data->set.str[STRING_RTSP_SESSION_ID]) != idlen ||
         strncmp(start, data->set.str[STRING_RTSP_SESSION_ID], idlen)) {
        failf(data, "Got RTSP Session ID Line [%s], but wanted ID [%s]",
              start, data->set.str[STRING_RTSP_SESSION_ID]);
        return CURLE_RTSP_SESSION_ERROR;
      }
    }
    else {
      /* If the Session ID is not set, and we find it in a response, then set
       * it.
       */

      /* Copy the id substring into a new buffer */
      data->set.str[STRING_RTSP_SESSION_ID] = curlx_memdup0(start, idlen);
      if(!data->set.str[STRING_RTSP_SESSION_ID])
        return CURLE_OUT_OF_MEMORY;
    }
  }
  else if(checkprefix("Transport:", header)) {
    CURLcode result;
    result = rtsp_parse_transport(data, header + 10);
    if(result)
      return result;
  }
  return CURLE_OK;
}

/*
 * RTSP handler interface.
 */
const struct Curl_protocol Curl_protocol_rtsp = {
  rtsp_setup_connection,                /* setup_connection */
  rtsp_do,                              /* do_it */
  rtsp_done,                            /* done */
  ZERO_NULL,                            /* do_more */
  rtsp_connect,                         /* connect_it */
  ZERO_NULL,                            /* connecting */
  ZERO_NULL,                            /* doing */
  ZERO_NULL,                            /* proto_pollset */
  rtsp_do_pollset,                      /* doing_pollset */
  ZERO_NULL,                            /* domore_pollset */
  Curl_http_perform_pollset,            /* perform_pollset */
  ZERO_NULL,                            /* disconnect */
  rtsp_rtp_write_resp,                  /* write_resp */
  rtsp_rtp_write_resp_hd,               /* write_resp_hd */
  rtsp_conn_is_dead,                    /* connection_is_dead */
  ZERO_NULL,                            /* attach connection */
  Curl_http_follow,                     /* follow */
};

#endif /* CURL_DISABLE_RTSP */
