Allow RtspHeaders to take multiple header values under the same name.

Some RTSP servers will offer multiple WWW-Authenticate options. We wanted to
be able to pick them up.

#minor-release

PiperOrigin-RevId: 375907276
This commit is contained in:
claincly 2021-05-26 11:36:23 +01:00 committed by Oliver Woodman
parent 99d3773eb9
commit 088ad91017
5 changed files with 215 additions and 131 deletions

View file

@ -20,9 +20,8 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables;
import java.util.List;
import java.util.Map;
@ -35,45 +34,45 @@ import java.util.Map;
*/
/* package */ final class RtspHeaders {
public static final String ACCEPT = "Accept";
public static final String ALLOW = "Allow";
public static final String AUTHORIZATION = "Authorization";
public static final String BANDWIDTH = "Bandwidth";
public static final String BLOCKSIZE = "Blocksize";
public static final String CACHE_CONTROL = "Cache-Control";
public static final String CONNECTION = "Connection";
public static final String CONTENT_BASE = "Content-Base";
public static final String CONTENT_ENCODING = "Content-Encoding";
public static final String CONTENT_LANGUAGE = "Content-Language";
public static final String CONTENT_LENGTH = "Content-Length";
public static final String CONTENT_LOCATION = "Content-Location";
public static final String CONTENT_TYPE = "Content-Type";
public static final String CSEQ = "CSeq";
public static final String DATE = "Date";
public static final String EXPIRES = "Expires";
public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
public static final String PROXY_REQUIRE = "Proxy-Require";
public static final String PUBLIC = "Public";
public static final String RANGE = "Range";
public static final String RTP_INFO = "RTP-Info";
public static final String RTCP_INTERVAL = "RTCP-Interval";
public static final String SCALE = "Scale";
public static final String SESSION = "Session";
public static final String SPEED = "Speed";
public static final String SUPPORTED = "Supported";
public static final String TIMESTAMP = "Timestamp";
public static final String TRANSPORT = "Transport";
public static final String USER_AGENT = "User-Agent";
public static final String VIA = "Via";
public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
public static final String ACCEPT = "accept";
public static final String ALLOW = "allow";
public static final String AUTHORIZATION = "authorization";
public static final String BANDWIDTH = "bandwidth";
public static final String BLOCKSIZE = "blocksize";
public static final String CACHE_CONTROL = "cache-control";
public static final String CONNECTION = "connection";
public static final String CONTENT_BASE = "content-base";
public static final String CONTENT_ENCODING = "content-encoding";
public static final String CONTENT_LANGUAGE = "content-language";
public static final String CONTENT_LENGTH = "content-length";
public static final String CONTENT_LOCATION = "content-location";
public static final String CONTENT_TYPE = "content-type";
public static final String CSEQ = "cseq";
public static final String DATE = "date";
public static final String EXPIRES = "expires";
public static final String PROXY_AUTHENTICATE = "proxy-authenticate";
public static final String PROXY_REQUIRE = "proxy-require";
public static final String PUBLIC = "public";
public static final String RANGE = "range";
public static final String RTP_INFO = "rtp-info";
public static final String RTCP_INTERVAL = "rtcp-interval";
public static final String SCALE = "scale";
public static final String SESSION = "session";
public static final String SPEED = "speed";
public static final String SUPPORTED = "supported";
public static final String TIMESTAMP = "timestamp";
public static final String TRANSPORT = "transport";
public static final String USER_AGENT = "user-agent";
public static final String VIA = "via";
public static final String WWW_AUTHENTICATE = "www-authenticate";
/** Builds {@link RtspHeaders} instances. */
public static final class Builder {
private final List<String> namesAndValues;
private final ImmutableListMultimap.Builder<String, String> namesAndValuesBuilder;
/** Creates a new instance. */
public Builder() {
namesAndValues = new ArrayList<>();
namesAndValuesBuilder = new ImmutableListMultimap.Builder<>();
}
/**
@ -84,8 +83,7 @@ import java.util.Map;
* @return This builder.
*/
public Builder add(String headerName, String headerValue) {
namesAndValues.add(headerName.trim());
namesAndValues.add(headerValue.trim());
namesAndValuesBuilder.put(Ascii.toLowerCase(headerName.trim()), headerValue.trim());
return this;
}
@ -130,37 +128,38 @@ import java.util.Map;
}
}
private final ImmutableList<String> namesAndValues;
private final ImmutableListMultimap<String, String> namesAndValues;
/**
* Gets the headers as a map, where the keys are the header names and values are the header
* values.
*
* @return The headers as a map. The keys of the map have follows those that are used to build
* this {@link RtspHeaders} instance.
* Returns a map that associates header names to the list of values associated with the
* corresponding header name.
*/
public ImmutableMap<String, String> asMap() {
Map<String, String> headers = new LinkedHashMap<>();
for (int i = 0; i < namesAndValues.size(); i += 2) {
headers.put(namesAndValues.get(i), namesAndValues.get(i + 1));
}
return ImmutableMap.copyOf(headers);
public ImmutableListMultimap<String, String> asMultiMap() {
return namesAndValues;
}
/**
* Returns a header value mapped to the argument, {@code null} if the header name is not recorded.
* Returns the most recent header value mapped to the argument, {@code null} if the header name is
* not recorded.
*/
@Nullable
public String get(String headerName) {
for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
if (Ascii.equalsIgnoreCase(headerName, namesAndValues.get(i))) {
return namesAndValues.get(i + 1);
}
ImmutableList<String> headerValues = values(headerName);
if (headerValues.isEmpty()) {
return null;
}
return null;
return Iterables.getLast(headerValues);
}
/**
* Returns a list of header values mapped to the argument, in the addition order. The returned
* list is empty if the header name is not recorded.
*/
public ImmutableList<String> values(String headerName) {
return namesAndValues.get(Ascii.toLowerCase(headerName));
}
private RtspHeaders(Builder builder) {
this.namesAndValues = ImmutableList.copyOf(builder.namesAndValues);
this.namesAndValues = builder.namesAndValuesBuilder.build();
}
}

