Support basic and digest authentication.
Authentication sequence in RTSP: - Server replies "Unauthorized" to our DESCRIBE request, and includes the necessary information (i.e. realm, digest nonce, etc) in WWW-Authenticate header - After `RtspClient` receives the response, we - Parse the WWW-Authenticate header, stores the auth info. The info is saved for all further RTSP requests (that all need to carry authorization headers) - send the second DESCRIBE request with the Authorization header. #minor-release PiperOrigin-RevId: 376116302
This commit is contained in:
parent
8f04d36f59
commit
0fb5fa75cf
@ -68,6 +68,8 @@
|
|||||||
* Fix `NullPointerException` in `StyledPlayerView` that could occur after
|
* Fix `NullPointerException` in `StyledPlayerView` that could occur after
|
||||||
calling `StyledPlayerView.setPlayer(null)`
|
calling `StyledPlayerView.setPlayer(null)`
|
||||||
([#8985](https://github.com/google/ExoPlayer/issues/8985)).
|
([#8985](https://github.com/google/ExoPlayer/issues/8985)).
|
||||||
|
* RTSP:
|
||||||
|
* Add support for RTSP basic and digest authentication.
|
||||||
|
|
||||||
### 2.14.0 (2021-05-13)
|
### 2.14.0 (2021-05-13)
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.checkArgument;
|
||||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
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.checkState;
|
||||||
|
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
|
||||||
import static com.google.common.base.Strings.nullToEmpty;
|
import static com.google.common.base.Strings.nullToEmpty;
|
||||||
|
|
||||||
import android.net.Uri;
|
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.RtspMediaPeriod.RtpLoadInfo;
|
||||||
import com.google.android.exoplayer2.source.rtsp.RtspMediaSource.RtspPlaybackException;
|
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.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.source.rtsp.RtspMessageUtil.RtspSessionHeader;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
@ -92,6 +94,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
|
|
||||||
private final SessionInfoListener sessionInfoListener;
|
private final SessionInfoListener sessionInfoListener;
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
|
@Nullable private final RtspAuthUserInfo rtspAuthUserInfo;
|
||||||
@Nullable private final String userAgent;
|
@Nullable private final String userAgent;
|
||||||
private final ArrayDeque<RtpLoadInfo> pendingSetupRtpLoadInfos;
|
private final ArrayDeque<RtpLoadInfo> pendingSetupRtpLoadInfos;
|
||||||
// TODO(b/172331505) Add a timeout monitor for pending requests.
|
// 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 String sessionId;
|
||||||
@Nullable private KeepAliveMonitor keepAliveMonitor;
|
@Nullable private KeepAliveMonitor keepAliveMonitor;
|
||||||
private boolean hasUpdatedTimelineAndTracks;
|
private boolean hasUpdatedTimelineAndTracks;
|
||||||
|
private boolean receivedAuthorizationRequest;
|
||||||
private long pendingSeekPositionUs;
|
private long pendingSeekPositionUs;
|
||||||
|
private @MonotonicNonNull RtspAuthenticationInfo rtspAuthenticationInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance.
|
* Creates a new instance.
|
||||||
@ -122,6 +127,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
public RtspClient(SessionInfoListener sessionInfoListener, @Nullable String userAgent, Uri uri) {
|
public RtspClient(SessionInfoListener sessionInfoListener, @Nullable String userAgent, Uri uri) {
|
||||||
this.sessionInfoListener = sessionInfoListener;
|
this.sessionInfoListener = sessionInfoListener;
|
||||||
this.uri = RtspMessageUtil.removeUserInfo(uri);
|
this.uri = RtspMessageUtil.removeUserInfo(uri);
|
||||||
|
this.rtspAuthUserInfo = RtspMessageUtil.parseUserInfo(uri);
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
pendingSetupRtpLoadInfos = new ArrayDeque<>();
|
pendingSetupRtpLoadInfos = new ArrayDeque<>();
|
||||||
pendingRequests = new SparseArray<>();
|
pendingRequests = new SparseArray<>();
|
||||||
@ -212,6 +218,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
messageChannel = new RtspMessageChannel(new MessageListener());
|
messageChannel = new RtspMessageChannel(new MessageListener());
|
||||||
messageChannel.open(getSocket(uri));
|
messageChannel.open(getSocket(uri));
|
||||||
sessionId = null;
|
sessionId = null;
|
||||||
|
receivedAuthorizationRequest = false;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
checkNotNull(playbackEventListener).onPlaybackError(new RtspPlaybackException(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);
|
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.
|
* 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);
|
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);
|
headersBuilder.addAll(additionalHeaders);
|
||||||
return new RtspRequest(uri, method, headersBuilder.build(), /* messageBody= */ "");
|
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;
|
@RtspRequest.Method int requestMethod = matchingRequest.method;
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
dispatchRtspError(
|
|
||||||
new RtspPlaybackException(
|
|
||||||
RtspMessageUtil.toMethodString(requestMethod) + " " + response.status));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
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) {
|
switch (requestMethod) {
|
||||||
case METHOD_OPTIONS:
|
case METHOD_OPTIONS:
|
||||||
onOptionsResponseReceived(
|
onOptionsResponseReceived(
|
||||||
@ -505,20 +556,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
startPlayback(C.usToMs(pendingSeekPositionUs));
|
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. */
|
/** Sends periodic OPTIONS requests to keep RTSP connection alive. */
|
||||||
|
@ -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.source.rtsp.RtspRequest.METHOD_UNSET;
|
||||||
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
|
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.checkNotNull;
|
||||||
|
import static com.google.common.base.Strings.nullToEmpty;
|
||||||
import static java.util.regex.Pattern.CASE_INSENSITIVE;
|
import static java.util.regex.Pattern.CASE_INSENSITIVE;
|
||||||
|
|
||||||
import android.net.Uri;
|
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). */
|
/** The default timeout, in milliseconds, defined for RTSP (RFC2326 Section 12.37). */
|
||||||
public static final long DEFAULT_RTSP_TIMEOUT_MS = 60_000;
|
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 =
|
private static final Pattern SESSION_HEADER_PATTERN =
|
||||||
Pattern.compile("(\\w+)(?:;\\s?timeout=(\\d+))?");
|
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";
|
private static final String RTSP_VERSION = "RTSP/1.0";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -157,6 +183,31 @@ import java.util.regex.Pattern;
|
|||||||
return uri.buildUpon().encodedAuthority(authority).build();
|
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. */
|
/** Returns the corresponding String representation of the {@link RtspRequest.Method} argument. */
|
||||||
public static String toMethodString(@RtspRequest.Method int method) {
|
public static String toMethodString(@RtspRequest.Method int method) {
|
||||||
switch (method) {
|
switch (method) {
|
||||||
@ -345,6 +396,39 @@ import java.util.regex.Pattern;
|
|||||||
return new RtspSessionHeader(sessionId, timeoutMs);
|
return new RtspSessionHeader(sessionId, timeoutMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a WWW-Authenticate header.
|
||||||
|
*
|
||||||
|
* <p>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) {
|
private static String getRtspStatusReasonPhrase(int statusCode) {
|
||||||
switch (statusCode) {
|
switch (statusCode) {
|
||||||
case 200:
|
case 200:
|
||||||
|
@ -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\"");
|
||||||
|
}
|
||||||
|
}
|
@ -19,7 +19,9 @@ package com.google.android.exoplayer2.source.rtsp;
|
|||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
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.ImmutableMap;
|
||||||
import com.google.common.collect.ListMultimap;
|
import com.google.common.collect.ListMultimap;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -399,4 +401,83 @@ public final class RtspMessageUtilTest {
|
|||||||
assertThat(RtspMessageUtil.isRtspStartLine("Transport: RTP/AVP;unicast;client_port=1000-1001"))
|
assertThat(RtspMessageUtil.isRtspStartLine("Transport: RTP/AVP;unicast;client_port=1000-1001"))
|
||||||
.isFalse();
|
.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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user