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