1 //
2 // ========================================================================
3 // Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4 // ------------------------------------------------------------------------
5 // All rights reserved. This program and the accompanying materials
6 // are made available under the terms of the Eclipse Public License v1.0
7 // and Apache License v2.0 which accompanies this distribution.
8 //
9 // The Eclipse Public License is available at
10 // http://www.eclipse.org/legal/epl-v10.html
11 //
12 // The Apache License v2.0 is available at
13 // http://www.opensource.org/licenses/apache2.0.php
14 //
15 // You may elect to redistribute this code under either of these licenses.
16 // ========================================================================
17 //
18
19 package org.eclipse.jetty.util.resource;
20
21 import java.io.Closeable;
22 import java.io.File;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.OutputStream;
27 import java.net.MalformedURLException;
28 import java.net.URI;
29 import java.net.URL;
30 import java.nio.channels.ReadableByteChannel;
31 import java.text.DateFormat;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Collection;
35 import java.util.Date;
36
37 import org.eclipse.jetty.util.B64Code;
38 import org.eclipse.jetty.util.IO;
39 import org.eclipse.jetty.util.Loader;
40 import org.eclipse.jetty.util.StringUtil;
41 import org.eclipse.jetty.util.URIUtil;
42 import org.eclipse.jetty.util.UrlEncoded;
43 import org.eclipse.jetty.util.log.Log;
44 import org.eclipse.jetty.util.log.Logger;
45
46
47 /* ------------------------------------------------------------ */
48 /**
49 * Abstract resource class.
50 * <p>
51 * This class provides a resource abstraction, where a resource may be
52 * a file, a URL or an entry in a jar file.
53 * </p>
54 */
55 public abstract class Resource implements ResourceFactory, Closeable
56 {
57 private static final Logger LOG = Log.getLogger(Resource.class);
58 public static boolean __defaultUseCaches = true;
59 volatile Object _associate;
60
61 /* ------------------------------------------------------------ */
62 /**
63 * Change the default setting for url connection caches.
64 * Subsequent URLConnections will use this default.
65 * @param useCaches true to enable URL connection caches, false otherwise.
66 */
67 public static void setDefaultUseCaches (boolean useCaches)
68 {
69 __defaultUseCaches=useCaches;
70 }
71
72 /* ------------------------------------------------------------ */
73 public static boolean getDefaultUseCaches ()
74 {
75 return __defaultUseCaches;
76 }
77
78 /* ------------------------------------------------------------ */
79 /** Construct a resource from a uri.
80 * @param uri A URI.
81 * @return A Resource object.
82 * @throws MalformedURLException Problem accessing URI
83 */
84 public static Resource newResource(URI uri)
85 throws MalformedURLException
86 {
87 return newResource(uri.toURL());
88 }
89
90 /* ------------------------------------------------------------ */
91 /** Construct a resource from a url.
92 * @param url A URL.
93 * @return A Resource object.
94 */
95 public static Resource newResource(URL url)
96 {
97 return newResource(url, __defaultUseCaches);
98 }
99
100 /* ------------------------------------------------------------ */
101 /**
102 * Construct a resource from a url.
103 * @param url the url for which to make the resource
104 * @param useCaches true enables URLConnection caching if applicable to the type of resource
105 * @return
106 */
107 static Resource newResource(URL url, boolean useCaches)
108 {
109 if (url==null)
110 return null;
111
112 String url_string=url.toExternalForm();
113 if( url_string.startsWith( "file:"))
114 {
115 try
116 {
117 return new PathResource(url);
118 }
119 catch(Exception e)
120 {
121 LOG.warn(e.toString());
122 LOG.debug(Log.EXCEPTION,e);
123 return new BadResource(url,e.toString());
124 }
125 }
126 else if( url_string.startsWith( "jar:file:"))
127 {
128 return new JarFileResource(url, useCaches);
129 }
130 else if( url_string.startsWith( "jar:"))
131 {
132 return new JarResource(url, useCaches);
133 }
134
135 return new URLResource(url,null,useCaches);
136 }
137
138
139
140 /* ------------------------------------------------------------ */
141 /** Construct a resource from a string.
142 * @param resource A URL or filename.
143 * @throws MalformedURLException Problem accessing URI
144 * @return A Resource object.
145 */
146 public static Resource newResource(String resource)
147 throws MalformedURLException, IOException
148 {
149 return newResource(resource, __defaultUseCaches);
150 }
151
152 /* ------------------------------------------------------------ */
153 /** Construct a resource from a string.
154 * @param resource A URL or filename.
155 * @param useCaches controls URLConnection caching
156 * @return A Resource object.
157 * @throws MalformedURLException Problem accessing URI
158 */
159 public static Resource newResource(String resource, boolean useCaches)
160 throws MalformedURLException, IOException
161 {
162 URL url=null;
163 try
164 {
165 // Try to format as a URL?
166 url = new URL(resource);
167 }
168 catch(MalformedURLException e)
169 {
170 if(!resource.startsWith("ftp:") &&
171 !resource.startsWith("file:") &&
172 !resource.startsWith("jar:"))
173 {
174 try
175 {
176 // It's a file.
177 if (resource.startsWith("./"))
178 resource=resource.substring(2);
179 File file=new File(resource).getCanonicalFile();
180 return new PathResource(file);
181 }
182 catch(IOException e2)
183 {
184 e2.addSuppressed(e);
185 throw e2;
186 }
187 }
188 else
189 {
190 LOG.warn("Bad Resource: "+resource);
191 throw e;
192 }
193 }
194
195 return newResource(url, useCaches);
196 }
197
198 /* ------------------------------------------------------------ */
199 public static Resource newResource(File file)
200 {
201 return new PathResource(file.toPath());
202 }
203
204 /* ------------------------------------------------------------ */
205 /** Construct a system resource from a string.
206 * The resource is tried as classloader resource before being
207 * treated as a normal resource.
208 * @param resource Resource as string representation
209 * @return The new Resource
210 * @throws IOException Problem accessing resource.
211 */
212 public static Resource newSystemResource(String resource)
213 throws IOException
214 {
215 URL url=null;
216 // Try to format as a URL?
217 ClassLoader loader=Thread.currentThread().getContextClassLoader();
218 if (loader!=null)
219 {
220 try
221 {
222 url = loader.getResource(resource);
223 if (url == null && resource.startsWith("/"))
224 url = loader.getResource(resource.substring(1));
225 }
226 catch (IllegalArgumentException e)
227 {
228 LOG.ignore(e);
229 // Catches scenario where a bad Windows path like "C:\dev" is
230 // improperly escaped, which various downstream classloaders
231 // tend to have a problem with
232 url = null;
233 }
234 }
235 if (url==null)
236 {
237 loader=Resource.class.getClassLoader();
238 if (loader!=null)
239 {
240 url=loader.getResource(resource);
241 if (url==null && resource.startsWith("/"))
242 url=loader.getResource(resource.substring(1));
243 }
244 }
245
246 if (url==null)
247 {
248 url=ClassLoader.getSystemResource(resource);
249 if (url==null && resource.startsWith("/"))
250 url=ClassLoader.getSystemResource(resource.substring(1));
251 }
252
253 if (url==null)
254 return null;
255
256 return newResource(url);
257 }
258
259 /* ------------------------------------------------------------ */
260 /** Find a classpath resource.
261 * @param resource the relative name of the resource
262 * @return Resource or null
263 */
264 public static Resource newClassPathResource(String resource)
265 {
266 return newClassPathResource(resource,true,false);
267 }
268
269 /* ------------------------------------------------------------ */
270 /** Find a classpath resource.
271 * The {@link java.lang.Class#getResource(String)} method is used to lookup the resource. If it is not
272 * found, then the {@link Loader#getResource(String)} method is used.
273 * If it is still not found, then {@link ClassLoader#getSystemResource(String)} is used.
274 * Unlike {@link ClassLoader#getSystemResource(String)} this method does not check for normal resources.
275 * @param name The relative name of the resource
276 * @param useCaches True if URL caches are to be used.
277 * @param checkParents True if forced searching of parent Classloaders is performed to work around
278 * loaders with inverted priorities
279 * @return Resource or null
280 */
281 public static Resource newClassPathResource(String name,boolean useCaches,boolean checkParents)
282 {
283 URL url=Resource.class.getResource(name);
284
285 if (url==null)
286 url=Loader.getResource(name);
287 if (url==null)
288 return null;
289 return newResource(url,useCaches);
290 }
291
292 /* ------------------------------------------------------------ */
293 public static boolean isContainedIn (Resource r, Resource containingResource) throws MalformedURLException
294 {
295 return r.isContainedIn(containingResource);
296 }
297
298 /* ------------------------------------------------------------ */
299 @Override
300 protected void finalize()
301 {
302 close();
303 }
304
305 /* ------------------------------------------------------------ */
306 public abstract boolean isContainedIn (Resource r) throws MalformedURLException;
307
308
309 /* ------------------------------------------------------------ */
310 /** Release any temporary resources held by the resource.
311 * @deprecated use {@link #close()}
312 */
313 public final void release()
314 {
315 close();
316 }
317
318 /* ------------------------------------------------------------ */
319 /** Release any temporary resources held by the resource.
320 */
321 @Override
322 public abstract void close();
323
324 /* ------------------------------------------------------------ */
325 /**
326 * @return true if the represented resource exists.
327 */
328 public abstract boolean exists();
329
330
331 /* ------------------------------------------------------------ */
332 /**
333 * @return true if the represented resource is a container/directory.
334 * if the resource is not a file, resources ending with "/" are
335 * considered directories.
336 */
337 public abstract boolean isDirectory();
338
339 /* ------------------------------------------------------------ */
340 /**
341 * Time resource was last modified.
342 *
343 * @return the last modified time as milliseconds since unix epoch
344 */
345 public abstract long lastModified();
346
347
348 /* ------------------------------------------------------------ */
349 /**
350 * Length of the resource.
351 *
352 * @return the length of the resource
353 */
354 public abstract long length();
355
356
357 /* ------------------------------------------------------------ */
358 /**
359 * URL representing the resource.
360 *
361 * @return an URL representing the given resource
362 * @deprecated use {{@link #getURI()}.toURL() instead.
363 */
364 @Deprecated
365 public abstract URL getURL();
366
367 /* ------------------------------------------------------------ */
368 /**
369 * URI representing the resource.
370 *
371 * @return an URI representing the given resource
372 */
373 public URI getURI()
374 {
375 try
376 {
377 return getURL().toURI();
378 }
379 catch(Exception e)
380 {
381 throw new RuntimeException(e);
382 }
383 }
384
385
386 /* ------------------------------------------------------------ */
387 /**
388 * File representing the given resource.
389 *
390 * @return an File representing the given resource or NULL if this
391 * is not possible.
392 * @throws IOException if unable to get the resource due to permissions
393 */
394 public abstract File getFile()
395 throws IOException;
396
397
398 /* ------------------------------------------------------------ */
399 /**
400 * The name of the resource.
401 *
402 * @return the name of the resource
403 */
404 public abstract String getName();
405
406
407 /* ------------------------------------------------------------ */
408 /**
409 * Input stream to the resource
410 *
411 * @return an input stream to the resource
412 * @throws IOException if unable to open the input stream
413 */
414 public abstract InputStream getInputStream()
415 throws IOException;
416
417 /* ------------------------------------------------------------ */
418 /**
419 * Readable ByteChannel for the resource.
420 *
421 * @return an readable bytechannel to the resource or null if one is not available.
422 * @throws IOException if unable to open the readable bytechannel for the resource.
423 */
424 public abstract ReadableByteChannel getReadableByteChannel()
425 throws IOException;
426
427 /* ------------------------------------------------------------ */
428 /**
429 * Deletes the given resource
430 * @return true if resource was found and successfully deleted, false if resource didn't exist or was unable to
431 * be deleted.
432 * @throws SecurityException if unable to delete due to permissions
433 */
434 public abstract boolean delete()
435 throws SecurityException;
436
437 /* ------------------------------------------------------------ */
438 /**
439 * Rename the given resource
440 * @param dest the destination name for the resource
441 * @return true if the resource was renamed, false if the resource didn't exist or was unable to be renamed.
442 * @throws SecurityException if unable to rename due to permissions
443 */
444 public abstract boolean renameTo(Resource dest)
445 throws SecurityException;
446
447 /* ------------------------------------------------------------ */
448 /**
449 * list of resource names contained in the given resource.
450 *
451 * @return a list of resource names contained in the given resource.
452 * Note: The resource names are not URL encoded.
453 */
454 public abstract String[] list();
455
456 /* ------------------------------------------------------------ */
457 /**
458 * Returns the resource contained inside the current resource with the
459 * given name.
460 * @param path The path segment to add, which is not encoded
461 * @return the Resource for the resolved path within this Resource.
462 * @throws IOException if unable to resolve the path
463 * @throws MalformedURLException if the resolution of the path fails because the input path parameter is malformed.
464 */
465 public abstract Resource addPath(String path)
466 throws IOException,MalformedURLException;
467
468 /* ------------------------------------------------------------ */
469 /** Get a resource from within this resource.
470 * <p>
471 * This method is essentially an alias for {@link #addPath(String)}, but without checked exceptions.
472 * This method satisfied the {@link ResourceFactory} interface.
473 * @see org.eclipse.jetty.util.resource.ResourceFactory#getResource(java.lang.String)
474 */
475 @Override
476 public Resource getResource(String path)
477 {
478 try
479 {
480 return addPath(path);
481 }
482 catch(Exception e)
483 {
484 LOG.debug(e);
485 return null;
486 }
487 }
488
489 /* ------------------------------------------------------------ */
490 /**
491 * @param uri the uri to encode
492 * @return null (this is deprecated)
493 * @deprecated use {@link URIUtil} or {@link UrlEncoded} instead
494 */
495 @Deprecated
496 public String encode(String uri)
497 {
498 return null;
499 }
500
501 /* ------------------------------------------------------------ */
502 // FIXME: this appears to not be used
503 @SuppressWarnings("javadoc")
504 public Object getAssociate()
505 {
506 return _associate;
507 }
508
509 /* ------------------------------------------------------------ */
510 // FIXME: this appear to not be used
511 @SuppressWarnings("javadoc")
512 public void setAssociate(Object o)
513 {
514 _associate=o;
515 }
516
517 /* ------------------------------------------------------------ */
518 /**
519 * @return true if this Resource is an alias to another real Resource
520 */
521 public boolean isAlias()
522 {
523 return getAlias()!=null;
524 }
525
526 /* ------------------------------------------------------------ */
527 /**
528 * @return The canonical Alias of this resource or null if none.
529 */
530 public URI getAlias()
531 {
532 return null;
533 }
534
535 /* ------------------------------------------------------------ */
536 /** Get the resource list as a HTML directory listing.
537 * @param base The base URL
538 * @param parent True if the parent directory should be included
539 * @return String of HTML
540 * @throws IOException if unable to get the list of resources as HTML
541 */
542 public String getListHTML(String base,boolean parent)
543 throws IOException
544 {
545 base=URIUtil.canonicalPath(base);
546 if (base==null || !isDirectory())
547 return null;
548
549 String[] ls = list();
550 if (ls==null)
551 return null;
552 Arrays.sort(ls);
553
554 String decodedBase = URIUtil.decodePath(base);
555 String title = "Directory: "+deTag(decodedBase);
556
557 StringBuilder buf=new StringBuilder(4096);
558 buf.append("<HTML><HEAD>");
559 buf.append("<LINK HREF=\"").append("jetty-dir.css").append("\" REL=\"stylesheet\" TYPE=\"text/css\"/><TITLE>");
560 buf.append(title);
561 buf.append("</TITLE></HEAD><BODY>\n<H1>");
562 buf.append(title);
563 buf.append("</H1>\n<TABLE BORDER=0>\n");
564
565 if (parent)
566 {
567 buf.append("<TR><TD><A HREF=\"");
568 buf.append(URIUtil.addPaths(base,"../"));
569 buf.append("\">Parent Directory</A></TD><TD></TD><TD></TD></TR>\n");
570 }
571
572 String encodedBase = hrefEncodeURI(base);
573
574 DateFormat dfmt=DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
575 DateFormat.MEDIUM);
576 for (int i=0 ; i< ls.length ; i++)
577 {
578 Resource item = addPath(ls[i]);
579
580 buf.append("\n<TR><TD><A HREF=\"");
581 String path=URIUtil.addPaths(encodedBase,URIUtil.encodePath(ls[i]));
582
583 buf.append(path);
584
585 if (item.isDirectory() && !path.endsWith("/"))
586 buf.append(URIUtil.SLASH);
587
588 // URIUtil.encodePath(buf,path);
589 buf.append("\">");
590 buf.append(deTag(ls[i]));
591 buf.append(" ");
592 buf.append("</A></TD><TD ALIGN=right>");
593 buf.append(item.length());
594 buf.append(" bytes </TD><TD>");
595 buf.append(dfmt.format(new Date(item.lastModified())));
596 buf.append("</TD></TR>");
597 }
598 buf.append("</TABLE>\n");
599 buf.append("</BODY></HTML>\n");
600
601 return buf.toString();
602 }
603
604 /**
605 * Encode any characters that could break the URI string in an HREF.
606 * Such as <a href="/path/to;<script>Window.alert("XSS"+'%20'+"here");</script>">Link</a>
607 *
608 * The above example would parse incorrectly on various browsers as the "<" or '"' characters
609 * would end the href attribute value string prematurely.
610 *
611 * @param raw the raw text to encode.
612 * @return the defanged text.
613 */
614 private static String hrefEncodeURI(String raw)
615 {
616 StringBuffer buf = null;
617
618 loop:
619 for (int i=0;i<raw.length();i++)
620 {
621 char c=raw.charAt(i);
622 switch(c)
623 {
624 case '\'':
625 case '"':
626 case '<':
627 case '>':
628 buf=new StringBuffer(raw.length()<<1);
629 break loop;
630 }
631 }
632 if (buf==null)
633 return raw;
634
635 for (int i=0;i<raw.length();i++)
636 {
637 char c=raw.charAt(i);
638 switch(c)
639 {
640 case '"':
641 buf.append("%22");
642 continue;
643 case '\'':
644 buf.append("%27");
645 continue;
646 case '<':
647 buf.append("%3C");
648 continue;
649 case '>':
650 buf.append("%3E");
651 continue;
652 default:
653 buf.append(c);
654 continue;
655 }
656 }
657
658 return buf.toString();
659 }
660
661 private static String deTag(String raw)
662 {
663 return StringUtil.sanitizeXmlString(raw);
664 }
665
666 /* ------------------------------------------------------------ */
667 /**
668 * @param out the output stream to write to
669 * @param start First byte to write
670 * @param count Bytes to write or -1 for all of them.
671 * @throws IOException if unable to copy the Resource to the output
672 */
673 public void writeTo(OutputStream out,long start,long count)
674 throws IOException
675 {
676 try (InputStream in = getInputStream())
677 {
678 in.skip(start);
679 if (count<0)
680 IO.copy(in,out);
681 else
682 IO.copy(in,out,count);
683 }
684 }
685
686 /* ------------------------------------------------------------ */
687 /**
688 * Copy the Resource to the new destination file.
689 * <p>
690 * Will not replace existing destination file.
691 *
692 * @param destination the destination file to create
693 * @throws IOException if unable to copy the resource
694 */
695 public void copyTo(File destination)
696 throws IOException
697 {
698 if (destination.exists())
699 throw new IllegalArgumentException(destination + " exists");
700
701 try (OutputStream out = new FileOutputStream(destination))
702 {
703 writeTo(out,0,-1);
704 }
705 }
706
707 /* ------------------------------------------------------------ */
708 /**
709 * Generate a weak ETag reference for this Resource.
710 *
711 * @return the weak ETag reference for this resource.
712 */
713 public String getWeakETag()
714 {
715 return getWeakETag("");
716 }
717
718 public String getWeakETag(String suffix)
719 {
720 try
721 {
722 StringBuilder b = new StringBuilder(32);
723 b.append("W/\"");
724
725 String name=getName();
726 int length=name.length();
727 long lhash=0;
728 for (int i=0; i<length;i++)
729 lhash=31*lhash+name.charAt(i);
730
731 B64Code.encode(lastModified()^lhash,b);
732 B64Code.encode(length()^lhash,b);
733 b.append(suffix);
734 b.append('"');
735 return b.toString();
736 }
737 catch (IOException e)
738 {
739 throw new RuntimeException(e);
740 }
741 }
742
743 /* ------------------------------------------------------------ */
744 public Collection<Resource> getAllResources()
745 {
746 try
747 {
748 ArrayList<Resource> deep=new ArrayList<>();
749 {
750 String[] list=list();
751 if (list!=null)
752 {
753 for (String i:list)
754 {
755 Resource r=addPath(i);
756 if (r.isDirectory())
757 deep.addAll(r.getAllResources());
758 else
759 deep.add(r);
760 }
761 }
762 }
763 return deep;
764 }
765 catch(Exception e)
766 {
767 throw new IllegalStateException(e);
768 }
769 }
770
771 /* ------------------------------------------------------------ */
772 /** Generate a properly encoded URL from a {@link File} instance.
773 * @param file Target file.
774 * @return URL of the target file.
775 * @throws MalformedURLException if unable to convert File to URL
776 */
777 public static URL toURL(File file) throws MalformedURLException
778 {
779 return file.toURI().toURL();
780 }
781 }