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