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
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
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 // TODO throw the IOException instead
185 e.addSuppressed(e2);
186 throw e;
187 }
188 }
189 else
190 {
191 LOG.warn("Bad Resource: "+resource);
192 throw e;
193 }
194 }
195
196 return newResource(url, useCaches);
197 }
198
199 /* ------------------------------------------------------------ */
200 public static Resource newResource(File file)
201 {
202 return new PathResource(file.toPath());
203 }
204
205 /* ------------------------------------------------------------ */
206 /** Construct a system resource from a string.
207 * The resource is tried as classloader resource before being
208 * treated as a normal resource.
209 * @param resource Resource as string representation
210 * @return The new Resource
211 * @throws IOException Problem accessing resource.
212 */
213 public static Resource newSystemResource(String resource)
214 throws IOException
215 {
216 URL url=null;
217 // Try to format as a URL?
218 ClassLoader loader=Thread.currentThread().getContextClassLoader();
219 if (loader!=null)
220 {
221 try
222 {
223 url = loader.getResource(resource);
224 if (url == null && resource.startsWith("/"))
225 url = loader.getResource(resource.substring(1));
226 }
227 catch (IllegalArgumentException e)
228 {
229 LOG.ignore(e);
230 // Catches scenario where a bad Windows path like "C:\dev" is
231 // improperly escaped, which various downstream classloaders
232 // tend to have a problem with
233 url = null;
234 }
235 }
236 if (url==null)
237 {
238 loader=Resource.class.getClassLoader();
239 if (loader!=null)
240 {
241 url=loader.getResource(resource);
242 if (url==null && resource.startsWith("/"))
243 url=loader.getResource(resource.substring(1));
244 }
245 }
246
247 if (url==null)
248 {
249 url=ClassLoader.getSystemResource(resource);
250 if (url==null && resource.startsWith("/"))
251 url=ClassLoader.getSystemResource(resource.substring(1));
252 }
253
254 if (url==null)
255 return null;
256
257 return newResource(url);
258 }
259
260 /* ------------------------------------------------------------ */
261 /** Find a classpath resource.
262 * @param resource the relative name of the resource
263 * @return Resource or null
264 */
265 public static Resource newClassPathResource(String resource)
266 {
267 return newClassPathResource(resource,true,false);
268 }
269
270 /* ------------------------------------------------------------ */
271 /** Find a classpath resource.
272 * The {@link java.lang.Class#getResource(String)} method is used to lookup the resource. If it is not
273 * found, then the {@link Loader#getResource(Class, String)} method is used.
274 * If it is still not found, then {@link ClassLoader#getSystemResource(String)} is used.
275 * Unlike {@link ClassLoader#getSystemResource(String)} this method does not check for normal resources.
276 * @param name The relative name of the resource
277 * @param useCaches True if URL caches are to be used.
278 * @param checkParents True if forced searching of parent Classloaders is performed to work around
279 * loaders with inverted priorities
280 * @return Resource or null
281 */
282 public static Resource newClassPathResource(String name,boolean useCaches,boolean checkParents)
283 {
284 URL url=Resource.class.getResource(name);
285
286 if (url==null)
287 url=Loader.getResource(Resource.class,name);
288 if (url==null)
289 return null;
290 return newResource(url,useCaches);
291 }
292
293 /* ------------------------------------------------------------ */
294 public static boolean isContainedIn (Resource r, Resource containingResource) throws MalformedURLException
295 {
296 return r.isContainedIn(containingResource);
297 }
298
299 /* ------------------------------------------------------------ */
300 @Override
301 protected void finalize()
302 {
303 close();
304 }
305
306 /* ------------------------------------------------------------ */
307 public abstract boolean isContainedIn (Resource r) throws MalformedURLException;
308
309
310 /* ------------------------------------------------------------ */
311 /** Release any temporary resources held by the resource.
312 * @deprecated use {@link #close()}
313 */
314 public final void release()
315 {
316 close();
317 }
318
319 /* ------------------------------------------------------------ */
320 /** Release any temporary resources held by the resource.
321 */
322 @Override
323 public abstract void close();
324
325 /* ------------------------------------------------------------ */
326 /**
327 * @return true if the represented resource exists.
328 */
329 public abstract boolean exists();
330
331
332 /* ------------------------------------------------------------ */
333 /**
334 * @return true if the represented resource is a container/directory.
335 * if the resource is not a file, resources ending with "/" are
336 * considered directories.
337 */
338 public abstract boolean isDirectory();
339
340 /* ------------------------------------------------------------ */
341 /**
342 * Time resource was last modified.
343 *
344 * @return the last modified time as milliseconds since unix epoch
345 */
346 public abstract long lastModified();
347
348
349 /* ------------------------------------------------------------ */
350 /**
351 * Length of the resource.
352 *
353 * @return the length of the resource
354 */
355 public abstract long length();
356
357
358 /* ------------------------------------------------------------ */
359 /**
360 * URL representing the resource.
361 *
362 * @return an URL representing the given resource
363 * @deprecated use {{@link #getURI()}.toURL() instead.
364 */
365 @Deprecated
366 public abstract URL getURL();
367
368 /* ------------------------------------------------------------ */
369 /**
370 * URI representing the resource.
371 *
372 * @return an URI representing the given resource
373 */
374 public URI getURI()
375 {
376 try
377 {
378 return getURL().toURI();
379 }
380 catch(Exception e)
381 {
382 throw new RuntimeException(e);
383 }
384 }
385
386
387 /* ------------------------------------------------------------ */
388 /**
389 * File representing the given resource.
390 *
391 * @return an File representing the given resource or NULL if this
392 * is not possible.
393 * @throws IOException if unable to get the resource due to permissions
394 */
395 public abstract File getFile()
396 throws IOException;
397
398
399 /* ------------------------------------------------------------ */
400 /**
401 * The name of the resource.
402 *
403 * @return the name of the resource
404 */
405 public abstract String getName();
406
407
408 /* ------------------------------------------------------------ */
409 /**
410 * Input stream to the resource
411 *
412 * @return an input stream to the resource
413 * @throws IOException if unable to open the input stream
414 */
415 public abstract InputStream getInputStream()
416 throws IOException;
417
418 /* ------------------------------------------------------------ */
419 /**
420 * Readable ByteChannel for the resource.
421 *
422 * @return an readable bytechannel to the resource or null if one is not available.
423 * @throws IOException if unable to open the readable bytechannel for the resource.
424 */
425 public abstract ReadableByteChannel getReadableByteChannel()
426 throws IOException;
427
428 /* ------------------------------------------------------------ */
429 /**
430 * Deletes the given resource
431 * @return true if resource was found and successfully deleted, false if resource didn't exist or was unable to
432 * be deleted.
433 * @throws SecurityException if unable to delete due to permissions
434 */
435 public abstract boolean delete()
436 throws SecurityException;
437
438 /* ------------------------------------------------------------ */
439 /**
440 * Rename the given resource
441 * @param dest the destination name for the resource
442 * @return true if the resource was renamed, false if the resource didn't exist or was unable to be renamed.
443 * @throws SecurityException if unable to rename due to permissions
444 */
445 public abstract boolean renameTo(Resource dest)
446 throws SecurityException;
447
448 /* ------------------------------------------------------------ */
449 /**
450 * list of resource names contained in the given resource.
451 *
452 * @return a list of resource names contained in the given resource.
453 * Note: The resource names are not URL encoded.
454 */
455 public abstract String[] list();
456
457 /* ------------------------------------------------------------ */
458 /**
459 * Returns the resource contained inside the current resource with the
460 * given name.
461 * @param path The path segment to add, which is not encoded
462 * @return the Resource for the resolved path within this Resource.
463 * @throws IOException if unable to resolve the path
464 * @throws MalformedURLException if the resolution of the path fails because the input path parameter is malformed.
465 */
466 public abstract Resource addPath(String path)
467 throws IOException,MalformedURLException;
468
469 /* ------------------------------------------------------------ */
470 /** Get a resource from within this resource.
471 * <p>
472 * This method is essentially an alias for {@link #addPath(String)}, but without checked exceptions.
473 * This method satisfied the {@link ResourceFactory} interface.
474 * @see org.eclipse.jetty.util.resource.ResourceFactory#getResource(java.lang.String)
475 */
476 @Override
477 public Resource getResource(String path)
478 {
479 try
480 {
481 return addPath(path);
482 }
483 catch(Exception e)
484 {
485 LOG.debug(e);
486 return null;
487 }
488 }
489
490 /* ------------------------------------------------------------ */
491 /**
492 * @param uri the uri to encode
493 * @return null (this is deprecated)
494 * @deprecated use {@link URIUtil} or {@link UrlEncoded} instead
495 */
496 @Deprecated
497 public String encode(String uri)
498 {
499 return null;
500 }
501
502 /* ------------------------------------------------------------ */
503 // FIXME: this appears to not be used
504 @SuppressWarnings("javadoc")
505 public Object getAssociate()
506 {
507 return _associate;
508 }
509
510 /* ------------------------------------------------------------ */
511 // FIXME: this appear to not be used
512 @SuppressWarnings("javadoc")
513 public void setAssociate(Object o)
514 {
515 _associate=o;
516 }
517
518 /* ------------------------------------------------------------ */
519 /**
520 * @return true if this Resource is an alias to another real Resource
521 */
522 public boolean isAlias()
523 {
524 return getAlias()!=null;
525 }
526
527 /* ------------------------------------------------------------ */
528 /**
529 * @return The canonical Alias of this resource or null if none.
530 */
531 public URI getAlias()
532 {
533 return null;
534 }
535
536 /* ------------------------------------------------------------ */
537 /** Get the resource list as a HTML directory listing.
538 * @param base The base URL
539 * @param parent True if the parent directory should be included
540 * @return String of HTML
541 * @throws IOException if unable to get the list of resources as HTML
542 */
543 public String getListHTML(String base,boolean parent)
544 throws IOException
545 {
546 base=URIUtil.canonicalPath(base);
547 if (base==null || !isDirectory())
548 return null;
549
550 String[] ls = list();
551 if (ls==null)
552 return null;
553 Arrays.sort(ls);
554
555 String decodedBase = URIUtil.decodePath(base);
556 String title = "Directory: "+deTag(decodedBase);
557
558 StringBuilder buf=new StringBuilder(4096);
559 buf.append("<HTML><HEAD>");
560 buf.append("<LINK HREF=\"").append("jetty-dir.css").append("\" REL=\"stylesheet\" TYPE=\"text/css\"/><TITLE>");
561 buf.append(title);
562 buf.append("</TITLE></HEAD><BODY>\n<H1>");
563 buf.append(title);
564 buf.append("</H1>\n<TABLE BORDER=0>\n");
565
566 if (parent)
567 {
568 buf.append("<TR><TD><A HREF=\"");
569 buf.append(URIUtil.addPaths(base,"../"));
570 buf.append("\">Parent Directory</A></TD><TD></TD><TD></TD></TR>\n");
571 }
572
573 String encodedBase = hrefEncodeURI(base);
574
575 DateFormat dfmt=DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
576 DateFormat.MEDIUM);
577 for (int i=0 ; i< ls.length ; i++)
578 {
579 Resource item = addPath(ls[i]);
580
581 buf.append("\n<TR><TD><A HREF=\"");
582 String path=URIUtil.addPaths(encodedBase,URIUtil.encodePath(ls[i]));
583
584 buf.append(path);
585
586 if (item.isDirectory() && !path.endsWith("/"))
587 buf.append(URIUtil.SLASH);
588
589 // URIUtil.encodePath(buf,path);
590 buf.append("\">");
591 buf.append(deTag(ls[i]));
592 buf.append(" ");
593 buf.append("</A></TD><TD ALIGN=right>");
594 buf.append(item.length());
595 buf.append(" bytes </TD><TD>");
596 buf.append(dfmt.format(new Date(item.lastModified())));
597 buf.append("</TD></TR>");
598 }
599 buf.append("</TABLE>\n");
600 buf.append("</BODY></HTML>\n");
601
602 return buf.toString();
603 }
604
605 /**
606 * Encode any characters that could break the URI string in an HREF.
607 * Such as <a href="/path/to;<script>Window.alert("XSS"+'%20'+"here");</script>">Link</a>
608 *
609 * The above example would parse incorrectly on various browsers as the "<" or '"' characters
610 * would end the href attribute value string prematurely.
611 *
612 * @param raw the raw text to encode.
613 * @return the defanged text.
614 */
615 private static String hrefEncodeURI(String raw)
616 {
617 StringBuffer buf = null;
618
619 loop:
620 for (int i=0;i<raw.length();i++)
621 {
622 char c=raw.charAt(i);
623 switch(c)
624 {
625 case '\'':
626 case '"':
627 case '<':
628 case '>':
629 buf=new StringBuffer(raw.length()<<1);
630 break loop;
631 }
632 }
633 if (buf==null)
634 return raw;
635
636 for (int i=0;i<raw.length();i++)
637 {
638 char c=raw.charAt(i);
639 switch(c)
640 {
641 case '"':
642 buf.append("%22");
643 continue;
644 case '\'':
645 buf.append("%27");
646 continue;
647 case '<':
648 buf.append("%3C");
649 continue;
650 case '>':
651 buf.append("%3E");
652 continue;
653 default:
654 buf.append(c);
655 continue;
656 }
657 }
658
659 return buf.toString();
660 }
661
662 private static String deTag(String raw)
663 {
664 return StringUtil.sanitizeXmlString(raw);
665 }
666
667 /* ------------------------------------------------------------ */
668 /**
669 * @param out the output stream to write to
670 * @param start First byte to write
671 * @param count Bytes to write or -1 for all of them.
672 * @throws IOException if unable to copy the Resource to the output
673 */
674 public void writeTo(OutputStream out,long start,long count)
675 throws IOException
676 {
677 try (InputStream in = getInputStream())
678 {
679 in.skip(start);
680 if (count<0)
681 IO.copy(in,out);
682 else
683 IO.copy(in,out,count);
684 }
685 }
686
687 /* ------------------------------------------------------------ */
688 /**
689 * Copy the Resource to the new destination file.
690 * <p>
691 * Will not replace existing destination file.
692 *
693 * @param destination the destination file to create
694 * @throws IOException if unable to copy the resource
695 */
696 public void copyTo(File destination)
697 throws IOException
698 {
699 if (destination.exists())
700 throw new IllegalArgumentException(destination + " exists");
701
702 try (OutputStream out = new FileOutputStream(destination))
703 {
704 writeTo(out,0,-1);
705 }
706 }
707
708 /* ------------------------------------------------------------ */
709 /**
710 * Generate a weak ETag reference for this Resource.
711 *
712 * @return the weak ETag reference for this resource.
713 */
714 public String getWeakETag()
715 {
716 return getWeakETag("");
717 }
718
719 public String getWeakETag(String suffix)
720 {
721 try
722 {
723 StringBuilder b = new StringBuilder(32);
724 b.append("W/\"");
725
726 String name=getName();
727 int length=name.length();
728 long lhash=0;
729 for (int i=0; i<length;i++)
730 lhash=31*lhash+name.charAt(i);
731
732 B64Code.encode(lastModified()^lhash,b);
733 B64Code.encode(length()^lhash,b);
734 b.append(suffix);
735 b.append('"');
736 return b.toString();
737 }
738 catch (IOException e)
739 {
740 throw new RuntimeException(e);
741 }
742 }
743
744 /* ------------------------------------------------------------ */
745 public Collection<Resource> getAllResources()
746 {
747 try
748 {
749 ArrayList<Resource> deep=new ArrayList<>();
750 {
751 String[] list=list();
752 if (list!=null)
753 {
754 for (String i:list)
755 {
756 Resource r=addPath(i);
757 if (r.isDirectory())
758 deep.addAll(r.getAllResources());
759 else
760 deep.add(r);
761 }
762 }
763 }
764 return deep;
765 }
766 catch(Exception e)
767 {
768 throw new IllegalStateException(e);
769 }
770 }
771
772 /* ------------------------------------------------------------ */
773 /** Generate a properly encoded URL from a {@link File} instance.
774 * @param file Target file.
775 * @return URL of the target file.
776 * @throws MalformedURLException if unable to convert File to URL
777 */
778 public static URL toURL(File file) throws MalformedURLException
779 {
780 return file.toURI().toURL();
781 }
782 }