Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

집요정 도비의 일기

Apply Digest Auth to Exoplayer 본문

개발 일기

Apply Digest Auth to Exoplayer

집요정_도비 2021. 1. 29. 16:50

I was doing a project that consisted of getting streams from AXIS web cams. 

 

I had two functions to implement ; First, play the live stream, Second, play a portion of the live stream's history

 

The AXIS web cam uses digest auth for authenticating it's stream. 

 

The first step was easily done by putting the stream link inside a webview. 

 

http://${userName}:${password}@${ip:port}/axis-cgi/mjpg/video.cgi?camera=1

 

But, the second approach did not work like this. Unlike the first link, the link I obtained for playing it's history was a file link. So, instead of messing around with webview for this, I decided to try out exoplayer. But, as far as I researched, exoplayer did not automatically authenticate digest auths. So, had to find a workaround. 

 

The main idea of this approach is authenticating the connection via a seperate httpUrlConnection and applying the header obtained by it. 

 

I found the below code from someone's gist. 

 

import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;


import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;



public class HttpDigestAuth {

public HttpURLConnection tryAuth(HttpURLConnection connection, String username, String password)
throws IOException
{
int responseCode = connection.getResponseCode();
if(responseCode == HttpURLConnection.HTTP_UNAUTHORIZED){
connection = tryDigestAuthentication(connection, username, password);
if(connection == null){
throw new AuthenticationException();
}
}
return connection;
}

public static HttpURLConnection tryDigestAuthentication(HttpURLConnection input, String username, String password)
{
String auth = input.getHeaderField("WWW-Authenticate");
if(auth == null || !auth.startsWith("Digest ")){
return null;
}
final HashMap<String, String> authFields = splitAuthFields(auth.substring(7));

MessageDigest md5 = null;
try{
md5 = MessageDigest.getInstance("MD5");
}
catch(NoSuchAlgorithmException e){
return null;
}

Joiner colonJoiner = Joiner.on(':');

String HA1 = null;
try{
md5.reset();
String ha1str = colonJoiner.join(username,
authFields.get("realm"), password);
md5.update(ha1str.getBytes("ISO-8859-1"));
byte[] ha1bytes = md5.digest();
HA1 = bytesToHexString(ha1bytes);
}
catch(UnsupportedEncodingException e){
return null;
}

String HA2 = null;
try{
md5.reset();
String ha2str = colonJoiner.join(input.getRequestMethod(),
input.getURL().getPath());
md5.update(ha2str.getBytes("ISO-8859-1"));
HA2 = bytesToHexString(md5.digest());
}
catch(UnsupportedEncodingException e){
return null;
}

String HA3 = null;
try{
md5.reset();
String ha3str = colonJoiner.join(HA1, authFields.get("nonce"), HA2);
md5.update(ha3str.getBytes("ISO-8859-1"));
HA3 = bytesToHexString(md5.digest());
}
catch(UnsupportedEncodingException e){
return null;
}

StringBuilder sb = new StringBuilder(128);
sb.append("Digest ");
sb.append("username").append("=\"").append(username ).append("\",");
sb.append("realm" ).append("=\"").append(authFields.get("realm") ).append("\",");
sb.append("nonce" ).append("=\"").append(authFields.get("nonce") ).append("\",");
sb.append("uri" ).append("=\"").append(input.getURL().getPath()).append("\",");
//sb.append("qop" ).append('=' ).append("auth" ).append(",");
sb.append("response").append("=\"").append(HA3 ).append("\"");

try{
final HttpURLConnection result = (HttpURLConnection)input.getURL().openConnection();
result.addRequestProperty("Authorization", sb.toString());
return result;
}
catch(IOException e){
return null;
}
}

private static HashMap<String, String> splitAuthFields(String authString)
{
final HashMap<String, String> fields = Maps.newHashMap();
final CharMatcher trimmer = CharMatcher.anyOf("\"\t ");
final Splitter commas = Splitter.on(',').trimResults().omitEmptyStrings();
final Splitter equals = Splitter.on('=').trimResults(trimmer).limit(2);
String[] valuePair;
for(String keyPair : commas.split(authString)){
valuePair = Iterables.toArray(equals.split(keyPair), String.class);
fields.put(valuePair[0], valuePair[1]);
}
return fields;
}

private static final String HEX_LOOKUP = "0123456789abcdef";
private static String bytesToHexString(byte[] bytes)
{
StringBuilder sb = new StringBuilder(bytes.length * 2);
for(int i = 0; i < bytes.length; i++){
sb.append(HEX_LOOKUP.charAt((bytes[i] & 0xF0) >> 4));
sb.append(HEX_LOOKUP.charAt((bytes[i] & 0x0F) >> 0));
}
return sb.toString();
}

public static class AuthenticationException extends IOException
{
private static final long serialVersionUID = 1L;
public AuthenticationException()
{
super("Problems authenticating");
}
}
}

 

You can call the above code like this.

 

player = ExoPlayerFactory.newSimpleInstance(this.applicationContext)

exoPlayerView.player = player
exoPlayerView.useController = false

CoroutineScope(Dispatchers.IO).launch {

try {

val url = URL(item.url)
val connection: HttpURLConnection = url.openConnection() as HttpURLConnection

val con = HttpDigestAuth.tryDigestAuthentication(
connection,
item.userName,
item.password
)

val request = con.getRequestProperty("Authorization")

CoroutineScope(Dispatchers.Main).launch {

try {

val uriString = item.url + "&resolution=640x480"

val uri = Uri.parse(uriString)

//ErrorController.showMessage("[BuildMediaSource] $uriString")

val mediaSource: MediaSource =
buildMediaSource(uri, request)
player?.playWhenReady = true
player?.prepare(mediaSource, true, false)

} catch (e: Exception) {
//ErrorController.showError(e)
}
}

} catch (e: Exception) {
//ErrorController.showError(e)
}
}



private fun buildMediaSource(uri: Uri, auth: String): MediaSource {

val userAgent = Util.getUserAgent(getContext(), packageName)

val factory = DefaultHttpDataSourceFactory(userAgent)
val property = factory.defaultRequestProperties
property.set("Authorization", auth)

val source = ProgressiveMediaSource.Factory(factory)
.createMediaSource(uri)

return source
}

 

You need the following dependency to make the code to work.

 

implementation 'com.google.guava:guava:30.1-android'

implementation 'com.google.android.exoplayer:exoplayer-core:2.11.3'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.11.3'

 

Comments