View file

@ -39,7 +39,7 @@ import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableListMultimap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -94,11 +94,13 @@ import java.util.regex.Pattern;
builder.add(
Util.formatInvariant(
"%s %s %s", toMethodString(request.method), request.uri, RTSP_VERSION));
ImmutableMap<String, String> headers = request.headers.asMap();
ImmutableListMultimap<String, String> headers = request.headers.asMultiMap();
for (String headerName : headers.keySet()) {
builder.add(
Util.formatInvariant(
"%s: %s", headerName, checkNotNull(request.headers.get(headerName))));
ImmutableList<String> headerValuesForName = headers.get(headerName);
for (int i = 0; i < headerValuesForName.size(); i++) {
builder.add(Util.formatInvariant("%s: %s", headerName, headerValuesForName.get(i)));
}
}
// Empty line after headers.
builder.add("");
@ -119,11 +121,12 @@ import java.util.regex.Pattern;
Util.formatInvariant(
"%s %s %s", RTSP_VERSION, response.status, getRtspStatusReasonPhrase(response.status)));
ImmutableMap<String, String> headers = response.headers.asMap();
ImmutableListMultimap<String, String> headers = response.headers.asMultiMap();
for (String headerName : headers.keySet()) {
builder.add(
Util.formatInvariant(
"%s: %s", headerName, checkNotNull(response.headers.get(headerName))));
ImmutableList<String> headerValuesForName = headers.get(headerName);
for (int i = 0; i < headerValuesForName.size(); i++) {
builder.add(Util.formatInvariant("%s: %s", headerName, headerValuesForName.get(i)));
}
}
// Empty line after headers.
builder.add("");

View file

