diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5e4c31c8f8..00d60e8be7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -68,6 +68,8 @@ * Fix `NullPointerException` in `StyledPlayerView` that could occur after calling `StyledPlayerView.setPlayer(null)` ([#8985](https://github.com/google/ExoPlayer/issues/8985)). +* RTSP: + * Add support for RTSP basic and digest authentication. ### 2.14.0 (2021-05-13) diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspAuthenticationInfo.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspAuthenticationInfo.java new file mode 100644 index 0000000000..82d15e7a7c --- /dev/null +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspAuthenticationInfo.java @@ -0,0 +1,142 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.rtsp; + +import android.net.Uri; +import android.util.Base64; +import androidx.annotation.IntDef; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.source.rtsp.RtspMessageUtil.RtspAuthUserInfo; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** Wraps RTSP authentication information. */ +/* package */ final class RtspAuthenticationInfo { + + /** The supported authentication methods. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({BASIC, DIGEST}) + @interface AuthenticationMechanism {} + + /** HTTP basic authentication (RFC2068 Section 11.1). */ + public static final int BASIC = 1; + /** HTTP digest authentication (RFC2069). */ + public static final int DIGEST = 2; + + private static final String DIGEST_FORMAT = + "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\""; + private static final String DIGEST_FORMAT_WITH_OPAQUE = + "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"," + + " opaque=\"%s\""; + + private static final String ALGORITHM = "MD5"; + + /** The authentication mechanism. */ + @AuthenticationMechanism public final int authenticationMechanism; + /** The authentication realm. */ + public final String realm; + /** The nonce used in digest authentication; empty if using {@link #BASIC} authentication. */ + public final String nonce; + /** The opaque used in digest authentication; empty if using {@link #BASIC} authentication. */ + public final String opaque; + + /** + * Creates a new instance. + * + * @param authenticationMechanism The authentication mechanism, as defined by {@link + * AuthenticationMechanism}. + * @param realm The authentication realm. + * @param nonce The nonce in digest authentication; empty if using {@link #BASIC} authentication. + * @param opaque The opaque in digest authentication; empty if using {@link #BASIC} + * authentication. + */ + public RtspAuthenticationInfo( + @AuthenticationMechanism int authenticationMechanism, + String realm, + String nonce, + String opaque) { + this.authenticationMechanism = authenticationMechanism; + this.realm = realm; + this.nonce = nonce; + this.opaque = opaque; + } + + /** + * Gets the string value for {@link RtspHeaders#AUTHORIZATION} header. + * + * @param authUserInfo The {@link RtspAuthUserInfo} for authentication. + * @param uri The request {@link Uri}. + * @param requestMethod The request method, defined in {@link RtspRequest.Method}. + * @return The string value for {@link RtspHeaders#AUTHORIZATION} header. + * @throws ParserException If the MD5 algorithm is not supported by {@link MessageDigest}. + */ + public String getAuthorizationHeaderValue( + RtspAuthUserInfo authUserInfo, Uri uri, @RtspRequest.Method int requestMethod) + throws ParserException { + switch (authenticationMechanism) { + case BASIC: + return getBasicAuthorizationHeaderValue(authUserInfo); + case DIGEST: + return getDigestAuthorizationHeaderValue(authUserInfo, uri, requestMethod); + default: + throw new ParserException(new UnsupportedOperationException()); + } + } + + private String getBasicAuthorizationHeaderValue(RtspAuthUserInfo authUserInfo) { + return Base64.encodeToString( + RtspMessageUtil.getStringBytes(authUserInfo.username + ":" + authUserInfo.password), + Base64.DEFAULT); + } + + private String getDigestAuthorizationHeaderValue( + RtspAuthUserInfo authUserInfo, Uri uri, @RtspRequest.Method int requestMethod) + throws ParserException { + try { + MessageDigest md = MessageDigest.getInstance(ALGORITHM); + String methodName = RtspMessageUtil.toMethodString(requestMethod); + // From RFC2069 Section 2.1.2: + // response-digest = H( H(A1) ":" unquoted nonce-value ":" H(A2) ) + // A1 = unquoted username-value ":" unquoted realm-value ":" password + // A2 = Method ":" request-uri + // H(x) = MD5(x) + + String hashA1 = + Util.toHexString( + md.digest( + RtspMessageUtil.getStringBytes( + authUserInfo.username + ":" + realm + ":" + authUserInfo.password))); + String hashA2 = + Util.toHexString(md.digest(RtspMessageUtil.getStringBytes(methodName + ":" + uri))); + String response = + Util.toHexString( + md.digest(RtspMessageUtil.getStringBytes(hashA1 + ":" + nonce + ":" + hashA2))); + + if (opaque.isEmpty()) { + return Util.formatInvariant( + DIGEST_FORMAT, authUserInfo.username, realm, nonce, uri, response); + } else { + return Util.formatInvariant( + DIGEST_FORMAT_WITH_OPAQUE, authUserInfo.username, realm, nonce, uri, response, opaque); + } + } catch (NoSuchAlgorithmException e) { + throw new ParserException(e); + } + } +} 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 ff39f792c2..e57f1703b1 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 @@ -32,6 +32,7 @@ import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_UNSET import static com.google.android.exoplayer2.util.Assertions.checkArgument; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkState; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; import static com.google.common.base.Strings.nullToEmpty; import android.net.Uri; @@ -44,6 +45,7 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.source.rtsp.RtspMediaPeriod.RtpLoadInfo; import com.google.android.exoplayer2.source.rtsp.RtspMediaSource.RtspPlaybackException; import com.google.android.exoplayer2.source.rtsp.RtspMessageChannel.InterleavedBinaryDataListener; +import com.google.android.exoplayer2.source.rtsp.RtspMessageUtil.RtspAuthUserInfo; import com.google.android.exoplayer2.source.rtsp.RtspMessageUtil.RtspSessionHeader; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; @@ -92,6 +94,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final SessionInfoListener sessionInfoListener; private final Uri uri; + @Nullable private final RtspAuthUserInfo rtspAuthUserInfo; @Nullable private final String userAgent; private final ArrayDeque pendingSetupRtpLoadInfos; // TODO(b/172331505) Add a timeout monitor for pending requests. @@ -103,7 +106,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Nullable private String sessionId; @Nullable private KeepAliveMonitor keepAliveMonitor; private boolean hasUpdatedTimelineAndTracks; + private boolean receivedAuthorizationRequest; private long pendingSeekPositionUs; + private @MonotonicNonNull RtspAuthenticationInfo rtspAuthenticationInfo; /** * Creates a new instance. @@ -122,6 +127,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public RtspClient(SessionInfoListener sessionInfoListener, @Nullable String userAgent, Uri uri) { this.sessionInfoListener = sessionInfoListener; this.uri = RtspMessageUtil.removeUserInfo(uri); + this.rtspAuthUserInfo = RtspMessageUtil.parseUserInfo(uri); this.userAgent = userAgent; pendingSetupRtpLoadInfos = new ArrayDeque<>(); pendingRequests = new SparseArray<>(); @@ -212,6 +218,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; messageChannel = new RtspMessageChannel(new MessageListener()); messageChannel.open(getSocket(uri)); sessionId = null; + receivedAuthorizationRequest = false; } catch (IOException e) { checkNotNull(playbackEventListener).onPlaybackError(new RtspPlaybackException(e)); } @@ -239,6 +246,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return SocketFactory.getDefault().createSocket(checkNotNull(uri.getHost()), rtspPort); } + private void dispatchRtspError(Throwable error) { + RtspPlaybackException playbackException = + error instanceof RtspPlaybackException + ? (RtspPlaybackException) error + : new RtspPlaybackException(error); + + if (hasUpdatedTimelineAndTracks) { + // Playback event listener must be non-null after timeline has been updated. + checkNotNull(playbackEventListener).onPlaybackError(playbackException); + } else { + sessionInfoListener.onSessionTimelineRequestFailed(nullToEmpty(error.getMessage()), error); + } + } + /** * Returns whether the RTSP server supports the DESCRIBE method. * @@ -334,6 +355,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; headersBuilder.add(RtspHeaders.SESSION, sessionId); } + if (rtspAuthenticationInfo != null) { + checkStateNotNull(rtspAuthUserInfo); + try { + headersBuilder.add( + RtspHeaders.AUTHORIZATION, + rtspAuthenticationInfo.getAuthorizationHeaderValue(rtspAuthUserInfo, uri, method)); + } catch (ParserException e) { + dispatchRtspError(new RtspPlaybackException(e)); + } + } + headersBuilder.addAll(additionalHeaders); return new RtspRequest(uri, method, headersBuilder.build(), /* messageBody= */ ""); } @@ -379,14 +411,33 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @RtspRequest.Method int requestMethod = matchingRequest.method; - if (response.status != 200) { - dispatchRtspError( - new RtspPlaybackException( - RtspMessageUtil.toMethodString(requestMethod) + " " + response.status)); - return; - } - try { + switch (response.status) { + case 200: + break; + case 401: + if (rtspAuthUserInfo != null && !receivedAuthorizationRequest) { + // Unauthorized. + @Nullable + String wwwAuthenticateHeader = response.headers.get(RtspHeaders.WWW_AUTHENTICATE); + if (wwwAuthenticateHeader == null) { + throw new ParserException("Missing WWW-Authenticate header in a 401 response."); + } + rtspAuthenticationInfo = + RtspMessageUtil.parseWwwAuthenticateHeader(wwwAuthenticateHeader); + messageSender.sendDescribeRequest(uri, sessionId); + receivedAuthorizationRequest = true; + return; + } + // fall through: if unauthorized and no userInfo present, or previous authentication + // unsuccessful. + default: + dispatchRtspError( + new RtspPlaybackException( + RtspMessageUtil.toMethodString(requestMethod) + " " + response.status)); + return; + } + switch (requestMethod) { case METHOD_OPTIONS: onOptionsResponseReceived( @@ -505,20 +556,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; startPlayback(C.usToMs(pendingSeekPositionUs)); } } - - private void dispatchRtspError(Throwable error) { - RtspPlaybackException playbackException = - error instanceof RtspPlaybackException - ? (RtspPlaybackException) error - : new RtspPlaybackException(error); - - if (hasUpdatedTimelineAndTracks) { - // Playback event listener must be non-null after timeline has been updated. - checkNotNull(playbackEventListener).onPlaybackError(playbackException); - } else { - sessionInfoListener.onSessionTimelineRequestFailed(nullToEmpty(error.getMessage()), error); - } - } } /** Sends periodic OPTIONS requests to keep RTSP connection alive. */ diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMessageUtil.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMessageUtil.java index d644514401..b385dc64b4 100644 --- a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMessageUtil.java +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMessageUtil.java @@ -30,6 +30,7 @@ import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_TEARD import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_UNSET; import static com.google.android.exoplayer2.util.Assertions.checkArgument; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.common.base.Strings.nullToEmpty; import static java.util.regex.Pattern.CASE_INSENSITIVE; import android.net.Uri; @@ -63,6 +64,20 @@ import java.util.regex.Pattern; } } + /** Wraps username and password for authentication purposes. */ + public static final class RtspAuthUserInfo { + /** The username. */ + public final String username; + /** The password. */ + public final String password; + + /** Creates a new instance. */ + public RtspAuthUserInfo(String username, String password) { + this.username = username; + this.password = password; + } + } + /** The default timeout, in milliseconds, defined for RTSP (RFC2326 Section 12.37). */ public static final long DEFAULT_RTSP_TIMEOUT_MS = 60_000; @@ -80,6 +95,17 @@ import java.util.regex.Pattern; private static final Pattern SESSION_HEADER_PATTERN = Pattern.compile("(\\w+)(?:;\\s?timeout=(\\d+))?"); + // WWW-Authenticate header pattern, see RFC2068 Sections 14.46 and RFC2069. + private static final Pattern WWW_AUTHENTICATION_HEADER_DIGEST_PATTERN = + Pattern.compile( + "Digest realm=\"([\\w\\s@.]+)\"" + + ",\\s?(?:domain=\"(.+)\",\\s?)?" + + "nonce=\"(\\w+)\"" + + "(?:,\\s?opaque=\"(\\w+)\")?"); + // WWW-Authenticate header pattern, see RFC2068 Section 11.1 and RFC2069. + private static final Pattern WWW_AUTHENTICATION_HEADER_BASIC_PATTERN = + Pattern.compile("Basic realm=\"([\\w\\s@.]+)\""); + private static final String RTSP_VERSION = "RTSP/1.0"; /** @@ -157,6 +183,31 @@ import java.util.regex.Pattern; return uri.buildUpon().encodedAuthority(authority).build(); } + /** + * Parses the user info encapsulated in the RTSP {@link Uri}. + * + * @param uri The {@link Uri}. + * @return The extracted {@link RtspAuthUserInfo}, {@code null} if the argument {@link Uri} does + * not contain userinfo, or it's not properly formatted. + */ + @Nullable + public static RtspAuthUserInfo parseUserInfo(Uri uri) { + @Nullable String userInfo = uri.getUserInfo(); + if (userInfo == null) { + return null; + } + if (userInfo.contains(":")) { + String[] userInfoStrings = Util.splitAtFirst(userInfo, ":"); + return new RtspAuthUserInfo(userInfoStrings[0], userInfoStrings[1]); + } + return null; + } + + /** Returns the byte array representation of a string, using RTSP's character encoding. */ + public static byte[] getStringBytes(String s) { + return s.getBytes(RtspMessageChannel.CHARSET); + } + /** Returns the corresponding String representation of the {@link RtspRequest.Method} argument. */ public static String toMethodString(@RtspRequest.Method int method) { switch (method) { @@ -345,6 +396,39 @@ import java.util.regex.Pattern; return new RtspSessionHeader(sessionId, timeoutMs); } + /** + * Parses a WWW-Authenticate header. + * + *

Reference RFC2068 Section 14.46 for WWW-Authenticate header. Only digest and basic + * authentication mechanisms are supported. + * + * @param headerValue The string representation of the content, without the header name + * (WWW-Authenticate: ). + * @return The parsed {@link RtspAuthenticationInfo}. + * @throws ParserException When the input header value does not follow the WWW-Authenticate header + * format, or is not using either Basic or Digest mechanisms. + */ + public static RtspAuthenticationInfo parseWwwAuthenticateHeader(String headerValue) + throws ParserException { + Matcher matcher = WWW_AUTHENTICATION_HEADER_DIGEST_PATTERN.matcher(headerValue); + if (matcher.find()) { + return new RtspAuthenticationInfo( + RtspAuthenticationInfo.DIGEST, + /* realm= */ checkNotNull(matcher.group(1)), + /* nonce= */ checkNotNull(matcher.group(3)), + /* opaque= */ nullToEmpty(matcher.group(4))); + } + matcher = WWW_AUTHENTICATION_HEADER_BASIC_PATTERN.matcher(headerValue); + if (matcher.matches()) { + return new RtspAuthenticationInfo( + RtspAuthenticationInfo.BASIC, + /* realm= */ checkNotNull(matcher.group(1)), + /* nonce= */ "", + /* opaque= */ ""); + } + throw new ParserException("Invalid WWW-Authenticate header " + headerValue); + } + private static String getRtspStatusReasonPhrase(int statusCode) { switch (statusCode) { case 200: diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspAuthenticationInfoTest.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspAuthenticationInfoTest.java new file mode 100644 index 0000000000..c191ad23c9 --- /dev/null +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspAuthenticationInfoTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.rtsp; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.source.rtsp.RtspMessageUtil.RtspAuthUserInfo; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link RtspAuthenticationInfo}. */ +@RunWith(AndroidJUnit4.class) +public class RtspAuthenticationInfoTest { + + @Test + public void getAuthorizationHeaderValue_withBasicAuthenticationMechanism_getsCorrectHeaderValue() + throws Exception { + String authenticationRealm = "WallyWorld"; + String username = "Aladdin"; + String password = "open sesame"; + String expectedAuthorizationHeaderValue = "QWxhZGRpbjpvcGVuIHNlc2FtZQ==\n"; + RtspAuthenticationInfo authenticator = + new RtspAuthenticationInfo( + RtspAuthenticationInfo.BASIC, authenticationRealm, /* nonce= */ "", /* opaque= */ ""); + + assertThat( + authenticator.getAuthorizationHeaderValue( + new RtspAuthUserInfo(username, password), Uri.EMPTY, RtspRequest.METHOD_DESCRIBE)) + .isEqualTo(expectedAuthorizationHeaderValue); + } + + @Test + public void getAuthorizationHeaderValue_withDigestAuthenticationMechanism_getsCorrectHeaderValue() + throws Exception { + RtspAuthenticationInfo authenticator = + new RtspAuthenticationInfo( + RtspAuthenticationInfo.DIGEST, + /* realm= */ "LIVE555 Streaming Media", + /* nonce= */ "0cdfe9719e7373b7d5bb2913e2115f3f", + /* opaque= */ "5ccc069c403ebaf9f0171e9517f40e41"); + + assertThat( + authenticator.getAuthorizationHeaderValue( + new RtspAuthUserInfo("username", "password"), + Uri.parse("rtsp://localhost:554/imax_cd_2k_264_6ch.mkv"), + RtspRequest.METHOD_DESCRIBE)) + .isEqualTo( + "Digest username=\"username\", realm=\"LIVE555 Streaming Media\"," + + " nonce=\"0cdfe9719e7373b7d5bb2913e2115f3f\"," + + " uri=\"rtsp://localhost:554/imax_cd_2k_264_6ch.mkv\"," + + " response=\"ba9433847439387776f7fb905db3fcae\"," + + " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""); + } +} diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMessageUtilTest.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMessageUtilTest.java index c454afdea2..4025a971ee 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMessageUtilTest.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMessageUtilTest.java @@ -19,7 +19,9 @@ package com.google.android.exoplayer2.source.rtsp; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.source.rtsp.RtspMessageUtil.RtspAuthUserInfo; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ListMultimap; import java.util.Arrays; @@ -399,4 +401,83 @@ public final class RtspMessageUtilTest { assertThat(RtspMessageUtil.isRtspStartLine("Transport: RTP/AVP;unicast;client_port=1000-1001")) .isFalse(); } + + @Test + public void extractUserInfo_withoutPassword_returnsNull() { + @Nullable + RtspAuthUserInfo authUserInfo = + RtspMessageUtil.parseUserInfo(Uri.parse("rtsp://username@mediaserver.com/stream1")); + + assertThat(authUserInfo).isNull(); + } + + @Test + public void extractUserInfo_withoutUserInfo_returnsNull() { + @Nullable + RtspAuthUserInfo authUserInfo = + RtspMessageUtil.parseUserInfo(Uri.parse("rtsp://mediaserver.com/stream1")); + assertThat(authUserInfo).isNull(); + } + + @Test + public void extractUserInfo_withProperlyFormattedUri_succeeds() { + @Nullable + RtspAuthUserInfo authUserInfo = + RtspMessageUtil.parseUserInfo( + Uri.parse("rtsp://username:pass:word@mediaserver.com/stream1")); + + assertThat(authUserInfo).isNotNull(); + assertThat(authUserInfo.username).isEqualTo("username"); + assertThat(authUserInfo.password).isEqualTo("pass:word"); + } + + @Test + public void parseWWWAuthenticateHeader_withBasicAuthentication_succeeds() throws Exception { + RtspAuthenticationInfo authenticationInfo = + RtspMessageUtil.parseWwwAuthenticateHeader("Basic realm=\"WallyWorld\""); + assertThat(authenticationInfo.authenticationMechanism).isEqualTo(RtspAuthenticationInfo.BASIC); + assertThat(authenticationInfo.nonce).isEmpty(); + assertThat(authenticationInfo.realm).isEqualTo("WallyWorld"); + } + + @Test + public void parseWWWAuthenticateHeader_withDigestAuthenticationWithDomain_succeeds() + throws Exception { + RtspAuthenticationInfo authenticationInfo = + RtspMessageUtil.parseWwwAuthenticateHeader( + "Digest realm=\"testrealm@host.com\", domain=\"host.com\"," + + " nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + + " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""); + + assertThat(authenticationInfo.authenticationMechanism).isEqualTo(RtspAuthenticationInfo.DIGEST); + assertThat(authenticationInfo.nonce).isEqualTo("dcd98b7102dd2f0e8b11d0f600bfb0c093"); + assertThat(authenticationInfo.realm).isEqualTo("testrealm@host.com"); + assertThat(authenticationInfo.opaque).isEmpty(); + } + + @Test + public void parseWWWAuthenticateHeader_withDigestAuthenticationWithOptionalParameters_succeeds() + throws Exception { + RtspAuthenticationInfo authenticationInfo = + RtspMessageUtil.parseWwwAuthenticateHeader( + "Digest realm=\"testrealm@host.com\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\"," + + " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\", stale=\"stalev\"," + + " algorithm=\"md5\""); + + assertThat(authenticationInfo.authenticationMechanism).isEqualTo(RtspAuthenticationInfo.DIGEST); + assertThat(authenticationInfo.nonce).isEqualTo("dcd98b7102dd2f0e8b11d0f600bfb0c093"); + assertThat(authenticationInfo.realm).isEqualTo("testrealm@host.com"); + assertThat(authenticationInfo.opaque).isEqualTo("5ccc069c403ebaf9f0171e9517f40e41"); + } + + @Test + public void parseWWWAuthenticateHeader_withDigestAuthentication_succeeds() throws Exception { + RtspAuthenticationInfo authenticationInfo = + RtspMessageUtil.parseWwwAuthenticateHeader( + "Digest realm=\"LIVE555 Streaming Media\", nonce=\"0cdfe9719e7373b7d5bb2913e2115f3f\""); + assertThat(authenticationInfo.authenticationMechanism).isEqualTo(RtspAuthenticationInfo.DIGEST); + assertThat(authenticationInfo.nonce).isEqualTo("0cdfe9719e7373b7d5bb2913e2115f3f"); + assertThat(authenticationInfo.realm).isEqualTo("LIVE555 Streaming Media"); + assertThat(authenticationInfo.opaque).isEmpty(); + } }