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