From 40ecb6c195981564753e18c6d9250e122f08a12f Mon Sep 17 00:00:00 2001 From: claincly Date: Thu, 6 Jan 2022 14:34:55 +0000 Subject: [PATCH] Prefers DIGEST when RTSP servers sends both BASIC and DIGEST auth info. Issue: google/ExoPlayer#9800 Added test for RTSP authentication. PiperOrigin-RevId: 420048821 --- RELEASENOTES.md | 2 + .../exoplayer2/source/rtsp/RtspClient.java | 19 ++-- .../source/rtsp/RtspClientTest.java | 12 +-- .../source/rtsp/RtspMediaPeriodTest.java | 87 ++++++++++++++++++- .../source/rtsp/RtspPlaybackTest.java | 2 +- .../exoplayer2/source/rtsp/RtspServer.java | 4 +- .../exoplayer2/source/rtsp/RtspTestUtils.java | 9 ++ 7 files changed, 120 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9e02140724..cd2c7b8293 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -87,6 +87,8 @@ * RTSP: * Provide a client API to override the `SocketFactory` used for any server connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)). + * Prefers DIGEST authentication method over BASIC if both are present. + ([#9800](https://github.com/google/ExoPlayer/issues/9800)). * Cast extension * Fix bug that prevented `CastPlayer` from calling `onIsPlayingChanged` correctly. diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspClient.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspClient.java index ee839c4227..5fd4823b95 100644 --- a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspClient.java +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspClient.java @@ -554,14 +554,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; case 401: if (rtspAuthUserInfo != null && !receivedAuthorizationRequest) { // Unauthorized. - @Nullable - String wwwAuthenticateHeader = response.headers.get(RtspHeaders.WWW_AUTHENTICATE); - if (wwwAuthenticateHeader == null) { + ImmutableList wwwAuthenticateHeaders = + response.headers.values(RtspHeaders.WWW_AUTHENTICATE); + if (wwwAuthenticateHeaders.isEmpty()) { throw ParserException.createForMalformedManifest( "Missing WWW-Authenticate header in a 401 response.", /* cause= */ null); } - rtspAuthenticationInfo = - RtspMessageUtil.parseWwwAuthenticateHeader(wwwAuthenticateHeader); + + for (int i = 0; i < wwwAuthenticateHeaders.size(); i++) { + rtspAuthenticationInfo = + RtspMessageUtil.parseWwwAuthenticateHeader(wwwAuthenticateHeaders.get(i)); + if (rtspAuthenticationInfo.authenticationMechanism + == RtspAuthenticationInfo.DIGEST) { + // Prefers DIGEST when RTSP servers sends both BASIC and DIGEST auth info. + break; + } + } + messageSender.retryLastRequest(); receivedAuthorizationRequest = true; return; diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspClientTest.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspClientTest.java index 02d083278c..410d8962b8 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspClientTest.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspClientTest.java @@ -92,7 +92,7 @@ public final class RtspClientTest { } @Override - public RtspResponse getDescribeResponse(Uri requestedUri) { + public RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) { return RtspTestUtils.newDescribeResponseWithSdpMessage( SESSION_DESCRIPTION, rtpPacketStreamDumps, requestedUri); } @@ -167,7 +167,7 @@ public final class RtspClientTest { } @Override - public RtspResponse getDescribeResponse(Uri requestedUri) { + public RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) { return RtspTestUtils.newDescribeResponseWithSdpMessage( SESSION_DESCRIPTION, rtpPacketStreamDumps, requestedUri); } @@ -209,7 +209,7 @@ public final class RtspClientTest { } @Override - public RtspResponse getDescribeResponse(Uri requestedUri) { + public RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) { if (!requestedUri.getPath().contains("redirect")) { return new RtspResponse( 301, @@ -263,7 +263,7 @@ public final class RtspClientTest { } @Override - public RtspResponse getDescribeResponse(Uri requestedUri) { + public RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) { return RtspTestUtils.newDescribeResponseWithSdpMessage( SESSION_DESCRIPTION, rtpPacketStreamDumps, requestedUri); } @@ -310,7 +310,7 @@ public final class RtspClientTest { } @Override - public RtspResponse getDescribeResponse(Uri requestedUri) { + public RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) { clientHasSentDescribeRequest.set(true); return RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED; } @@ -356,7 +356,7 @@ public final class RtspClientTest { } @Override - public RtspResponse getDescribeResponse(Uri requestedUri) { + public RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) { // This session description misses required the o, t and s tags. return RtspTestUtils.newDescribeResponseWithSdpMessage( /* sessionDescription= */ "v=0\r\n", rtpPacketStreamDumps, requestedUri); diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriodTest.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriodTest.java index 8c669ccc36..75b6881c3a 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriodTest.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriodTest.java @@ -62,7 +62,7 @@ public final class RtspMediaPeriodTest { } @Override - public RtspResponse getDescribeResponse(Uri requestedUri) { + public RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) { return RtspTestUtils.newDescribeResponseWithSdpMessage( "v=0\r\n" + "o=- 1606776316530225 1 IN IP4 127.0.0.1\r\n" @@ -106,4 +106,89 @@ public final class RtspMediaPeriodTest { assertThat(refreshedSourceDurationMs.get()).isEqualTo(50_460); } + + @Test + public void prepareMediaPeriod_withWwwAuthentication_refreshesSourceInfoAndCallsOnPrepared() + throws Exception { + RtpPacketStreamDump rtpPacketStreamDump = + RtspTestUtils.readRtpPacketStreamDump("media/rtsp/aac-dump.json"); + + rtspServer = + new RtspServer( + new RtspServer.ResponseProvider() { + @Override + public RtspResponse getOptionsResponse() { + return new RtspResponse( + /* status= */ 200, + new RtspHeaders.Builder().add(RtspHeaders.PUBLIC, "OPTIONS, DESCRIBE").build()); + } + + @Override + public RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) { + String authorizationHeader = headers.get(RtspHeaders.AUTHORIZATION); + if (authorizationHeader == null) { + return new RtspResponse( + /* status= */ 401, + new RtspHeaders.Builder() + .add(RtspHeaders.CSEQ, headers.get(RtspHeaders.CSEQ)) + .add( + RtspHeaders.WWW_AUTHENTICATE, + "Digest realm=\"LIVE555 Streaming Media\"," + + " nonce=\"0cdfe9719e7373b7d5bb2913e2115f3f\"," + + " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"") + .add(RtspHeaders.WWW_AUTHENTICATE, "BASIC realm=\"WallyWorld\"") + .build()); + } + + if (!authorizationHeader.contains("Digest")) { + return new RtspResponse( + 401, + new RtspHeaders.Builder() + .add(RtspHeaders.CSEQ, headers.get(RtspHeaders.CSEQ)) + .build()); + } + + return RtspTestUtils.newDescribeResponseWithSdpMessage( + "v=0\r\n" + + "o=- 1606776316530225 1 IN IP4 127.0.0.1\r\n" + + "s=Exoplayer test\r\n" + + "t=0 0\r\n" + // The session is 50.46s long. + + "a=range:npt=0-50.46\r\n", + ImmutableList.of(rtpPacketStreamDump), + requestedUri); + } + }); + AtomicBoolean prepareCallbackCalled = new AtomicBoolean(); + AtomicLong refreshedSourceDurationMs = new AtomicLong(); + + mediaPeriod = + new RtspMediaPeriod( + new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE), + new TransferRtpDataChannelFactory(DEFAULT_TIMEOUT_MS), + RtspTestUtils.getTestUriWithUserInfo( + "username", "password", rtspServer.startAndGetPortNumber()), + /* listener= */ timing -> refreshedSourceDurationMs.set(timing.getDurationMs()), + /* userAgent= */ "ExoPlayer:RtspPeriodTest", + /* socketFactory= */ SocketFactory.getDefault(), + /* debugLoggingEnabled= */ false); + + mediaPeriod.prepare( + new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + prepareCallbackCalled.set(true); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + source.continueLoading(/* positionUs= */ 0); + } + }, + /* positionUs= */ 0); + RobolectricUtil.runMainLooperUntil(prepareCallbackCalled::get); + mediaPeriod.release(); + + assertThat(refreshedSourceDurationMs.get()).isEqualTo(50_460); + } } diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspPlaybackTest.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspPlaybackTest.java index 1b86d031d3..1cb87f6e86 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspPlaybackTest.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspPlaybackTest.java @@ -209,7 +209,7 @@ public final class RtspPlaybackTest { } @Override - public RtspResponse getDescribeResponse(Uri requestedUri) { + public RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) { return RtspTestUtils.newDescribeResponseWithSdpMessage( SESSION_DESCRIPTION, rtpPacketStreamDumps, requestedUri); } diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspServer.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspServer.java index fe537fc720..c918cf6b55 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspServer.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspServer.java @@ -45,7 +45,7 @@ public final class RtspServer implements Closeable { RtspResponse getOptionsResponse(); /** Returns an RTSP DESCRIBE {@link RtspResponse response}. */ - default RtspResponse getDescribeResponse(Uri requestedUri) { + default RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) { return RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED; } @@ -143,7 +143,7 @@ public final class RtspServer implements Closeable { break; case METHOD_DESCRIBE: - sendResponse(responseProvider.getDescribeResponse(request.uri), cSeq); + sendResponse(responseProvider.getDescribeResponse(request.uri, request.headers), cSeq); break; case METHOD_SETUP: diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspTestUtils.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspTestUtils.java index ce0f0c232d..59ce18a5c6 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspTestUtils.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspTestUtils.java @@ -28,6 +28,7 @@ import java.util.List; /* package */ final class RtspTestUtils { private static final String TEST_BASE_URI = "rtsp://localhost:%d/test"; + private static final String TEST_BASE_URI_WITH_USER_INFO = "rtsp://%s:%s@localhost:%d/test"; private static final String RTP_TIME_FORMAT = "url=rtsp://localhost/test/%s;seq=%d;rtptime=%d"; /** RTSP error Method Not Allowed (RFC2326 Section 7.1.1). */ @@ -70,6 +71,14 @@ import java.util.List; return Uri.parse(Util.formatInvariant(TEST_BASE_URI, serverRtspPortNumber)); } + /** Returns the test RTSP {@link Uri} with user info. */ + public static Uri getTestUriWithUserInfo( + String username, String password, int serverRtspPortNumber) { + return Uri.parse( + Util.formatInvariant( + TEST_BASE_URI_WITH_USER_INFO, username, password, serverRtspPortNumber)); + } + public static String getRtpInfoForDumps(List rtpPacketStreamDumps) { ArrayList rtpInfos = new ArrayList<>(rtpPacketStreamDumps.size()); for (RtpPacketStreamDump rtpPacketStreamDump : rtpPacketStreamDumps) {