@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -82,7 +83,47 @@ public final class RtspHeadersTest {
}
@Test
public void asMap() {
public void get_withMultipleValuesMappedToTheSameName_getsTheMostRecentValue() {
RtspHeaders headers =
new RtspHeaders.Builder()
.addAll(
ImmutableList.of(
"WWW-Authenticate: Digest realm=\"2857be52f47f\","
+ " nonce=\"f4cba07ad14b5bf181ac77c5a92ba65f\", stale=\"FALSE\"",
"WWW-Authenticate: Basic realm=\"2857be52f47f\""))
.build();
assertThat(headers.get("WWW-Authenticate")).isEqualTo("Basic realm=\"2857be52f47f\"");
}
@Test
public void values_withNoHeaders_returnsAnEmptyList() {
RtspHeaders headers = new RtspHeaders.Builder().build();
assertThat(headers.values("WWW-Authenticate")).isEmpty();
}
@Test
public void values_withMultipleValuesMappedToTheSameName_returnsAllMappedValues() {
RtspHeaders headers =
new RtspHeaders.Builder()
.addAll(
ImmutableList.of(
"WWW-Authenticate: Digest realm=\"2857be52f47f\","
+ " nonce=\"f4cba07ad14b5bf181ac77c5a92ba65f\", stale=\"FALSE\"",
"WWW-Authenticate: Basic realm=\"2857be52f47f\""))
.build();
assertThat(headers.values("WWW-Authenticate"))
.containsExactly(
"Digest realm=\"2857be52f47f\", nonce=\"f4cba07ad14b5bf181ac77c5a92ba65f\","
+ " stale=\"FALSE\"",
"Basic realm=\"2857be52f47f\"")
.inOrder();
}
@Test
public void asMultiMap_withoutValuesMappedToTheSameName_getsTheMappedValuesInAdditionOrder() {
RtspHeaders headers =
new RtspHeaders.Builder()
.addAll(
@ -92,11 +133,39 @@ public final class RtspHeadersTest {
"Content-Length: 707",
"Transport: RTP/AVP;unicast;client_port=65458-65459\r\n"))
.build();
assertThat(headers.asMap())
assertThat(headers.asMultiMap())
.containsExactly(
"Accept", "application/sdp",
"CSeq", "3",
"Content-Length", "707",
"Transport", "RTP/AVP;unicast;client_port=65458-65459");
"accept", "application/sdp",
"cseq", "3",
"content-length", "707",
"transport", "RTP/AVP;unicast;client_port=65458-65459");
}
@Test
public void asMap_withMultipleValuesMappedToTheSameName_getsTheMappedValuesInAdditionOrder() {
RtspHeaders headers =
new RtspHeaders.Builder()
.addAll(
ImmutableList.of(
"Accept: application/sdp ", // Extra space after header value.
"Accept: application/sip ", // Extra space after header value.
"CSeq:3", // No space after colon.
"CSeq:5", // No space after colon.
"Transport: RTP/AVP;unicast;client_port=65456-65457",
"Transport: RTP/AVP;unicast;client_port=65458-65459\r\n"))
.build();
ListMultimap<String, String> headersMap = headers.asMultiMap();
assertThat(headersMap.keySet()).containsExactly("accept", "cseq", "transport").inOrder();
assertThat(headersMap)
.valuesForKey("accept")
.containsExactly("application/sdp", "application/sip")
.inOrder();
assertThat(headersMap).valuesForKey("cseq").containsExactly("3", "5").inOrder();
assertThat(headersMap)
.valuesForKey("transport")
.containsExactly(
"RTP/AVP;unicast;client_port=65456-65457", "RTP/AVP;unicast;client_port=65458-65459")
.inOrder();
}
}

View file

