| /* |
| * Copyright 2017 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 static android.media.MediaPlayer2.MEDIA_ERROR_UNSUPPORTED; |
| |
| import android.os.StrictMode; |
| import android.util.Log; |
| |
| import java.io.BufferedInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.CookieHandler; |
| import java.net.HttpURLConnection; |
| import java.net.InetAddress; |
| import java.net.MalformedURLException; |
| import java.net.NoRouteToHostException; |
| import java.net.ProtocolException; |
| import java.net.Proxy; |
| import java.net.URL; |
| import java.net.UnknownHostException; |
| import java.net.UnknownServiceException; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| /** @hide */ |
| public class Media2HTTPConnection { |
| private static final String TAG = "Media2HTTPConnection"; |
| private static final boolean VERBOSE = false; |
| |
| // connection timeout - 30 sec |
| private static final int CONNECT_TIMEOUT_MS = 30 * 1000; |
| |
| 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; |
| private boolean mAllowCrossProtocolRedirect = true; |
| |
| // from com.squareup.okhttp.internal.http |
| private final static int HTTP_TEMP_REDIRECT = 307; |
| private final static int MAX_REDIRECTS = 20; |
| |
| public Media2HTTPConnection() { |
| CookieHandler cookieHandler = CookieHandler.getDefault(); |
| if (cookieHandler == null) { |
| Log.w(TAG, "Media2HTTPConnection: Unexpected. No CookieHandler found."); |
| } |
| } |
| |
| public boolean 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 false; |
| } |
| |
| return true; |
| } |
| |
| 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); |
| // cross-protocol redirects are also controlled by this flag |
| mAllowCrossProtocolRedirect = mAllowCrossDomainRedirect; |
| } 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; |
| } |
| |
| public void disconnect() { |
| teardownConnection(); |
| mHeaders = null; |
| mURL = null; |
| } |
| |
| private void teardownConnection() { |
| if (mConnection != null) { |
| if (mInputStream != null) { |
| try { |
| mInputStream.close(); |
| } catch (IOException e) { |
| } |
| mInputStream = null; |
| } |
| |
| mConnection.disconnect(); |
| mConnection = null; |
| |
| mCurrentOffset = -1; |
| } |
| } |
| |
| private static final boolean isLocalHost(URL url) { |
| if (url == null) { |
| return false; |
| } |
| |
| String host = url.getHost(); |
| |
| if (host == null) { |
| return false; |
| } |
| |
| try { |
| if (host.equalsIgnoreCase("localhost")) { |
| return true; |
| } |
| if (InetAddress.getByName(host).isLoopbackAddress()) { |
| return true; |
| } |
| } catch (IllegalArgumentException | UnknownHostException e) { |
| } |
| return false; |
| } |
| |
| private void seekTo(long offset) throws IOException { |
| teardownConnection(); |
| |
| try { |
| int response; |
| int redirectCount = 0; |
| |
| URL url = mURL; |
| |
| // do not use any proxy for localhost (127.0.0.1) |
| boolean noProxy = isLocalHost(url); |
| |
| while (true) { |
| if (noProxy) { |
| mConnection = (HttpURLConnection)url.openConnection(Proxy.NO_PROXY); |
| } else { |
| mConnection = (HttpURLConnection)url.openConnection(); |
| } |
| mConnection.setConnectTimeout(CONNECT_TIMEOUT_MS); |
| |
| // 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 sameProtocol = mURL.getProtocol().equals(url.getProtocol()); |
| if (!mAllowCrossProtocolRedirect && !sameProtocol) { |
| throw new NoRouteToHostException("Cross-protocol redirects are disallowed"); |
| } |
| boolean sameHost = mURL.getHost().equals(url.getHost()); |
| if (!mAllowCrossDomainRedirect && !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 ProtocolException(); |
| } |
| |
| mInputStream = |
| new BufferedInputStream(mConnection.getInputStream()); |
| |
| mCurrentOffset = offset; |
| } catch (IOException e) { |
| mTotalSize = -1; |
| teardownConnection(); |
| mCurrentOffset = -1; |
| |
| throw e; |
| } |
| } |
| |
| public 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 (ProtocolException e) { |
| Log.w(TAG, "readAt " + offset + " / " + size + " => " + e); |
| return MEDIA_ERROR_UNSUPPORTED; |
| } catch (NoRouteToHostException e) { |
| Log.w(TAG, "readAt " + offset + " / " + size + " => " + e); |
| return MEDIA_ERROR_UNSUPPORTED; |
| } catch (UnknownServiceException 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; |
| } |
| } |
| |
| public long getSize() { |
| if (mConnection == null) { |
| try { |
| seekTo(0); |
| } catch (IOException e) { |
| return -1; |
| } |
| } |
| |
| return mTotalSize; |
| } |
| |
| public String getMIMEType() { |
| if (mConnection == null) { |
| try { |
| seekTo(0); |
| } catch (IOException e) { |
| return "application/octet-stream"; |
| } |
| } |
| |
| return mConnection.getContentType(); |
| } |
| |
| public String getUri() { |
| return mURL.toString(); |
| } |
| } |