Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.media;
     18 
     19 import android.net.NetworkUtils;
     20 import android.os.StrictMode;
     21 import android.util.Log;
     22 
     23 import java.io.BufferedInputStream;
     24 import java.io.InputStream;
     25 import java.io.IOException;
     26 import java.net.CookieHandler;
     27 import java.net.CookieManager;
     28 import java.net.Proxy;
     29 import java.net.URL;
     30 import java.net.HttpURLConnection;
     31 import java.net.MalformedURLException;
     32 import java.net.NoRouteToHostException;
     33 import java.net.ProtocolException;
     34 import java.net.UnknownServiceException;
     35 import java.util.HashMap;
     36 import java.util.Map;
     37 
     38 import static android.media.MediaPlayer2.MEDIA_ERROR_UNSUPPORTED;
     39 
     40 /** @hide */
     41 public class Media2HTTPConnection {
     42     private static final String TAG = "Media2HTTPConnection";
     43     private static final boolean VERBOSE = false;
     44 
     45     // connection timeout - 30 sec
     46     private static final int CONNECT_TIMEOUT_MS = 30 * 1000;
     47 
     48     private long mCurrentOffset = -1;
     49     private URL mURL = null;
     50     private Map<String, String> mHeaders = null;
     51     private HttpURLConnection mConnection = null;
     52     private long mTotalSize = -1;
     53     private InputStream mInputStream = null;
     54 
     55     private boolean mAllowCrossDomainRedirect = true;
     56     private boolean mAllowCrossProtocolRedirect = true;
     57 
     58     // from com.squareup.okhttp.internal.http
     59     private final static int HTTP_TEMP_REDIRECT = 307;
     60     private final static int MAX_REDIRECTS = 20;
     61 
     62     public Media2HTTPConnection() {
     63         CookieHandler cookieHandler = CookieHandler.getDefault();
     64         if (cookieHandler == null) {
     65             Log.w(TAG, "Media2HTTPConnection: Unexpected. No CookieHandler found.");
     66         }
     67     }
     68 
     69     public boolean connect(String uri, String headers) {
     70         if (VERBOSE) {
     71             Log.d(TAG, "connect: uri=" + uri + ", headers=" + headers);
     72         }
     73 
     74         try {
     75             disconnect();
     76             mAllowCrossDomainRedirect = true;
     77             mURL = new URL(uri);
     78             mHeaders = convertHeaderStringToMap(headers);
     79         } catch (MalformedURLException e) {
     80             return false;
     81         }
     82 
     83         return true;
     84     }
     85 
     86     private boolean parseBoolean(String val) {
     87         try {
     88             return Long.parseLong(val) != 0;
     89         } catch (NumberFormatException e) {
     90             return "true".equalsIgnoreCase(val) ||
     91                 "yes".equalsIgnoreCase(val);
     92         }
     93     }
     94 
     95     /* returns true iff header is internal */
     96     private boolean filterOutInternalHeaders(String key, String val) {
     97         if ("android-allow-cross-domain-redirect".equalsIgnoreCase(key)) {
     98             mAllowCrossDomainRedirect = parseBoolean(val);
     99             // cross-protocol redirects are also controlled by this flag
    100             mAllowCrossProtocolRedirect = mAllowCrossDomainRedirect;
    101         } else {
    102             return false;
    103         }
    104         return true;
    105     }
    106 
    107     private Map<String, String> convertHeaderStringToMap(String headers) {
    108         HashMap<String, String> map = new HashMap<String, String>();
    109 
    110         String[] pairs = headers.split("\r\n");
    111         for (String pair : pairs) {
    112             int colonPos = pair.indexOf(":");
    113             if (colonPos >= 0) {
    114                 String key = pair.substring(0, colonPos);
    115                 String val = pair.substring(colonPos + 1);
    116 
    117                 if (!filterOutInternalHeaders(key, val)) {
    118                     map.put(key, val);
    119                 }
    120             }
    121         }
    122 
    123         return map;
    124     }
    125 
    126     public void disconnect() {
    127         teardownConnection();
    128         mHeaders = null;
    129         mURL = null;
    130     }
    131 
    132     private void teardownConnection() {
    133         if (mConnection != null) {
    134             if (mInputStream != null) {
    135                 try {
    136                     mInputStream.close();
    137                 } catch (IOException e) {
    138                 }
    139                 mInputStream = null;
    140             }
    141 
    142             mConnection.disconnect();
    143             mConnection = null;
    144 
    145             mCurrentOffset = -1;
    146         }
    147     }
    148 
    149     private static final boolean isLocalHost(URL url) {
    150         if (url == null) {
    151             return false;
    152         }
    153 
    154         String host = url.getHost();
    155 
    156         if (host == null) {
    157             return false;
    158         }
    159 
    160         try {
    161             if (host.equalsIgnoreCase("localhost")) {
    162                 return true;
    163             }
    164             if (NetworkUtils.numericToInetAddress(host).isLoopbackAddress()) {
    165                 return true;
    166             }
    167         } catch (IllegalArgumentException iex) {
    168         }
    169         return false;
    170     }
    171 
    172     private void seekTo(long offset) throws IOException {
    173         teardownConnection();
    174 
    175         try {
    176             int response;
    177             int redirectCount = 0;
    178 
    179             URL url = mURL;
    180 
    181             // do not use any proxy for localhost (127.0.0.1)
    182             boolean noProxy = isLocalHost(url);
    183 
    184             while (true) {
    185                 if (noProxy) {
    186                     mConnection = (HttpURLConnection)url.openConnection(Proxy.NO_PROXY);
    187                 } else {
    188                     mConnection = (HttpURLConnection)url.openConnection();
    189                 }
    190                 mConnection.setConnectTimeout(CONNECT_TIMEOUT_MS);
    191 
    192                 // handle redirects ourselves if we do not allow cross-domain redirect
    193                 mConnection.setInstanceFollowRedirects(mAllowCrossDomainRedirect);
    194 
    195                 if (mHeaders != null) {
    196                     for (Map.Entry<String, String> entry : mHeaders.entrySet()) {
    197                         mConnection.setRequestProperty(
    198                                 entry.getKey(), entry.getValue());
    199                     }
    200                 }
    201 
    202                 if (offset > 0) {
    203                     mConnection.setRequestProperty(
    204                             "Range", "bytes=" + offset + "-");
    205                 }
    206 
    207                 response = mConnection.getResponseCode();
    208                 if (response != HttpURLConnection.HTTP_MULT_CHOICE &&
    209                         response != HttpURLConnection.HTTP_MOVED_PERM &&
    210                         response != HttpURLConnection.HTTP_MOVED_TEMP &&
    211                         response != HttpURLConnection.HTTP_SEE_OTHER &&
    212                         response != HTTP_TEMP_REDIRECT) {
    213                     // not a redirect, or redirect handled by HttpURLConnection
    214                     break;
    215                 }
    216 
    217                 if (++redirectCount > MAX_REDIRECTS) {
    218                     throw new NoRouteToHostException("Too many redirects: " + redirectCount);
    219                 }
    220 
    221                 String method = mConnection.getRequestMethod();
    222                 if (response == HTTP_TEMP_REDIRECT &&
    223                         !method.equals("GET") && !method.equals("HEAD")) {
    224                     // "If the 307 status code is received in response to a
    225                     // request other than GET or HEAD, the user agent MUST NOT
    226                     // automatically redirect the request"
    227                     throw new NoRouteToHostException("Invalid redirect");
    228                 }
    229                 String location = mConnection.getHeaderField("Location");
    230                 if (location == null) {
    231                     throw new NoRouteToHostException("Invalid redirect");
    232                 }
    233                 url = new URL(mURL /* TRICKY: don't use url! */, location);
    234                 if (!url.getProtocol().equals("https") &&
    235                         !url.getProtocol().equals("http")) {
    236                     throw new NoRouteToHostException("Unsupported protocol redirect");
    237                 }
    238                 boolean sameProtocol = mURL.getProtocol().equals(url.getProtocol());
    239                 if (!mAllowCrossProtocolRedirect && !sameProtocol) {
    240                     throw new NoRouteToHostException("Cross-protocol redirects are disallowed");
    241                 }
    242                 boolean sameHost = mURL.getHost().equals(url.getHost());
    243                 if (!mAllowCrossDomainRedirect && !sameHost) {
    244                     throw new NoRouteToHostException("Cross-domain redirects are disallowed");
    245                 }
    246 
    247                 if (response != HTTP_TEMP_REDIRECT) {
    248                     // update effective URL, unless it is a Temporary Redirect
    249                     mURL = url;
    250                 }
    251             }
    252 
    253             if (mAllowCrossDomainRedirect) {
    254                 // remember the current, potentially redirected URL if redirects
    255                 // were handled by HttpURLConnection
    256                 mURL = mConnection.getURL();
    257             }
    258 
    259             if (response == HttpURLConnection.HTTP_PARTIAL) {
    260                 // Partial content, we cannot just use getContentLength
    261                 // because what we want is not just the length of the range
    262                 // returned but the size of the full content if available.
    263 
    264                 String contentRange =
    265                     mConnection.getHeaderField("Content-Range");
    266 
    267                 mTotalSize = -1;
    268                 if (contentRange != null) {
    269                     // format is "bytes xxx-yyy/zzz
    270                     // where "zzz" is the total number of bytes of the
    271                     // content or '*' if unknown.
    272 
    273                     int lastSlashPos = contentRange.lastIndexOf('/');
    274                     if (lastSlashPos >= 0) {
    275                         String total =
    276                             contentRange.substring(lastSlashPos + 1);
    277 
    278                         try {
    279                             mTotalSize = Long.parseLong(total);
    280                         } catch (NumberFormatException e) {
    281                         }
    282                     }
    283                 }
    284             } else if (response != HttpURLConnection.HTTP_OK) {
    285                 throw new IOException();
    286             } else {
    287                 mTotalSize = mConnection.getContentLength();
    288             }
    289 
    290             if (offset > 0 && response != HttpURLConnection.HTTP_PARTIAL) {
    291                 // Some servers simply ignore "Range" requests and serve
    292                 // data from the start of the content.
    293                 throw new ProtocolException();
    294             }
    295 
    296             mInputStream =
    297                 new BufferedInputStream(mConnection.getInputStream());
    298 
    299             mCurrentOffset = offset;
    300         } catch (IOException e) {
    301             mTotalSize = -1;
    302             teardownConnection();
    303             mCurrentOffset = -1;
    304 
    305             throw e;
    306         }
    307     }
    308 
    309     public int readAt(long offset, byte[] data, int size) {
    310         StrictMode.ThreadPolicy policy =
    311             new StrictMode.ThreadPolicy.Builder().permitAll().build();
    312 
    313         StrictMode.setThreadPolicy(policy);
    314 
    315         try {
    316             if (offset != mCurrentOffset) {
    317                 seekTo(offset);
    318             }
    319 
    320             int n = mInputStream.read(data, 0, size);
    321 
    322             if (n == -1) {
    323                 // InputStream signals EOS using a -1 result, our semantics
    324                 // are to return a 0-length read.
    325                 n = 0;
    326             }
    327 
    328             mCurrentOffset += n;
    329 
    330             if (VERBOSE) {
    331                 Log.d(TAG, "readAt " + offset + " / " + size + " => " + n);
    332             }
    333 
    334             return n;
    335         } catch (ProtocolException e) {
    336             Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
    337             return MEDIA_ERROR_UNSUPPORTED;
    338         } catch (NoRouteToHostException e) {
    339             Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
    340             return MEDIA_ERROR_UNSUPPORTED;
    341         } catch (UnknownServiceException e) {
    342             Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
    343             return MEDIA_ERROR_UNSUPPORTED;
    344         } catch (IOException e) {
    345             if (VERBOSE) {
    346                 Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
    347             }
    348             return -1;
    349         } catch (Exception e) {
    350             if (VERBOSE) {
    351                 Log.d(TAG, "unknown exception " + e);
    352                 Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
    353             }
    354             return -1;
    355         }
    356     }
    357 
    358     public long getSize() {
    359         if (mConnection == null) {
    360             try {
    361                 seekTo(0);
    362             } catch (IOException e) {
    363                 return -1;
    364             }
    365         }
    366 
    367         return mTotalSize;
    368     }
    369 
    370     public String getMIMEType() {
    371         if (mConnection == null) {
    372             try {
    373                 seekTo(0);
    374             } catch (IOException e) {
    375                 return "application/octet-stream";
    376             }
    377         }
    378 
    379         return mConnection.getContentType();
    380     }
    381 
    382     public String getUri() {
    383         return mURL.toString();
    384     }
    385 }
    386