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
17package android.media;
18
19import android.net.NetworkUtils;
20import android.os.IBinder;
21import android.os.StrictMode;
22import android.util.Log;
23
24import java.io.BufferedInputStream;
25import java.io.InputStream;
26import java.io.IOException;
27import java.net.CookieHandler;
28import java.net.CookieManager;
29import java.net.Proxy;
30import java.net.URL;
31import java.net.HttpURLConnection;
32import java.net.MalformedURLException;
33import java.net.NoRouteToHostException;
34import java.net.ProtocolException;
35import java.net.UnknownServiceException;
36import java.util.HashMap;
37import java.util.Map;
38
39import static android.media.MediaPlayer.MEDIA_ERROR_UNSUPPORTED;
40
41/** @hide */
42public 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
63    public MediaHTTPConnection() {
64        if (CookieHandler.getDefault() == null) {
65            CookieHandler.setDefault(new CookieManager());
66        }
67
68        native_setup();
69    }
70
71    @Override
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
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 */
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
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
130    public void disconnect() {
131        teardownConnection();
132        mHeaders = null;
133        mURL = null;
134    }
135
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
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
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
309    public int readAt(long offset, int size) {
310        return native_readAt(offset, size);
311    }
312
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
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
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
389    public String getUri() {
390        return mURL.toString();
391    }
392
393    @Override
394    protected void finalize() {
395        native_finalize();
396    }
397
398    private static native final void native_init();
399    private native final void native_setup();
400    private native final void native_finalize();
401
402    private native final IBinder native_getIMemory();
403    private native final int native_readAt(long offset, int size);
404
405    static {
406        System.loadLibrary("media_jni");
407        native_init();
408    }
409
410    private long mNativeContext;
411
412}
413