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