• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static android.media.MediaPlayer2.MEDIA_ERROR_UNSUPPORTED;
20 
21 import android.os.StrictMode;
22 import android.util.Log;
23 
24 import java.io.BufferedInputStream;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.net.CookieHandler;
28 import java.net.HttpURLConnection;
29 import java.net.InetAddress;
30 import java.net.MalformedURLException;
31 import java.net.NoRouteToHostException;
32 import java.net.ProtocolException;
33 import java.net.Proxy;
34 import java.net.URL;
35 import java.net.UnknownHostException;
36 import java.net.UnknownServiceException;
37 import java.util.HashMap;
38 import java.util.Map;
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 
Media2HTTPConnection()62     public Media2HTTPConnection() {
63         CookieHandler cookieHandler = CookieHandler.getDefault();
64         if (cookieHandler == null) {
65             Log.w(TAG, "Media2HTTPConnection: Unexpected. No CookieHandler found.");
66         }
67     }
68 
connect(String uri, String headers)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 
parseBoolean(String val)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 */
filterOutInternalHeaders(String key, String val)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 
convertHeaderStringToMap(String headers)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 
disconnect()126     public void disconnect() {
127         teardownConnection();
128         mHeaders = null;
129         mURL = null;
130     }
131 
teardownConnection()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 
isLocalHost(URL url)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 (InetAddress.getByName(host).isLoopbackAddress()) {
165                 return true;
166             }
167         } catch (IllegalArgumentException | UnknownHostException e) {
168         }
169         return false;
170     }
171 
seekTo(long offset)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 
readAt(long offset, byte[] data, int size)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 
getSize()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 
getMIMEType()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 
getUri()382     public String getUri() {
383         return mURL.toString();
384     }
385 }
386