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