1 /*
   2  * Copyright (c) 2010, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package com.sun.media.jfxmedia.locator;
  26 
  27 import com.sun.media.jfxmedia.MediaException;
  28 import com.sun.media.jfxmedia.MediaManager;
  29 import com.sun.media.jfxmedia.logging.Logger;
  30 import com.sun.media.jfxmediaimpl.HostUtils;
  31 import com.sun.media.jfxmediaimpl.MediaUtils;
  32 import java.io.FileNotFoundException;
  33 import java.io.IOException;
  34 import java.io.InputStream;
  35 import java.lang.reflect.InvocationTargetException;
  36 import java.lang.reflect.Method;
  37 import java.net.HttpURLConnection;
  38 import java.net.JarURLConnection;
  39 import java.net.MalformedURLException;
  40 import java.net.URI;
  41 import java.net.URISyntaxException;
  42 import java.net.URL;
  43 import java.net.URLConnection;
  44 import java.nio.ByteBuffer;
  45 import java.security.AccessController;
  46 import java.security.PrivilegedAction;
  47 import java.util.Map;
  48 import java.util.TreeMap;
  49 import java.util.concurrent.CountDownLatch;
  50 
  51 /**
  52  * A
  53  * <code>Locator</code> which refers to a
  54  * <code>URI</code>.
  55  */
  56 public class Locator {
  57 
  58     /**
  59      * The content type used if no more specific one may be derived.
  60      */
  61     public static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
  62     /**
  63      * The number of times to attempt to open a URL connection to test the URI.
  64      */
  65     private static final int MAX_CONNECTION_ATTEMPTS = 5;
  66     /**
  67      * The number of milliseconds between attempts to open a URL connection.
  68      */
  69     private static final long CONNECTION_RETRY_INTERVAL = 1000L;
  70     /**
  71      * Timeout in milliseconds to wait for connection (5 min).
  72      */
  73     private static final int CONNECTION_TIMEOUT = 300000;
  74     /**
  75      * The content type of the media content.
  76      */
  77     protected String contentType = DEFAULT_CONTENT_TYPE;
  78     /**
  79      * A hint for the internal player.
  80      */
  81     protected long contentLength = -1;    //Used as a hint for the native layer
  82     /**
  83      * The URI source.
  84      */
  85     protected URI uri;
  86     /**
  87      * Properties to be associated with the connection made to the URI. The
  88      * significance of the properties depends on the URI protocol and type of
  89      * media source.
  90      */
  91     private Map<String, Object> connectionProperties;
  92     /**
  93      * Mutex for connectionProperties;
  94      */
  95     private final Object propertyLock = new Object();
  96 
  97     /*
  98      * These variables will be initialized by constructor and used by init()
  99      */
 100     private String uriString = null;
 101     private String scheme = null;
 102     private String protocol = null;
 103 
 104     /*
 105      * if cached, we store a hard reference to keep it alive
 106      */
 107     private LocatorCache.CacheReference cacheEntry = null;
 108 
 109     /*
 110      * True if init(), getContentLength() and getContentType() can block; false
 111      * otherwise.
 112      */
 113     private boolean canBlock = false;
 114 
 115     /*
 116      * Used to block getContentLength() and getContentType().
 117      */
 118     private CountDownLatch readySignal = new CountDownLatch(1);
 119 
 120     /**
 121      * iOS only: determines if the given URL points to the iPod library
 122      */
 123     private boolean isIpod;
 124 
 125     /**
 126      * Holds connection and response code returned from getConnection()
 127      */
 128     private static class LocatorConnection {
 129 
 130         public HttpURLConnection connection = null;
 131         public int responseCode = HttpURLConnection.HTTP_OK;
 132     }
 133 
 134     private LocatorConnection getConnection(URI uri, String requestMethod)
 135             throws MalformedURLException, IOException {
 136 
 137         // Check ability to connect.
 138         LocatorConnection locatorConnection = new LocatorConnection();
 139         HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
 140         connection.setRequestMethod(requestMethod);
 141         // Set timeouts, otherwise we can wait forever.
 142         connection.setConnectTimeout(CONNECTION_TIMEOUT);
 143         connection.setReadTimeout(CONNECTION_TIMEOUT);
 144 
 145         // Set request headers.
 146         synchronized (propertyLock) {
 147             if (connectionProperties != null) {
 148                 for (String key : connectionProperties.keySet()) {
 149                     Object value = connectionProperties.get(key);
 150                     if (value instanceof String) {
 151                         connection.setRequestProperty(key, (String) value);
 152                     }
 153                 }
 154             }
 155         }
 156 
 157         // Store response code so we can get more information about
 158         // returning connection.
 159         locatorConnection.responseCode = connection.getResponseCode();
 160         if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
 161             locatorConnection.connection = connection;
 162         } else {
 163             closeConnection(connection);
 164             locatorConnection.connection = null;
 165         }
 166         return locatorConnection;
 167     }
 168 
 169     private static long getContentLengthLong(URLConnection connection) {
 170         Method method = AccessController.doPrivileged((PrivilegedAction<Method>) () -> {
 171             try {
 172                 return URLConnection.class.getMethod("getContentLengthLong");
 173             } catch (NoSuchMethodException ex) {
 174                 return null;
 175             }
 176         });
 177 
 178         try {
 179             if (method != null) {
 180                 return (long) method.invoke(connection);
 181             } else {
 182                 return connection.getContentLength();
 183             }
 184         } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
 185             return -1;
 186         }
 187     }
 188 
 189     /**
 190      * Constructs an object representing a media source.
 191      *
 192      * @param uri The URI source.
 193      * @throws NullPointerException if
 194      * <code>uri</code> is
 195      * <code>null</code>.
 196      * @throws IllegalArgumentException if the URI's scheme is
 197      * <code>null</code>.
 198      * @throws URISyntaxException if the supplied URI requires some further
 199      * manipulation in order to be used and this procedure fails to produce a
 200      * usable URI.
 201      * @throws IllegalArgumentException if the URI is a Jar URL as described in
 202      * {@link JarURLConnection https://docs.oracle.com/javase/8/docs/api/java/net/JarURLConnection.html},
 203      * and the scheme of the URL after removing the leading four characters is
 204      * <code>null</code>.
 205      * @throws UnsupportedOperationException if the URI's protocol is
 206      * unsupported.
 207      */
 208     public Locator(URI uri) throws URISyntaxException {
 209         // Check for NULL parameter.
 210         if (uri == null) {
 211             throw new NullPointerException("uri == null!");
 212         }
 213 
 214         // Get the scheme part.
 215         uriString = uri.toASCIIString();
 216         scheme = uri.getScheme();
 217         if (scheme == null) {
 218             throw new IllegalArgumentException("uri.getScheme() == null! uri == '" + uri + "'");
 219         }
 220         scheme = scheme.toLowerCase();
 221 
 222         // Get the protocol.
 223         if (scheme.equals("jar")) {
 224             URI subURI = new URI(uriString.substring(4));
 225             protocol = subURI.getScheme();
 226             if (protocol == null) {
 227                 throw new IllegalArgumentException("uri.getScheme() == null! subURI == '" + subURI + "'");
 228             }
 229             protocol = protocol.toLowerCase();
 230         } else {
 231             protocol = scheme; // scheme is already lower case.
 232         }
 233 
 234         if (HostUtils.isIOS() && protocol.equals("ipod-library")) {
 235             isIpod = true;
 236         }
 237 
 238         // Verify the protocol is supported.
 239         if (!isIpod && !MediaManager.canPlayProtocol(protocol)) {
 240             throw new UnsupportedOperationException("Unsupported protocol \"" + protocol + "\"");
 241         }
 242 
 243         // Check if we can block
 244         if (protocol.equals("http") || protocol.equals("https")) {
 245             canBlock = true;
 246         }
 247 
 248         // Set instance variable.
 249         this.uri = uri;
 250     }
 251 
 252     private InputStream getInputStream(URI uri)
 253             throws MalformedURLException, IOException {
 254         URL url = uri.toURL();
 255         URLConnection connection = url.openConnection();
 256 
 257         // Set request headers.
 258         synchronized (propertyLock) {
 259             if (connectionProperties != null) {
 260                 for (String key : connectionProperties.keySet()) {
 261                     Object value = connectionProperties.get(key);
 262                     if (value instanceof String) {
 263                         connection.setRequestProperty(key, (String) value);
 264                     }
 265                 }
 266             }
 267         }
 268 
 269         InputStream inputStream = url.openStream();
 270         contentLength = getContentLengthLong(connection);
 271         return inputStream;
 272     }
 273 
 274     /**
 275      * Tell this Locator to preload the media into memory, if it hasn't been
 276      * already.
 277      */
 278     public void cacheMedia() {
 279         LocatorCache.CacheReference ref = LocatorCache.locatorCache().fetchURICache(uri);
 280         if (null == ref) {
 281             ByteBuffer cacheBuffer;
 282 
 283             // not cached, load it
 284             InputStream is;
 285             try {
 286                 is = getInputStream(uri);
 287             } catch (Throwable t) {
 288                 return; // just bail
 289             }
 290 
 291             // contentLength is set now, so we can go ahead and allocate
 292             cacheBuffer = ByteBuffer.allocateDirect((int) contentLength);
 293             byte[] readBuf = new byte[8192];
 294             int total = 0;
 295             int count;
 296             while (total < contentLength) {
 297                 try {
 298                     count = is.read(readBuf);
 299                 } catch (IOException ioe) {
 300                     try {
 301                         is.close();
 302                     } catch (Throwable t) {
 303                     }
 304                     if (Logger.canLog(Logger.DEBUG)) {
 305                         Logger.logMsg(Logger.DEBUG, "IOException trying to preload media: " + ioe);
 306                     }
 307                     return;
 308                 }
 309 
 310                 if (count == -1) {
 311                     break; // EOS
 312                 }
 313 
 314                 cacheBuffer.put(readBuf, 0, count);
 315             }
 316 
 317             try {
 318                 is.close();
 319             } catch (Throwable t) {
 320             }
 321 
 322             cacheEntry = LocatorCache.locatorCache().registerURICache(uri, cacheBuffer, contentType);
 323             canBlock = false;
 324         }
 325     }
 326 
 327     /*
 328      * True if init() can block; false otherwise.
 329      */
 330     public boolean canBlock() {
 331         return canBlock;
 332     }
 333 
 334     /*
 335      * Initialize locator. Use canBlock() to determine if init() can block.
 336      *
 337      * @throws URISyntaxException if the supplied URI requires some further
 338      * manipulation in order to be used and this procedure fails to produce a
 339      * usable URI. @throws IOExceptions if a stream cannot be opened over a
 340      * connection of the corresponding URL. @throws MediaException if the
 341      * content type of the media is not supported. @throws FileNotFoundException
 342      * if the media is not available.
 343      */
 344     public void init() throws URISyntaxException, IOException, FileNotFoundException {
 345         try {
 346             // Ensure the correct number of '/'s follows the ':'.
 347             int firstSlash = uriString.indexOf("/");
 348             if (firstSlash != -1 && uriString.charAt(firstSlash + 1) != '/') {
 349                 // Only one '/' after the ':'.
 350                 if (protocol.equals("file")) {
 351                     // Map file:/somepath to file:///somepath
 352                     uriString = uriString.replaceFirst("/", "///");
 353                 } else if (protocol.equals("http") || protocol.equals("https")) {
 354                     // Map http:/somepath to http://somepath
 355                     uriString = uriString.replaceFirst("/", "//");
 356                 }
 357             }
 358 
 359             // On non-Windows systems, replace "/~/" with home directory path + "/".
 360             if (System.getProperty("os.name").toLowerCase().indexOf("win") == -1
 361                     && protocol.equals("file")) {
 362                 int index = uriString.indexOf("/~/");
 363                 if (index != -1) {
 364                     uriString = uriString.substring(0, index)
 365                             + System.getProperty("user.home")
 366                             + uriString.substring(index + 2);
 367                 }
 368             }
 369 
 370             // Recreate the URI if needed
 371             uri = new URI(uriString);
 372 
 373             // First check if this URI is cached, if it is then we're done
 374             cacheEntry = LocatorCache.locatorCache().fetchURICache(uri);
 375             if (null != cacheEntry) {
 376                 // Cache hit! Grab contentType and contentLength and be done
 377                 contentType = cacheEntry.getMIMEType();
 378                 contentLength = cacheEntry.getBuffer().capacity();
 379                 if (Logger.canLog(Logger.DEBUG)) {
 380                     Logger.logMsg(Logger.DEBUG, "Locator init cache hit:"
 381                             + "\n    uri " + uri
 382                             + "\n    type " + contentType
 383                             + "\n    length " + contentLength);
 384                 }
 385                 return;
 386             }
 387 
 388             // Try to open a connection on the corresponding URL.
 389             boolean isConnected = false;
 390             boolean isMediaUnAvailable = false;
 391             boolean isMediaSupported = true;
 392             if (!isIpod) {
 393                 for (int numConnectionAttempts = 0; numConnectionAttempts < MAX_CONNECTION_ATTEMPTS; numConnectionAttempts++) {
 394                     try {
 395                         // Verify existence.
 396                         if (scheme.equals("http") || scheme.equals("https")) {
 397                             // Check ability to connect, trying HEAD before GET.
 398                             LocatorConnection locatorConnection = getConnection(uri, "HEAD");
 399                             if (locatorConnection == null || locatorConnection.connection == null) {
 400                                 locatorConnection = getConnection(uri, "GET");
 401                             }
 402 
 403                             if (locatorConnection != null && locatorConnection.connection != null) {
 404                                 isConnected = true;
 405 
 406                                 // Get content type.
 407                                 contentType = locatorConnection.connection.getContentType();
 408                                 contentLength = getContentLengthLong(locatorConnection.connection);
 409 
 410                                 // Disconnect.
 411                                 closeConnection(locatorConnection.connection);
 412                                 locatorConnection.connection = null;
 413                             } else if (locatorConnection != null) {
 414                                 if (locatorConnection.responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
 415                                     isMediaUnAvailable = true;
 416                                 }
 417                             }
 418 
 419                             // FIXME: get cache settings from server, honor them
 420                         } else if (scheme.equals("file") || scheme.equals("jar") || scheme.equals("jrt")) {
 421                             InputStream stream = getInputStream(uri);
 422                             stream.close();
 423                             isConnected = true;
 424                             contentType = MediaUtils.filenameToContentType(uriString); // We need to provide at least something
 425                         }
 426 
 427                         if (isConnected) {
 428                             // Check whether content may be played.
 429                             // For WAV use file signature, since it can detect audio format
 430                             // and we can fail sooner, then doing it at runtime.
 431                             // This is important for AudioClip.
 432                             if (MediaUtils.CONTENT_TYPE_WAV.equals(contentType)) {
 433                                 contentType = getContentTypeFromFileSignature(uri);
 434                                 if (!MediaManager.canPlayContentType(contentType)) {
 435                                     isMediaSupported = false;
 436                                 }
 437                             } else {
 438                                 if (contentType == null || !MediaManager.canPlayContentType(contentType)) {
 439                                     // Try content based on file name.
 440                                     contentType = MediaUtils.filenameToContentType(uriString);
 441 
 442                                     if (Locator.DEFAULT_CONTENT_TYPE.equals(contentType)) {
 443                                         // Try content based on file signature.
 444                                         contentType = getContentTypeFromFileSignature(uri);
 445                                     }
 446 
 447                                     if (!MediaManager.canPlayContentType(contentType)) {
 448                                         isMediaSupported = false;
 449                                     }
 450                                 }
 451                             }
 452 
 453                             // Break as connection has been made and media type checked.
 454                             break;
 455                         }
 456                     } catch (IOException ioe) {
 457                         if (numConnectionAttempts + 1 >= MAX_CONNECTION_ATTEMPTS) {
 458                             throw ioe;
 459                         }
 460                     }
 461 
 462                     try {
 463                         Thread.sleep(CONNECTION_RETRY_INTERVAL);
 464                     } catch (InterruptedException ie) {
 465                         // Ignore it.
 466                     }
 467                 }
 468             }
 469             else {
 470                 // in case of iPod files we can be sure all files are supported
 471                 contentType = MediaUtils.filenameToContentType(uriString);
 472             }
 473 
 474             if (Logger.canLog(Logger.WARNING)) {
 475                 if (contentType.equals(MediaUtils.CONTENT_TYPE_FLV)) {
 476                     Logger.logMsg(Logger.WARNING, "Support for FLV container and VP6 video is removed.");
 477                     throw new MediaException("media type not supported (" + uri.toString() + ")");
 478                 } else if (contentType.equals(MediaUtils.CONTENT_TYPE_JFX)) {
 479                     Logger.logMsg(Logger.WARNING, "Support for FXM container and VP6 video is removed.");
 480                     throw new MediaException("media type not supported (" + uri.toString() + ")");
 481                 }
 482             }
 483 
 484             // Check URI validity.
 485             if (!isIpod && !isConnected) {
 486                 if (isMediaUnAvailable) {
 487                     throw new FileNotFoundException("media is unavailable (" + uri.toString() + ")");
 488                 } else {
 489                     throw new IOException("could not connect to media (" + uri.toString() + ")");
 490                 }
 491             } else if (!isMediaSupported) {
 492                 throw new MediaException("media type not supported (" + uri.toString() + ")");
 493             }
 494         } catch (FileNotFoundException e) {
 495             throw e; // Just re-throw exception
 496         } catch (IOException e) {
 497             throw e; // Just re-throw exception
 498         } catch (MediaException e) {
 499             throw e; // Just re-throw exception
 500         } finally {
 501             readySignal.countDown();
 502         }
 503     }
 504 
 505     /**
 506      * Retrieves the content type describing the media content or
 507      * <code>"application/octet-stream"</code> if no more specific content type
 508      * may be detected.
 509      */
 510     public String getContentType() {
 511         try {
 512             readySignal.await();
 513         } catch (Exception e) {
 514         }
 515         return contentType;
 516     }
 517 
 518     /**
 519      * Retrieves the protocol of the media URL
 520      */
 521     public String getProtocol() {
 522         return protocol;
 523     }
 524 
 525     /**
 526      * Retrieves the media size.
 527      *
 528      * @return size of the media file in bytes. -1 indicates unknown, which may
 529      * happen with network streams.
 530      */
 531     public long getContentLength() {
 532         try {
 533             readySignal.await();
 534         } catch (Exception e) {
 535         }
 536         return contentLength;
 537     }
 538 
 539     /**
 540      * Blocks until locator is ready (connection is established or failed).
 541      */
 542     public void waitForReadySignal() {
 543         try {
 544             readySignal.await();
 545         } catch (Exception e) {
 546         }
 547     }
 548 
 549     /**
 550      * Retrieves the associated
 551      * <code>URI</code>.
 552      *
 553      * @return The URI source.
 554      */
 555     public URI getURI() {
 556         return this.uri;
 557     }
 558 
 559     /**
 560      * Retrieves a string representation of the
 561      * <code>Locator</code>
 562      *
 563      * @return The
 564      * <code>LocatorURI</code> as a
 565      * <code>String</code>.
 566      */
 567     @Override
 568     public String toString() {
 569         if (LocatorCache.locatorCache().isCached(uri)) {
 570             return "{LocatorURI uri: " + uri.toString() + " (media cached)}";
 571         }
 572         return "{LocatorURI uri: " + uri.toString() + "}";
 573     }
 574 
 575     public String getStringLocation() {
 576         return uri.toString();
 577     }
 578 
 579     /**
 580      * Sets a property to be used by the connection to the media specified by
 581      * the URI. The meaning of the property is a function of the URI protocol
 582      * and type of media source. This method should be invoked <i>before</i>
 583      * calling {@link #createConnectionHolder()} or it will have no effect.
 584      *
 585      * @param property The name of the property.
 586      * @param value The value of the property.
 587      */
 588     public void setConnectionProperty(String property, Object value) {
 589         synchronized (propertyLock) {
 590             if (connectionProperties == null) {
 591                 connectionProperties = new TreeMap<String, Object>();
 592             }
 593 
 594             connectionProperties.put(property, value);
 595         }
 596     }
 597 
 598     public ConnectionHolder createConnectionHolder() throws IOException {
 599         // first check if it's cached
 600         if (null != cacheEntry) {
 601             if (Logger.canLog(Logger.DEBUG)) {
 602                 Logger.logMsg(Logger.DEBUG, "Locator.createConnectionHolder: media cached, creating memory connection holder");
 603             }
 604             return ConnectionHolder.createMemoryConnectionHolder(cacheEntry.getBuffer());
 605         }
 606 
 607         // then fall back on other methods
 608         ConnectionHolder holder;
 609         if ("file".equals(scheme)) {
 610             holder = ConnectionHolder.createFileConnectionHolder(uri);
 611         } else if (uri.toString().endsWith(".m3u8") || uri.toString().endsWith(".m3u")) {
 612             holder = ConnectionHolder.createHLSConnectionHolder(uri);
 613         } else {
 614             synchronized (propertyLock) {
 615                 holder = ConnectionHolder.createURIConnectionHolder(uri, connectionProperties);
 616             }
 617         }
 618 
 619         return holder;
 620     }
 621 
 622     private String getContentTypeFromFileSignature(URI uri) throws MalformedURLException, IOException {
 623         InputStream stream = getInputStream(uri);
 624         byte[] signature = new byte[MediaUtils.MAX_FILE_SIGNATURE_LENGTH];
 625         int size = stream.read(signature);
 626         stream.close();
 627 
 628         return MediaUtils.fileSignatureToContentType(signature, size);
 629     }
 630 
 631     static void closeConnection(URLConnection connection) {
 632         if (connection instanceof HttpURLConnection) {
 633             HttpURLConnection httpConnection = (HttpURLConnection)connection;
 634             try {
 635                 if (httpConnection.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST &&
 636                     httpConnection.getInputStream() != null) {
 637                     httpConnection.getInputStream().close();
 638                 }
 639             } catch (IOException ex) {
 640                 try {
 641                     if (httpConnection.getErrorStream() != null) {
 642                         httpConnection.getErrorStream().close();
 643                     }
 644                 } catch (IOException e) {}
 645             }
 646         }
 647     }
 648 }