@ -141,18 +141,18 @@ public final class RtspMessageChannelTest {
assertThat(receivedRtspResponses)
.containsExactly(
/* optionsResponse */
ImmutableList.of("RTSP/1.0 200 OK", "CSeq: 2", "Public: OPTIONS", ""),
ImmutableList.of("RTSP/1.0 200 OK", "cseq: 2", "public: OPTIONS", ""),
/* describeResponse */
ImmutableList.of(
"RTSP/1.0 200 OK",
"CSeq: 3",
"Content-Type: application/sdp",
"Content-Length: 28",
"cseq: 3",
"content-type: application/sdp",
"content-length: 28",
"",
"v=安卓アンドロイド"),
/* setupResponse */
ImmutableList.of(
"RTSP/1.0 200 OK", "CSeq: 3", "Transport: RTP/AVP/TCP;unicast;interleaved=0-1", ""))
"RTSP/1.0 200 OK", "cseq: 3", "transport: RTP/AVP/TCP;unicast;interleaved=0-1", ""))
.inOrder();
assertThat(receivedInterleavedData)
.containsExactly(

View file

@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
@ -41,8 +42,10 @@ public final class RtspMessageUtilTest {
RtspRequest request = RtspMessageUtil.parseRequest(requestLines);
assertThat(request.method).isEqualTo(RtspRequest.METHOD_OPTIONS);
assertThat(request.headers.asMap())
.containsExactly(RtspHeaders.CSEQ, "2", RtspHeaders.USER_AGENT, "LibVLC/3.0.11");
assertThat(request.headers.asMultiMap())
.containsExactly(
RtspHeaders.CSEQ, "2",
RtspHeaders.USER_AGENT, "LibVLC/3.0.11");
assertThat(request.messageBody).isEmpty();
}
@ -57,12 +60,12 @@ public final class RtspMessageUtilTest {
RtspResponse response = RtspMessageUtil.parseResponse(responseLines);
assertThat(response.status).isEqualTo(200);
assertThat(response.headers.asMap())
assertThat(response.headers.asMultiMap())
.containsExactly(
RtspHeaders.CSEQ,
"2",
RtspHeaders.PUBLIC,
"OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER");
"OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER," + " SET_PARAMETER");
assertThat(response.messageBody).isEmpty();
}
@ -78,14 +81,11 @@ public final class RtspMessageUtilTest {
RtspRequest request = RtspMessageUtil.parseRequest(requestLines);
assertThat(request.method).isEqualTo(RtspRequest.METHOD_DESCRIBE);
assertThat(request.headers.asMap())
assertThat(request.headers.asMultiMap())
.containsExactly(
RtspHeaders.CSEQ,
"3",
RtspHeaders.USER_AGENT,
"LibVLC/3.0.11",
RtspHeaders.ACCEPT,
"application/sdp");
RtspHeaders.CSEQ, "3",
RtspHeaders.USER_AGENT, "LibVLC/3.0.11",
RtspHeaders.ACCEPT, "application/sdp");
assertThat(request.messageBody).isEmpty();
}
@ -111,16 +111,12 @@ public final class RtspMessageUtilTest {
RtspResponse response = RtspMessageUtil.parseResponse(responseLines);
assertThat(response.status).isEqualTo(200);
assertThat(response.headers.asMap())
assertThat(response.headers.asMultiMap())
.containsExactly(
RtspHeaders.CSEQ,
"3",
RtspHeaders.CONTENT_BASE,
"rtsp://127.0.0.1/test.mkv/",
RtspHeaders.CONTENT_TYPE,
"application/sdp",
RtspHeaders.CONTENT_LENGTH,
"707");
RtspHeaders.CSEQ, "3",
RtspHeaders.CONTENT_BASE, "rtsp://127.0.0.1/test.mkv/",
RtspHeaders.CONTENT_TYPE, "application/sdp",
RtspHeaders.CONTENT_LENGTH, "707");
assertThat(response.messageBody)
.isEqualTo(
@ -135,6 +131,30 @@ public final class RtspMessageUtilTest {
+ "a=control:track2");
}
@Test
public void parseResponse_with401DescribeResponse_succeeds() {
List<String> responseLines =
Arrays.asList(
"RTSP/1.0 401 Unauthorized",
"CSeq: 3",
"WWW-Authenticate: BASIC realm=\"wow\"",
"WWW-Authenticate: DIGEST realm=\"wow\", nonce=\"nonce\"",
"");
RtspResponse response = RtspMessageUtil.parseResponse(responseLines);
ListMultimap<String, String> headersMap = response.headers.asMultiMap();
assertThat(response.status).isEqualTo(401);
assertThat(headersMap.keySet()).containsExactly("cseq", "www-authenticate").inOrder();
assertThat(headersMap).valuesForKey("cseq").containsExactly("3");
assertThat(headersMap)
.valuesForKey("www-authenticate")
.containsExactly("BASIC realm=\"wow\"", "DIGEST realm=\"wow\", nonce=\"nonce\"")
.inOrder();
assertThat(response.messageBody).isEmpty();
}
@Test
public void parseRequest_withSetParameterRequest_succeeds() {
List<String> requestLines =
@ -149,16 +169,12 @@ public final class RtspMessageUtilTest {
RtspRequest request = RtspMessageUtil.parseRequest(requestLines);
assertThat(request.method).isEqualTo(RtspRequest.METHOD_SET_PARAMETER);
assertThat(request.headers.asMap())
assertThat(request.headers.asMultiMap())
.containsExactly(
RtspHeaders.CSEQ,
"3",
RtspHeaders.USER_AGENT,
"LibVLC/3.0.11",
RtspHeaders.CONTENT_LENGTH,
"20",
RtspHeaders.CONTENT_TYPE,
"text/parameters");
RtspHeaders.CSEQ, "3",
RtspHeaders.USER_AGENT, "LibVLC/3.0.11",
RtspHeaders.CONTENT_LENGTH, "20",
RtspHeaders.CONTENT_TYPE, "text/parameters");
assertThat(request.messageBody).isEqualTo("param: stuff");
}
@ -176,14 +192,11 @@ public final class RtspMessageUtilTest {
RtspResponse response = RtspMessageUtil.parseResponse(responseLines);
assertThat(response.status).isEqualTo(200);
assertThat(response.headers.asMap())
assertThat(response.headers.asMultiMap())
.containsExactly(
RtspHeaders.CSEQ,
"431",
RtspHeaders.CONTENT_LENGTH,
"46",
RtspHeaders.CONTENT_TYPE,
"text/parameters");
RtspHeaders.CSEQ, "431",
RtspHeaders.CONTENT_LENGTH, "46",
RtspHeaders.CONTENT_TYPE, "text/parameters");
assertThat(response.messageBody).isEqualTo("packets_received: 10\r\n" + "jitter: 0.3838");
}
@ -206,14 +219,14 @@ public final class RtspMessageUtilTest {
List<String> expectedLines =
Arrays.asList(
"SETUP rtsp://127.0.0.1/test.mkv/track1 RTSP/1.0",
"CSeq: 4",
"Transport: RTP/AVP;unicast;client_port=65458-65459",
"cseq: 4",
"transport: RTP/AVP;unicast;client_port=65458-65459",
"",
"");
String expectedRtspMessage =
"SETUP rtsp://127.0.0.1/test.mkv/track1 RTSP/1.0\r\n"
+ "CSeq: 4\r\n"
+ "Transport: RTP/AVP;unicast;client_port=65458-65459\r\n"
+ "cseq: 4\r\n"
+ "transport: RTP/AVP;unicast;client_port=65458-65459\r\n"
+ "\r\n";
assertThat(messageLines).isEqualTo(expectedLines);
@ -240,14 +253,14 @@ public final class RtspMessageUtilTest {
List<String> expectedLines =
Arrays.asList(
"RTSP/1.0 200 OK",
"CSeq: 4",
"Transport: RTP/AVP;unicast;client_port=65458-65459;server_port=5354-5355",
"cseq: 4",
"transport: RTP/AVP;unicast;client_port=65458-65459;server_port=5354-5355",
"",
"");
String expectedRtspMessage =
"RTSP/1.0 200 OK\r\n"
+ "CSeq: 4\r\n"
+ "Transport: RTP/AVP;unicast;client_port=65458-65459;server_port=5354-5355\r\n"
+ "cseq: 4\r\n"
+ "transport: RTP/AVP;unicast;client_port=65458-65459;server_port=5354-5355\r\n"
+ "\r\n";
assertThat(messageLines).isEqualTo(expectedLines);
assertThat(RtspMessageUtil.convertMessageToByteArray(messageLines))
@ -282,10 +295,10 @@ public final class RtspMessageUtilTest {
List<String> expectedLines =
Arrays.asList(
"RTSP/1.0 200 OK",
"CSeq: 4",
"Content-Base: rtsp://127.0.0.1/test.mkv/",
"Content-Type: application/sdp",
"Content-Length: 707",
"cseq: 4",
"content-base: rtsp://127.0.0.1/test.mkv/",
"content-type: application/sdp",
"content-length: 707",
"",
"v=0\r\n"
+ "o=- 1606776316530225 1 IN IP4 192.168.2.176\r\n"
@ -299,10 +312,10 @@ public final class RtspMessageUtilTest {
String expectedRtspMessage =
"RTSP/1.0 200 OK\r\n"
+ "CSeq: 4\r\n"
+ "Content-Base: rtsp://127.0.0.1/test.mkv/\r\n"
+ "Content-Type: application/sdp\r\n"
+ "Content-Length: 707\r\n"
+ "cseq: 4\r\n"
+ "content-base: rtsp://127.0.0.1/test.mkv/\r\n"
+ "content-type: application/sdp\r\n"
+ "content-length: 707\r\n"
+ "\r\n"
+ "v=0\r\n"
+ "o=- 1606776316530225 1 IN IP4 192.168.2.176\r\n"
@ -328,8 +341,8 @@ public final class RtspMessageUtilTest {
/* messageBody= */ "");
List<String> messageLines = RtspMessageUtil.serializeResponse(response);
List<String> expectedLines = Arrays.asList("RTSP/1.0 454 Session Not Found", "CSeq: 4", "", "");
String expectedRtspMessage = "RTSP/1.0 454 Session Not Found\r\n" + "CSeq: 4\r\n" + "\r\n";
List<String> expectedLines = Arrays.asList("RTSP/1.0 454 Session Not Found", "cseq: 4", "", "");
String expectedRtspMessage = "RTSP/1.0 454 Session Not Found\r\n" + "cseq: 4\r\n" + "\r\n";
assertThat(RtspMessageUtil.serializeResponse(response)).isEqualTo(expectedLines);
assertThat(RtspMessageUtil.convertMessageToByteArray(messageLines))