If present and set to false, media http redirects across domains will not be followed. As long as domains are identical, redirects across protocols or ports will still be followed. Also fail more seriously if redirection fails or is not supported, so that media client does not keep retrying the connection. Bug: 12573548 Change-Id: Ifd2539ad3a90f669d43bd0e82845dbc8ae0b4a3e
360 lines
11 KiB
Java
360 lines
11 KiB
Java
/*
|
|
* Copyright (C) 2013 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 android.media;
|
|
|
|
import android.os.IBinder;
|
|
import android.os.StrictMode;
|
|
import android.util.Log;
|
|
|
|
import java.io.BufferedInputStream;
|
|
import java.io.InputStream;
|
|
import java.io.IOException;
|
|
import java.net.CookieHandler;
|
|
import java.net.CookieManager;
|
|
import java.net.URL;
|
|
import java.net.HttpURLConnection;
|
|
import java.net.MalformedURLException;
|
|
import java.net.NoRouteToHostException;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
|
|
import static android.media.MediaPlayer.MEDIA_ERROR_UNSUPPORTED;
|
|
|
|
/** @hide */
|
|
public class MediaHTTPConnection extends IMediaHTTPConnection.Stub {
|
|
private static final String TAG = "MediaHTTPConnection";
|
|
private static final boolean VERBOSE = false;
|
|
|
|
private long mCurrentOffset = -1;
|
|
private URL mURL = null;
|
|
private Map<String, String> mHeaders = null;
|
|
private HttpURLConnection mConnection = null;
|
|
private long mTotalSize = -1;
|
|
private InputStream mInputStream = null;
|
|
|
|
private boolean mAllowCrossDomainRedirect = true;
|
|
|
|
// from com.squareup.okhttp.internal.http
|
|
private final static int HTTP_TEMP_REDIRECT = 307;
|
|
private final static int MAX_REDIRECTS = 20;
|
|
|
|
public MediaHTTPConnection() {
|
|
if (CookieHandler.getDefault() == null) {
|
|
CookieHandler.setDefault(new CookieManager());
|
|
}
|
|
|
|
native_setup();
|
|
}
|
|
|
|
@Override
|
|
public IBinder connect(String uri, String headers) {
|
|
if (VERBOSE) {
|
|
Log.d(TAG, "connect: uri=" + uri + ", headers=" + headers);
|
|
}
|
|
|
|
try {
|
|
disconnect();
|
|
mAllowCrossDomainRedirect = true;
|
|
mURL = new URL(uri);
|
|
mHeaders = convertHeaderStringToMap(headers);
|
|
} catch (MalformedURLException e) {
|
|
return null;
|
|
}
|
|
|
|
return native_getIMemory();
|
|
}
|
|
|
|
private boolean parseBoolean(String val) {
|
|
try {
|
|
return Long.parseLong(val) != 0;
|
|
} catch (NumberFormatException e) {
|
|
return "true".equalsIgnoreCase(val) ||
|
|
"yes".equalsIgnoreCase(val);
|
|
}
|
|
}
|
|
|
|
/* returns true iff header is internal */
|
|
private boolean filterOutInternalHeaders(String key, String val) {
|
|
if ("android-allow-cross-domain-redirect".equalsIgnoreCase(key)) {
|
|
mAllowCrossDomainRedirect = parseBoolean(val);
|
|
} else {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private Map<String, String> convertHeaderStringToMap(String headers) {
|
|
HashMap<String, String> map = new HashMap<String, String>();
|
|
|
|
String[] pairs = headers.split("\r\n");
|
|
for (String pair : pairs) {
|
|
int colonPos = pair.indexOf(":");
|
|
if (colonPos >= 0) {
|
|
String key = pair.substring(0, colonPos);
|
|
String val = pair.substring(colonPos + 1);
|
|
|
|
if (!filterOutInternalHeaders(key, val)) {
|
|
map.put(key, val);
|
|
}
|
|
}
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
@Override
|
|
public void disconnect() {
|
|
teardownConnection();
|
|
mHeaders = null;
|
|
mURL = null;
|
|
}
|
|
|
|
private void teardownConnection() {
|
|
if (mConnection != null) {
|
|
mInputStream = null;
|
|
|
|
mConnection.disconnect();
|
|
mConnection = null;
|
|
|
|
mCurrentOffset = -1;
|
|
}
|
|
}
|
|
|
|
private void seekTo(long offset) throws IOException {
|
|
teardownConnection();
|
|
|
|
try {
|
|
int response;
|
|
int redirectCount = 0;
|
|
|
|
URL url = mURL;
|
|
while (true) {
|
|
mConnection = (HttpURLConnection)url.openConnection();
|
|
// handle redirects ourselves if we do not allow cross-domain redirect
|
|
mConnection.setInstanceFollowRedirects(mAllowCrossDomainRedirect);
|
|
|
|
if (mHeaders != null) {
|
|
for (Map.Entry<String, String> entry : mHeaders.entrySet()) {
|
|
mConnection.setRequestProperty(
|
|
entry.getKey(), entry.getValue());
|
|
}
|
|
}
|
|
|
|
if (offset > 0) {
|
|
mConnection.setRequestProperty(
|
|
"Range", "bytes=" + offset + "-");
|
|
}
|
|
|
|
response = mConnection.getResponseCode();
|
|
if (response != HttpURLConnection.HTTP_MULT_CHOICE &&
|
|
response != HttpURLConnection.HTTP_MOVED_PERM &&
|
|
response != HttpURLConnection.HTTP_MOVED_TEMP &&
|
|
response != HttpURLConnection.HTTP_SEE_OTHER &&
|
|
response != HTTP_TEMP_REDIRECT) {
|
|
// not a redirect, or redirect handled by HttpURLConnection
|
|
break;
|
|
}
|
|
|
|
if (++redirectCount > MAX_REDIRECTS) {
|
|
throw new NoRouteToHostException("Too many redirects: " + redirectCount);
|
|
}
|
|
|
|
String method = mConnection.getRequestMethod();
|
|
if (response == HTTP_TEMP_REDIRECT &&
|
|
!method.equals("GET") && !method.equals("HEAD")) {
|
|
// "If the 307 status code is received in response to a
|
|
// request other than GET or HEAD, the user agent MUST NOT
|
|
// automatically redirect the request"
|
|
throw new NoRouteToHostException("Invalid redirect");
|
|
}
|
|
String location = mConnection.getHeaderField("Location");
|
|
if (location == null) {
|
|
throw new NoRouteToHostException("Invalid redirect");
|
|
}
|
|
url = new URL(mURL /* TRICKY: don't use url! */, location);
|
|
if (!url.getProtocol().equals("https") &&
|
|
!url.getProtocol().equals("http")) {
|
|
throw new NoRouteToHostException("Unsupported protocol redirect");
|
|
}
|
|
boolean sameHost = mURL.getHost().equals(url.getHost());
|
|
if (!sameHost) {
|
|
throw new NoRouteToHostException("Cross-domain redirects are disallowed");
|
|
}
|
|
|
|
if (response != HTTP_TEMP_REDIRECT) {
|
|
// update effective URL, unless it is a Temporary Redirect
|
|
mURL = url;
|
|
}
|
|
}
|
|
|
|
if (mAllowCrossDomainRedirect) {
|
|
// remember the current, potentially redirected URL if redirects
|
|
// were handled by HttpURLConnection
|
|
mURL = mConnection.getURL();
|
|
}
|
|
|
|
if (response == HttpURLConnection.HTTP_PARTIAL) {
|
|
// Partial content, we cannot just use getContentLength
|
|
// because what we want is not just the length of the range
|
|
// returned but the size of the full content if available.
|
|
|
|
String contentRange =
|
|
mConnection.getHeaderField("Content-Range");
|
|
|
|
mTotalSize = -1;
|
|
if (contentRange != null) {
|
|
// format is "bytes xxx-yyy/zzz
|
|
// where "zzz" is the total number of bytes of the
|
|
// content or '*' if unknown.
|
|
|
|
int lastSlashPos = contentRange.lastIndexOf('/');
|
|
if (lastSlashPos >= 0) {
|
|
String total =
|
|
contentRange.substring(lastSlashPos + 1);
|
|
|
|
try {
|
|
mTotalSize = Long.parseLong(total);
|
|
} catch (NumberFormatException e) {
|
|
}
|
|
}
|
|
}
|
|
} else if (response != HttpURLConnection.HTTP_OK) {
|
|
throw new IOException();
|
|
} else {
|
|
mTotalSize = mConnection.getContentLength();
|
|
}
|
|
|
|
if (offset > 0 && response != HttpURLConnection.HTTP_PARTIAL) {
|
|
// Some servers simply ignore "Range" requests and serve
|
|
// data from the start of the content.
|
|
throw new IOException();
|
|
}
|
|
|
|
mInputStream =
|
|
new BufferedInputStream(mConnection.getInputStream());
|
|
|
|
mCurrentOffset = offset;
|
|
} catch (IOException e) {
|
|
mTotalSize = -1;
|
|
mInputStream = null;
|
|
mConnection = null;
|
|
mCurrentOffset = -1;
|
|
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public int readAt(long offset, int size) {
|
|
return native_readAt(offset, size);
|
|
}
|
|
|
|
private int readAt(long offset, byte[] data, int size) {
|
|
StrictMode.ThreadPolicy policy =
|
|
new StrictMode.ThreadPolicy.Builder().permitAll().build();
|
|
|
|
StrictMode.setThreadPolicy(policy);
|
|
|
|
try {
|
|
if (offset != mCurrentOffset) {
|
|
seekTo(offset);
|
|
}
|
|
|
|
int n = mInputStream.read(data, 0, size);
|
|
|
|
if (n == -1) {
|
|
// InputStream signals EOS using a -1 result, our semantics
|
|
// are to return a 0-length read.
|
|
n = 0;
|
|
}
|
|
|
|
mCurrentOffset += n;
|
|
|
|
if (VERBOSE) {
|
|
Log.d(TAG, "readAt " + offset + " / " + size + " => " + n);
|
|
}
|
|
|
|
return n;
|
|
} catch (NoRouteToHostException e) {
|
|
Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
|
|
return MEDIA_ERROR_UNSUPPORTED;
|
|
} catch (IOException e) {
|
|
if (VERBOSE) {
|
|
Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
|
|
}
|
|
return -1;
|
|
} catch (Exception e) {
|
|
if (VERBOSE) {
|
|
Log.d(TAG, "unknown exception " + e);
|
|
Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
|
|
}
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public long getSize() {
|
|
if (mConnection == null) {
|
|
try {
|
|
seekTo(0);
|
|
} catch (IOException e) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return mTotalSize;
|
|
}
|
|
|
|
@Override
|
|
public String getMIMEType() {
|
|
if (mConnection == null) {
|
|
try {
|
|
seekTo(0);
|
|
} catch (IOException e) {
|
|
return "application/octet-stream";
|
|
}
|
|
}
|
|
|
|
return mConnection.getContentType();
|
|
}
|
|
|
|
@Override
|
|
public String getUri() {
|
|
return mURL.toString();
|
|
}
|
|
|
|
@Override
|
|
protected void finalize() {
|
|
native_finalize();
|
|
}
|
|
|
|
private static native final void native_init();
|
|
private native final void native_setup();
|
|
private native final void native_finalize();
|
|
|
|
private native final IBinder native_getIMemory();
|
|
private native final int native_readAt(long offset, int size);
|
|
|
|
static {
|
|
System.loadLibrary("media_jni");
|
|
native_init();
|
|
}
|
|
|
|
private long mNativeContext;
|
|
|
|
}
|