1 // Copyright (C) 2001-2003 Jon A. Maxwell (JAM)
2 //
3 // This library is free software; you can redistribute it and/or
4 // modify it under the terms of the GNU Lesser General Public
5 // License as published by the Free Software Foundation; either
6 // version 2.1 of the License, or (at your option) any later version.
7 //
8 // This library is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11 // Lesser General Public License for more details.
12 //
13 // You should have received a copy of the GNU Lesser General Public
14 // License along with this library; if not, write to the Free Software
15 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
16 
17 package net.sourceforge.jnlp.cache;
18 
19 import java.io.BufferedInputStream;
20 import java.io.BufferedOutputStream;
21 import java.io.File;
22 import java.io.FileOutputStream;
23 import java.io.FilePermission;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.OutputStream;
27 import java.net.MalformedURLException;
28 import java.net.URL;
29 import java.net.URLConnection;
30 import java.nio.channels.FileChannel;
31 import java.nio.channels.FileLock;
32 import java.security.Permission;
33 import java.util.ArrayList;
34 import java.util.HashSet;
35 import java.util.List;
36 import java.util.Map.Entry;
37 import java.util.Set;
38 
39 import javax.jnlp.DownloadServiceListener;
40 
41 import net.sourceforge.jnlp.Version;
42 import net.sourceforge.jnlp.config.DeploymentConfiguration;
43 import net.sourceforge.jnlp.config.PathsAndFiles;
44 import net.sourceforge.jnlp.runtime.ApplicationInstance;
45 import net.sourceforge.jnlp.runtime.JNLPRuntime;
46 import static net.sourceforge.jnlp.runtime.Translator.R;
47 
48 import net.sourceforge.jnlp.security.ConnectionFactory;
49 import net.sourceforge.jnlp.util.FileUtils;
50 import net.sourceforge.jnlp.util.PropertiesFile;
51 import net.sourceforge.jnlp.util.logging.OutputController;
52 
53 /**
54  * Provides static methods to interact with the cache, download
55  * indicator, and other utility methods.
56  *
57  * @author <a href="mailto:jmaxwell@users.sourceforge.net">Jon A. Maxwell (JAM)</a> - initial author
58  * @version $Revision: 1.17 $
59  */
60 public class CacheUtil {
61 
62 
63 
64     /**
65      * Caches a resource and returns a URL for it in the cache;
66      * blocks until resource is cached. If the resource location is
67      * not cacheable (points to a local file, etc) then the original
68      * URL is returned.
69      *
70      * @param location location of the resource
71      * @param version the version, or {@code null}
72      * @param policy how to handle update
73      * @return either the location in the cache or the original location
74      */
getCachedResourceURL(URL location, Version version, UpdatePolicy policy)75     public static URL getCachedResourceURL(URL location, Version version, UpdatePolicy policy) {
76         try {
77             File f = getCachedResourceFile(location, version, policy);
78             //url was ponting to nowhere eg 404
79             if (f == null){
80                 //originally  f.toUrl was throwing NPE
81                 return null;
82                 //returning null seems to be better
83             }
84             // TODO: Should be toURI().toURL()
85             return f.toURL();
86         } catch (MalformedURLException ex) {
87             return location;
88         }
89     }
90 
91     /**
92      * This is returning File object of cached resource originally from URL
93      * @param location original location of blob
94      * @param version version of resource
95      * @param policy update policy of resource
96      * @return location in ITW cache on filesystem
97      */
getCachedResourceFile(URL location, Version version, UpdatePolicy policy)98     public static File  getCachedResourceFile(URL location, Version version, UpdatePolicy policy) {
99         ResourceTracker rt = new ResourceTracker();
100         rt.addResource(location, version, null, policy);
101         File f = rt.getCacheFile(location);
102         return f;
103     }
104 
105     /**
106      * Returns the Permission object necessary to access the
107      * resource, or {@code null} if no permission is needed.
108      * @param location location of the resource
109      * @param version the version, or {@code null}
110      * @return permissions of the location
111      */
getReadPermission(URL location, Version version)112     public static Permission getReadPermission(URL location, Version version) {
113         Permission result = null;
114         if (CacheUtil.isCacheable(location, version)) {
115             File file = CacheUtil.getCacheFile(location, version);
116             result = new FilePermission(file.getPath(), "read");
117         } else {
118             try {
119                 // this is what URLClassLoader does
120                 URLConnection conn = ConnectionFactory.getConnectionFactory().openConnection(location);
121                 result = conn.getPermission();
122                  ConnectionFactory.getConnectionFactory().disconnect(conn);
123             } catch (java.io.IOException ioe) {
124                 // should try to figure out the permission
125                 OutputController.getLogger().log(ioe);
126             }
127         }
128 
129         return result;
130     }
131 
132     /**
133      * Clears the cache by deleting all the Netx cache files
134      *
135      * Note: Because of how our caching system works, deleting jars of another javaws
136      * process is using them can be quite disasterous. Hence why Launcher creates lock files
137      * and we check for those by calling {@link #okToClearCache()}
138      * @return true if the cache could be cleared and was cleared
139      */
clearCache()140     public static boolean clearCache() {
141 
142         if (!okToClearCache()) {
143             OutputController.getLogger().log(OutputController.Level.ERROR_ALL, R("CCannotClearCache"));
144             return false;
145         }
146 
147         CacheLRUWrapper lruHandler = CacheLRUWrapper.getInstance();
148         File cacheDir = lruHandler.getCacheDir().getFile();
149         if (!(cacheDir.isDirectory())) {
150             return false;
151         }
152 
153         OutputController.getLogger().log(OutputController.Level.ERROR_DEBUG, "Clearing cache directory: " + cacheDir);
154         lruHandler.lock();
155         try {
156             cacheDir = cacheDir.getCanonicalFile();
157             FileUtils.recursiveDelete(cacheDir, cacheDir);
158             cacheDir.mkdir();
159             lruHandler.clearLRUSortedEntries();
160             lruHandler.store();
161         } catch (IOException e) {
162             throw new RuntimeException(e);
163         } finally {
164             lruHandler.unlock();
165         }
166         return true;
167     }
168 
169     /**
170      * Returns a boolean indicating if it ok to clear the netx application cache at this point
171      * @return true if the cache can be cleared at this time without problems
172      */
okToClearCache()173     private static boolean okToClearCache() {
174         File otherJavawsRunning = PathsAndFiles.MAIN_LOCK.getFile();
175         FileLock locking = null;
176         try {
177             if (otherJavawsRunning.isFile()) {
178                 FileOutputStream fis = new FileOutputStream(otherJavawsRunning);
179 
180                 FileChannel channel = fis.getChannel();
181                 locking  = channel.tryLock();
182                 if (locking == null) {
183                     OutputController.getLogger().log("Other instances of netx are running");
184                     return false;
185                 }
186                 OutputController.getLogger().log("No other instances of netx are running");
187                 return true;
188 
189             } else {
190                 OutputController.getLogger().log("No instance file found");
191                 return true;
192             }
193         } catch (IOException e) {
194             return false;
195         } finally {
196             if (locking != null) {
197                 try {
198                     locking.release();
199                 } catch (IOException ex) {
200                     OutputController.getLogger().log(ex);
201                 }
202             }
203         }
204     }
205 
206     /**
207      * Returns whether there is a version of the URL contents in the
208      * cache and it is up to date.  This method may not return
209      * immediately.
210      *
211      * @param source the source {@link URL}
212      * @param version the versions to check for
213      * @param lastModifed time in milis since epoch of last modfication
214      * @return whether the cache contains the version
215      * @throws IllegalArgumentException if the source is not cacheable
216      */
isCurrent(URL source, Version version, long lastModifed)217     public static boolean isCurrent(URL source, Version version, long lastModifed) {
218 
219         if (!isCacheable(source, version))
220             throw new IllegalArgumentException(R("CNotCacheable", source));
221 
222         try {
223             CacheEntry entry = new CacheEntry(source, version); // could pool this
224             boolean result = entry.isCurrent(lastModifed);
225 
226             OutputController.getLogger().log("isCurrent: " + source + " = " + result);
227 
228             return result;
229         } catch (Exception ex) {
230             OutputController.getLogger().log(ex);
231             return isCached(source, version); // if can't connect return whether already in cache
232         }
233     }
234 
235     /**
236      * Returns true if the cache has a local copy of the contents of
237      * the URL matching the specified version string.
238      *
239      * @param source the source URL
240      * @param version the versions to check for
241      * @return true if the source is in the cache
242      * @throws IllegalArgumentException if the source is not cacheable
243      */
isCached(URL source, Version version)244     public static boolean isCached(URL source, Version version) {
245         if (!isCacheable(source, version))
246             throw new IllegalArgumentException(R("CNotCacheable", source));
247 
248         CacheEntry entry = new CacheEntry(source, version); // could pool this
249         boolean result = entry.isCached();
250 
251         OutputController.getLogger().log("isCached: " + source + " = " + result);
252 
253         return result;
254     }
255 
256     /**
257      * Returns whether the resource can be cached as a local file;
258      * if not, then URLConnection.openStream can be used to obtain
259      * the contents.
260      * @param source the url of resource
261      * @param version version of resource
262      * @return whether this resource can be cached
263      */
isCacheable(URL source, Version version)264     public static boolean isCacheable(URL source, Version version) {
265         if (source == null)
266             return false;
267 
268         if (source.getProtocol().equals("file")){
269             return false;
270         }
271         if (source.getProtocol().equals("jar")){
272             return false;
273         }
274         return true;
275     }
276 
277     /**
278      * Returns the file for the locally cached contents of the
279      * source.  This method returns the file location only and does
280      * not download the resource.  The latest version of the
281      * resource that matches the specified version will be returned.
282      *
283      * @param source the source {@link URL}
284      * @param version the version id of the local file
285      * @return the file location in the cache, or {@code null} if no versions cached
286      * @throws IllegalArgumentException if the source is not cacheable
287      */
getCacheFile(URL source, Version version)288     public static File getCacheFile(URL source, Version version) {
289         // ensure that version is an version id not version string
290 
291         if (!isCacheable(source, version))
292             throw new IllegalArgumentException(R("CNotCacheable", source));
293 
294         File cacheFile = null;
295         CacheLRUWrapper lruHandler = CacheLRUWrapper.getInstance();
296         synchronized (lruHandler) {
297             try {
298                 lruHandler.lock();
299 
300                 // We need to reload the cacheOrder file each time
301                 // since another plugin/javaws instance may have updated it.
302                 lruHandler.load();
303                 cacheFile = getCacheFileIfExist(urlToPath(source, ""));
304                 if (cacheFile == null) { // We did not find a copy of it.
305                     cacheFile = makeNewCacheFile(source, version);
306                 } else
307                     lruHandler.store();
308             } finally {
309                 lruHandler.unlock();
310             }
311         }
312         return cacheFile;
313     }
314 
315     /**
316      * This will return a File pointing to the location of cache item.
317      *
318      * @param urlPath Path of cache item within cache directory.
319      * @return File if we have searched before, {@code null} otherwise.
320      */
getCacheFileIfExist(File urlPath)321     private static File getCacheFileIfExist(File urlPath) {
322         CacheLRUWrapper lruHandler = CacheLRUWrapper.getInstance();
323         synchronized (lruHandler) {
324             File cacheFile = null;
325             List<Entry<String, String>> entries = lruHandler.getLRUSortedEntries();
326             // Start searching from the most recent to least recent.
327             for (Entry<String, String> e : entries) {
328                 final String key = e.getKey();
329                 final String path = e.getValue();
330 
331                 if (pathToURLPath(path).equals(urlPath.getPath())) { // Match found.
332                     cacheFile = new File(path);
333                     lruHandler.updateEntry(key);
334                     break; // Stop searching since we got newest one already.
335                 }
336             }
337             return cacheFile;
338         }
339     }
340 
341     /**
342      * Get the path to file minus the cache directory and indexed folder.
343      */
pathToURLPath(String path)344     private static String pathToURLPath(String path) {
345         int len = CacheLRUWrapper.getInstance().getCacheDir().getFullPath().length();
346         int index = path.indexOf(File.separatorChar, len + 1);
347         return path.substring(index);
348     }
349 
350     /**
351      * Returns the parent directory of the cached resource.
352      * @param filePath The path of the cached resource directory.
353      * @return parent dir of cache
354      */
getCacheParentDirectory(String filePath)355     public static String getCacheParentDirectory(String filePath) {
356         String path = filePath;
357         String tempPath;
358         String cacheDir = CacheLRUWrapper.getInstance().getCacheDir().getFullPath();
359 
360         while(path.startsWith(cacheDir) && !path.equals(cacheDir)){
361                 tempPath = new File(path).getParent();
362 
363                 if (tempPath.equals(cacheDir))
364                     break;
365 
366                 path = tempPath;
367         }
368         return path;
369     }
370 
371     /**
372      * This will create a new entry for the cache item. It is however not
373      * initialized but any future calls to getCacheFile with the source and
374      * version given to here, will cause it to return this item.
375      *
376      * @param source the source URL
377      * @param version the version id of the local file
378      * @return the file location in the cache.
379      */
makeNewCacheFile(URL source, Version version)380     public static File makeNewCacheFile(URL source, Version version) {
381         CacheLRUWrapper lruHandler = CacheLRUWrapper.getInstance();
382         synchronized (lruHandler) {
383             File cacheFile = null;
384             try {
385                 lruHandler.lock();
386                 lruHandler.load();
387                 for (long i = 0; i < Long.MAX_VALUE; i++) {
388                     String path = lruHandler.getCacheDir().getFullPath()+ File.separator + i;
389                     File cDir = new File(path);
390                     if (!cDir.exists()) {
391                         // We can use this directory.
392                         try {
393                             cacheFile = urlToPath(source, path);
394                             FileUtils.createParentDir(cacheFile);
395                             File pf = new File(cacheFile.getPath() + ".info");
396                             FileUtils.createRestrictedFile(pf, true); // Create the info file for marking later.
397                             lruHandler.addEntry(lruHandler.generateKey(cacheFile.getPath()), cacheFile.getPath());
398                         } catch (IOException ioe) {
399                             OutputController.getLogger().log(ioe);
400                         }
401 
402                         break;
403                     }
404                 }
405 
406                 lruHandler.store();
407             } finally {
408                 lruHandler.unlock();
409             }
410             return cacheFile;
411         }
412     }
413 
414     /**
415      * Returns a buffered output stream open for writing to the
416      * cache file.
417      *
418      * @param source the remote location
419      * @param version the file version to write to
420      * @return the stream to write to resource
421      * @throws java.io.IOException if IO breaks
422      */
getOutputStream(URL source, Version version)423     public static OutputStream getOutputStream(URL source, Version version) throws IOException {
424         File localFile = getCacheFile(source, version);
425         OutputStream out = new FileOutputStream(localFile);
426 
427         return new BufferedOutputStream(out);
428     }
429 
430     /**
431      * Copies from an input stream to an output stream.  On
432      * completion, both streams will be closed.  Streams are
433      * buffered automatically.
434      * @param is stream to read from
435      * @param os stream to write to
436      * @throws java.io.IOException if copy fails
437      */
streamCopy(InputStream is, OutputStream os)438     public static void streamCopy(InputStream is, OutputStream os) throws IOException {
439         if (!(is instanceof BufferedInputStream))
440             is = new BufferedInputStream(is);
441 
442         if (!(os instanceof BufferedOutputStream))
443             os = new BufferedOutputStream(os);
444 
445         try {
446             byte b[] = new byte[4096];
447             while (true) {
448                 int c = is.read(b, 0, b.length);
449                 if (c == -1)
450                     break;
451 
452                 os.write(b, 0, c);
453             }
454         } finally {
455             is.close();
456             os.close();
457         }
458     }
459 
460     /**
461      * Converts a URL into a local path string within the given directory. For
462      * example a url with subdirectory /tmp/ will
463      * result in a File that is located somewhere within /tmp/
464      *
465      * @param location the url
466      * @param subdir the subdirectory
467      * @return the file
468      */
urlToPath(URL location, String subdir)469     public static File urlToPath(URL location, String subdir) {
470         if (subdir == null) {
471             throw new NullPointerException();
472         }
473 
474         StringBuilder path = new StringBuilder();
475 
476         path.append(subdir);
477         path.append(File.separatorChar);
478 
479         path.append(location.getProtocol());
480         path.append(File.separatorChar);
481         path.append(location.getHost());
482         path.append(File.separatorChar);
483         path.append(location.getPath().replace('/', File.separatorChar));
484         if (location.getQuery() != null && !location.getQuery().trim().isEmpty()) {
485             path.append(".").append(location.getQuery());
486         }
487 
488         return new File(FileUtils.sanitizePath(path.toString()));
489     }
490 
491     /**
492      * Waits until the resources are downloaded, while showing a
493      * progress indicator.
494      *
495      * @param app application instance with context for this resource
496      * @param tracker the resource tracker
497      * @param resources the resources to wait for
498      * @param title name of the download
499      */
waitForResources(ApplicationInstance app, ResourceTracker tracker, URL resources[], String title)500     public static void waitForResources(ApplicationInstance app, ResourceTracker tracker, URL resources[], String title) {
501         DownloadIndicator indicator = JNLPRuntime.getDefaultDownloadIndicator();
502         DownloadServiceListener listener = null;
503 
504         try {
505             if (indicator == null) {
506                 tracker.waitForResources(resources, 0);
507                 return;
508             }
509 
510             // see if resources can be downloaded very quickly; avoids
511             // overhead of creating display components for the resources
512             if (tracker.waitForResources(resources, indicator.getInitialDelay()))
513                 return;
514 
515             // only resources not starting out downloaded are displayed
516             List<URL> urlList = new ArrayList<>();
517             for (URL url : resources) {
518                 if (!tracker.checkResource(url))
519                     urlList.add(url);
520             }
521             URL undownloaded[] = urlList.toArray(new URL[urlList.size()]);
522 
523             listener = indicator.getListener(app, title, undownloaded);
524 
525             do {
526                 long read = 0;
527                 long total = 0;
528 
529                 for (URL url : undownloaded) {
530                     // add in any -1's; they're insignificant
531                     total += tracker.getTotalSize(url);
532                     read += tracker.getAmountRead(url);
533                 }
534 
535                 int percent = (int) ((100 * read) / Math.max(1, total));
536 
537                 for (URL url : undownloaded) {
538                     listener.progress(url, "version",
539                                       tracker.getAmountRead(url),
540                                       tracker.getTotalSize(url),
541                                       percent);
542                 }
543             } while (!tracker.waitForResources(resources, indicator.getUpdateRate()));
544 
545             // make sure they read 100% until indicator closes
546             for (URL url : undownloaded) {
547                 listener.progress(url, "version",
548                                   tracker.getTotalSize(url),
549                                   tracker.getTotalSize(url),
550                                   100);
551             }
552         } catch (InterruptedException ex) {
553             OutputController.getLogger().log(ex);
554         } finally {
555             if (listener != null)
556                 indicator.disposeListener(listener);
557         }
558     }
559 
560     /**
561      * This will remove all old cache items.
562      */
cleanCache()563     public static void cleanCache() {
564         CacheLRUWrapper lruHandler = CacheLRUWrapper.getInstance();
565         if (okToClearCache()) {
566             // First we want to figure out which stuff we need to delete.
567             HashSet<String> keep = new HashSet<>();
568             HashSet<String> remove = new HashSet<>();
569             try {
570                 lruHandler.lock();
571                 lruHandler.load();
572 
573                 long maxSize = -1; // Default
574                 try {
575                     maxSize = Long.parseLong(JNLPRuntime.getConfiguration().getProperty(DeploymentConfiguration.KEY_CACHE_MAX_SIZE));
576                 } catch (NumberFormatException nfe) {
577                 }
578 
579                 maxSize = maxSize << 20; // Convert from megabyte to byte (Negative values will be considered unlimited.)
580                 long curSize = 0;
581 
582                 for (Entry<String, String> e : lruHandler.getLRUSortedEntries()) {
583                     // Check if the item is contained in cacheOrder.
584                     final String key = e.getKey();
585                     final String path = e.getValue();
586 
587                     File file = new File(path);
588                     PropertiesFile pf = new PropertiesFile(new File(path + ".info"));
589                     boolean delete = Boolean.parseBoolean(pf.getProperty("delete"));
590 
591                 /*
592                  * This will get me the root directory specific to this cache item.
593                  * Example:
594                  *  cacheDir = /home/user1/.icedtea/cache
595                  *  file.getPath() = /home/user1/.icedtea/cache/0/http/www.example.com/subdir/a.jar
596                  *  rStr first becomes: /0/http/www.example.com/subdir/a.jar
597                  *  then rstr becomes: /home/user1/.icedtea/cache/0
598                  */
599                     String rStr = file.getPath().substring(lruHandler.getCacheDir().getFullPath().length());
600                     rStr = lruHandler.getCacheDir().getFullPath()+ rStr.substring(0, rStr.indexOf(File.separatorChar, 1));
601                     long len = file.length();
602 
603                     if (keep.contains(file.getPath().substring(rStr.length()))) {
604                         lruHandler.removeEntry(key);
605                         continue;
606                     }
607 
608                 /*
609                  * we remove entries from our lru if any of the following condition is met.
610                  * Conditions:
611                  *  - delete: file has been marked for deletion.
612                  *  - !file.isFile(): if someone tampered with the directory, file doesn't exist.
613                  *  - maxSize >= 0 && curSize + len > maxSize: If a limit was set and the new size
614                  *  on disk would exceed the maximum size.
615                  */
616                     if (delete || !file.isFile() || (maxSize >= 0 && curSize + len > maxSize)) {
617                         lruHandler.removeEntry(key);
618                         remove.add(rStr);
619                         continue;
620                     }
621 
622                     curSize += len;
623                     keep.add(file.getPath().substring(rStr.length()));
624 
625                     for (File f : file.getParentFile().listFiles()) {
626                         if (!(f.equals(file) || f.equals(pf.getStoreFile()))) {
627                             try {
628                                 FileUtils.recursiveDelete(f, f);
629                             } catch (IOException e1) {
630                                 OutputController.getLogger().log(OutputController.Level.ERROR_ALL, e1);
631                             }
632                         }
633 
634                     }
635                 }
636                 lruHandler.store();
637             } finally {
638                 lruHandler.unlock();
639             }
640             removeSetOfDirectories(remove);
641         }
642     }
643 
removeSetOfDirectories(Set<String> remove)644     private static void removeSetOfDirectories(Set<String> remove) {
645         for (String s : remove) {
646             File f = new File(s);
647             try {
648                 FileUtils.recursiveDelete(f, f);
649             } catch (IOException e) {
650             }
651         }
652     }
653 }
654