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